EN 中文

System Design Lab

A Job Board starts with PostgreSQL full-text retrieval and typed filters. A Search Service becomes a separate component only when candidate scoring, search features, or independent scaling becomes the binding constraint.

Adjust active jobs, search QPS, measured cache hits for reusable queries, keyword candidates, job updates, and freshness. See when PostgreSQL is enough, when popular results or a search session deserve a cache, and when Elasticsearch / OpenSearch should become an independent component.

Guided walkthrough

Reason about it one step at a time

Normal evolution scenarios

Click left to right for the intended demo path. Each card changes the workload inputs.

Workload

These are inputs, not preset architecture stages.

Recommended shape

Current architecture path
Job Board search architecture Whiteboard Job Board architecture: cache is conditional and appears only with evidence for popular normalized queries or stable search sessions; PostgreSQL stores truth and the CDC pipeline updates a derived Search Index. Clients APIs PostgreSQL Derived search Job seeker search + apply Employer publish + close Search API retrieve + rank Conditional cache hot results / session Job API source writes Application API validate active Job table source of truth PG indexes B-tree + GIN + GiST CDC / Outbox capture commits Event log / Queue buffer + replay Indexer versioned upsert Search service BM25 + filters
Clients
Job seeker Submits keywords and filters, then applies to a currently active job
Employer Creates, edits, pauses, or closes a Job
APIs
Search API Queries PostgreSQL indexes or runs BM25 + filters in a dedicated Search Service
Conditional cache Caches only popular first-page results or a bounded Top 1,000 session; long-tail misses still reach the search backend
Job API Writes Job changes to the PostgreSQL source of truth
Application API Rechecks the latest Job.status in PostgreSQL before accepting an application
PostgreSQL
Job table Stores Job, status, version, and typed search fields
PG indexes Active-only partial indexes retrieve, filter, and shrink ranking candidates
Derived search
CDC / Outbox CDC reads WAL, or a Relay reads outbox events committed in the Job transaction
Event log / Queue A durable log such as Kafka buffers, retries, and replays events; it is neither CDC nor source of truth
Indexer Consumes events and builds idempotent, versioned SearchDocument upserts
Search service Elasticsearch/OpenSearch independently stores and scales a derived inverted index

Bottlenecks

Candidates ranked per query

PostgreSQL search CPU pressure

Search latency vs p95 budget

Backend search traffic after cache

Search freshness lag

Top-K / total-count work

Why this changes

    Decision tradeoffs

    Typed filters

    PostgreSQL FTS

    Active-only partial indexes

    Conditional result / session cache

    Independent Search Service

    CDC / Outbox / Queue

    Application-time validation

    Source-backed rules

    These are the durable system-design claims behind the model. The exact slider thresholds are deliberately labeled as teaching assumptions.

    Verified rule

    A partial index stores only rows satisfying its predicate

    Active-only GIN, B-tree, and GiST indexes exclude years of closed history, but the query predicate must imply the index predicate.

    PostgreSQL — Partial Indexes
    Verified rule

    OpenSearch keyword search uses BM25 by default

    A dedicated Search Service treats full-text relevance, non-scoring filters, and Top-K retrieval as the primary workload rather than an add-on to a relational database.

    OpenSearch — Keyword search
    Verified rule

    Elasticsearch does not cache full search hits by default

    The shard request cache defaults to size=0 totals, aggregations, and suggestions. Popular Top 20 results require an explicit Search API cache such as Redis.

    Elasticsearch — Shard request cache
    Verified rule

    CDC captures committed row-level database changes

    CDC is the change-capture mechanism: a connector reads the database transaction log and emits a change-event stream. A Queue is an optional transport and buffer; the Indexer updates SearchDocuments.

    Debezium — CDC Architecture
    Verified rule

    Outbox commits business state and its event in one transaction

    Job API writes the Job row and outbox row together; a CDC connector then captures the outbox change. This avoids a PostgreSQL-success/message-send-failure dual-write split.

    Debezium — Outbox Event Router
    Verified rule

    A derived Search Index is near-real-time, not transactionally visible

    Document changes become searchable after refresh, so search freshness and Application API business correctness must remain separate boundaries.

    Elasticsearch — Near real-time search

    Teaching assumptions

    • Candidate, latency, and throughput formulas are conservative teaching models that expose an inflection point—not PostgreSQL, Elasticsearch, or OpenSearch benchmarks.
    • Keyword match rate and filter retention should come from the real query distribution; total row count alone cannot decide a search migration.
    • PostgreSQL indexes update in the Job transaction; a separate Search Service is modeled with an approximately one-second refresh plus indexing backlog.
    • The production path uses Outbox / CDC -> durable Queue -> Indexer. A small system may omit the Queue, but it must not synchronously dual-write PostgreSQL and the Search Service.
    • Result cache is off by default. Only measured reuse of normalized popular-query / first-page keys earns a 30-second TTL. A search session stores bounded Top 1,000 Job IDs for a few minutes to stabilize pagination, not accelerate the first query.
    • Regardless of which index serves search, the Application API uses current PostgreSQL Job.status for the final correctness decision.