Post, edit, and delete listings: Authenticated users create classified ads with a title, description, category, price, location, and optional images. They can update or remove their own listings at any time. This is the core write path of the system.
Browse listings by category and location: Users navigate a hierarchical category tree (for sale > electronics > phones) and filter by geographic region. Most users start by browsing a specific category in their city, so this path must be fast.
Search with keyword and filter combinations: Users search by keywords and apply filters for category, price range, location, and date posted. This is the most performance-sensitive read path because it hits the search index with complex multi-field queries.
User authentication: Users register with email and password, then authenticate to manage their listings. Only the listing owner can edit or delete a listing.
Image upload: Sellers attach photos to listings. Images are stored externally and served through a CDN for fast loading.
High availability (99.9% uptime): The platform must be accessible around the clock. Users posting time-sensitive listings (job openings, rental vacancies) cannot tolerate downtime.
Low-latency search and browsing: Search results and category pages load in under 200ms. Slow results drive users to competing platforms.
Horizontal scalability: The system grows by adding servers, not upgrading them. Both the application layer and data stores scale independently to handle increasing listings and traffic.
Define the APIs expected from the system. This is your chance to analyze and define the read and write paths so that you can come up with the high-level design...
POST /v1/listings
Headers: Authorization: Bearer {token}
Body: {
title: string,
description: string,
category_id: number,
price: number (optional),
location: { city: string, state: string, zip: string },
image_ids: [string] (optional)
}
Response: 201 Created { listing_id: string, created_at: ISO-8601 }
The image_ids reference previously uploaded images rather than embedding image data in the listing creation request. This separation means image upload (slow, large payload) does not block listing creation (fast, small payload).
PUT /v1/listings/:listing_id
Headers: Authorization: Bearer {token}
Body: { title?, description?, category_id?, price?, location? }
Response: 200 OK { listing_id: string, updated_at: ISO-8601 }
DELETE /v1/listings/:listing_id
Headers: Authorization: Bearer {token}
Response: 204 No Content
Both PUT and DELETE verify the authenticated user owns the listing before proceeding. Attempting to modify another user's listing returns 403 Forbidden.
GET /v1/listings/search?q=honda+civic&category=vehicles&min_price=5000&max_price=15000&location=san-francisco&sort=date&page=1&limit=20
Response: 200 OK {
results: [{ listing_id, title, price, location, thumbnail_url, created_at }],
total: number,
page: number,
pages: number
}
Search results return lightweight summaries (no full description) to minimize payload size. The client fetches the full listing only when the user clicks through.
GET /v1/listings/:listing_id
Response: 200 OK { listing_id, title, description, category, price, location, images: [url], user: { id, name }, created_at }
POST /v1/images/upload-url
Headers: Authorization: Bearer {token}
Body: { filename: string, content_type: string }
Response: 200 OK { image_id: string, upload_url: string }
The server returns a pre-signed S3 URL. The client uploads the image directly to S3, bypassing the application server entirely. This keeps image traffic off the API servers and lets S3 handle the heavy lifting of large file ingestion.
Describe the overall system architecture. Identify the main components needed to solve the problem end-to-end. Use the diagramming tool to create a block diagram.
The architecture splits into three main paths: the write path (creating and managing listings), the read path (browsing and viewing listings), and the search path (querying Elasticsearch). Each path has different performance requirements, which is why they use different infrastructure.
High-level architecture showing the write, read, and search paths
Level Expectations
Mid-level: sketch the basic client-server flow with a database but miss the search infrastructure.
Senior: design the full pipeline with Elasticsearch, CDN for images, and the sync mechanism between PostgreSQL and the search index.
Staff: additionally consider geo-partitioned search clusters, multi-tenant isolation for moderation tooling, and the operational tradeoffs of eventually consistent search indexes.
API Gateway / Load Balancer: The single entry point for all client requests. Handles SSL termination, JWT authentication verification, rate limiting, and routes requests to the appropriate backend service. Clients never talk directly to internal services.
Listing Service: Owns the listing lifecycle: create, update, delete, and retrieve individual listings. Reads and writes to PostgreSQL. Publishes listing change events to Kafka for downstream consumers (search indexing, cache invalidation).
Search Indexer / Cache Invalidator: A Kafka consumer that processes listing change events. It updates the Elasticsearch index so search results reflect the latest data, and removes stale entries from the Redis cache so subsequent reads fetch fresh data from PostgreSQL.
Search Service: Accepts search queries from the gateway, translates them into Elasticsearch queries with filters for category, location, price, and keywords, and returns ranked, paginated results. This service is stateless and horizontally scalable.
Media Service: Generates pre-signed S3 URLs for image upload, triggers image processing (resizing, thumbnail generation), and manages image metadata. Image processing runs asynchronously so users do not wait for it.
Auth Service: Handles user registration, login, and JWT token issuance. The API Gateway verifies JWTs on every request but only routes to the Auth Service for login and registration.
On the write path, a new listing flows through: Client -> API Gateway -> Listing Service -> PostgreSQL (store) + Kafka (publish event). Downstream, the Search Indexer consumes the event and updates Elasticsearch, and the Cache Invalidator removes any stale entries from the Redis cache.
On the read path, a listing view flows through: Client -> CDN (images) + API Gateway -> Listing Service -> Redis (cache hit) or PostgreSQL (cache miss).
On the search path: Client -> API Gateway -> Search Service -> Elasticsearch -> return results. The search path primarily reads from Elasticsearch and Redis, falling back to PostgreSQL only for cache misses during result enrichment. Because the heavy lifting happens in Elasticsearch, search remains fast even when the database is under write load.
Deep dive into 2-3 key components. Explain how they work, how they scale, discuss tradeoffs, capacity, and any relevant algorithms or data structures.
The core architectural challenge of a classifieds platform is keeping the search index fast and in sync with the listing database. Every listing change in PostgreSQL must be reflected in Elasticsearch, and this synchronization must handle failures gracefully without blocking the user experience.
Search index synchronization via Kafka events with retry and reconciliation
The search index is the heart of the user experience. When someone searches for "used bicycle in Brooklyn under $200," the system must query across multiple dimensions (text relevance, category, geography, price) and return ranked results in under 100ms. This is why Elasticsearch exists in the architecture: it maintains inverted indexes for text, geo-indexes for location, and numeric range indexes for price, all queryable in a single request.
Index structure: Each Elasticsearch document mirrors a listing with fields mapped for their query type: title and description as analyzed text (for full-text search), category_id as a keyword (for exact match filtering), location as a geo_point (for distance queries), price as a numeric field (for range filters), and created_at as a date (for sorting and time-based filtering).
Multi-faceted queries: The Search Service constructs Elasticsearch bool queries that combine multiple filters. A typical search combines a must clause for keyword matching with filter clauses for category, price range, and geographic radius. Filters run on exact-match or range fields and are cacheable by Elasticsearch, so repeated similar queries benefit from the filter cache.
The sync pipeline flows: PostgreSQL write -> Kafka event -> Search Indexer -> Elasticsearch. This event-driven approach means the Listing Service never directly calls Elasticsearch.
Normal flow: The Listing Service writes to PostgreSQL, publishes a listing.created, listing.updated, or listing.deleted event to Kafka, and returns to the client. The Search Indexer consumer processes events from Kafka and applies the corresponding operation to Elasticsearch (index, update, or delete document).
Failure handling: If the Indexer fails to update Elasticsearch (cluster down, network error), the event stays in Kafka and is retried. Kafka's consumer offset tracking ensures no events are lost. If the Indexer crashes and restarts, it resumes from its last committed offset.
Safety net: A periodic reconciliation job (hourly) queries PostgreSQL for all active listing IDs and compares them against Elasticsearch. Any listings missing from the index are reindexed. Any listings in the index that no longer exist in PostgreSQL are removed. This catches edge cases that event-driven sync might miss (double failures, bugs in the indexer).
Key Insight
Three layers of defense for index consistency: event-driven sync (handles 99.9% of updates in seconds), Kafka retry (handles transient Elasticsearch failures), and hourly reconciliation (catches everything else). This defense-in-depth approach is a pattern interviewers love because it shows you do not rely on a single mechanism.
When a user submits a listing and the network times out before receiving the response, the client retries. Without idempotency, this creates a duplicate listing. The Listing Service uses a client-generated idempotency key (sent in the request header). On the first request, it stores the key with the created listing_id. On retry, it finds the existing key and returns the already-created listing instead of creating a duplicate.
When a popular listing's Redis cache entry expires, hundreds of simultaneous requests hit PostgreSQL for the same listing. This thundering herd can spike database load. The solution: the first request to find an expired cache entry acquires a distributed lock (using Redis SETNX with a short TTL), fetches from PostgreSQL, and repopulates the cache. All other concurrent requests wait briefly for the lock to release, then read from the newly populated cache.
Image upload pipeline using pre-signed URLs and async processing
Images follow a separate path from listing data because they have fundamentally different characteristics: large binary blobs (vs. small structured data), write-once (vs. updatable), and served via CDN (vs. served via API).
Upload: The client requests a pre-signed URL from the Media Service, then uploads the image directly to S3. This bypasses the API servers entirely: image bytes never flow through the application layer.
Processing: S3 triggers a notification when the upload completes. The Media Service generates thumbnails (150x150 for search results) and resized versions (800px wide for listing detail pages). Processed images are stored back in S3 with predictable naming conventions.
Delivery: Images are served through a CDN. The first request for an image goes to S3 (origin), and subsequent requests are served from CDN edge caches. With a 90% CDN cache hit rate, only 10% of image requests reach S3.
Every design decision involves a trade-off. The key to a strong interview answer is not just stating what you chose, but explaining what you gave up and why that trade-off is acceptable.
Using two data stores (PostgreSQL as source of truth, Elasticsearch for search) adds operational complexity: you must keep them in sync, monitor two systems, and handle consistency gaps. The alternative, using PostgreSQL's built-in full-text search (tsvector/tsquery), is simpler but breaks down at scale. PostgreSQL full-text search works well up to a few hundred thousand listings, but at 50M listings with 10K search QPS and multi-field faceted queries, it cannot compete with Elasticsearch's inverted indexes and distributed architecture.
The trade-off is acceptable because the consistency gap (new listings take 1-3 seconds to appear in search) has minimal user impact. Nobody notices a 2-second delay on a classifieds platform where listings stay active for days or weeks.
Updating Elasticsearch synchronously on every listing write would guarantee immediate search visibility but would add 20-50ms of latency to every write and couple the Listing Service to Elasticsearch availability. If Elasticsearch is slow or down, listing creation fails, even though PostgreSQL is healthy.
Async updates via Kafka decouple the services but introduce a brief search visibility delay and add operational complexity (Kafka cluster, consumer management). This is the right trade-off because write availability (users can always post listings) is more important than instant search visibility.
The design separates Listing, Search, Auth, and Media into independent services. This lets each scale independently: Search needs 10x more instances than Listing due to the read-heavy workload. The cost is inter-service communication overhead (network latency, serialization) and operational complexity (deploying and monitoring four services instead of one).
For a small team or early-stage product, starting with a modular monolith (single deployment, well-separated internal modules) and extracting services as scaling bottlenecks emerge is often the pragmatic choice. The Search module is typically the first to extract because its scaling needs diverge earliest.