Event-driven microservices architecture.
Distributed transactions do not exist here, so every consistency guarantee you want has to be rebuilt out of events, retries and compensation.
The components
Every row below is read from the graph that produced the diagram above, so the two cannot disagree.
| Component | Tier | Why it is there |
|---|---|---|
| Client | Client | Supporting component |
| API gateway | Edge | Supporting component |
| Orders | Application | Publishes, never calls the others directly |
| Inventory | Application | Supporting component |
| Shipping | Application | Supporting component |
| Billing | Application | Supporting component |
| Saga orchestrator | Application | Compensating actions, because there is no distributed transaction |
| Outbox relay | Application | Publishes only what committed |
| Kafka | Data | Partitioned by aggregate id to keep ordering |
| Schema registry | Data | Backward compatibility is enforced here or nowhere |
| Per-service DBs | Data | No shared database, that is the whole point |
| Dead letter queue | Data | Supporting component |
Design decisions worth arguing about
A diagram shows what was chosen. It does not show what it cost, and that is usually the part that matters in a review or an interview.
No shared database, which is the entire point
The moment two services read each other's tables, you have a distributed monolith: deployments couple, schema changes break things at a distance, and the service boundary is decorative. Enforcing separate stores is what makes the boundary real. The cost is that every query spanning services becomes an API call or a materialised projection, and joins that were free become code you maintain.
Sagas instead of transactions
Without a distributed transaction, a multi-service operation is a sequence of local commits, and failure partway through leaves the system inconsistent unless you explicitly undo the earlier steps. Compensating actions are that undo, and they are harder than they look because they are not true rollbacks: refunding a payment is a new fact, not the erasure of an old one. This is the largest single cost of the architecture and it is paid per workflow.
Partition by aggregate id to keep ordering
Kafka guarantees order within a partition and nothing across partitions. Keying by the aggregate id gives you ordered events per entity, which is almost always the ordering that matters, without needing a global sequence. The consequence is that a single hot aggregate is a single partition and therefore a throughput ceiling that adding consumers does not raise.
A schema registry, or compatibility is a rumour
Events outlive the code that wrote them, and consumers upgrade on their own schedule, so a producer changing a field breaks consumers at some unpredictable later time. A registry enforcing backward compatibility moves that failure to the producer's deploy, where someone can act on it. The cost is a component in the publish path and genuine constraints on how schemas may evolve, which teams experience as friction until the first time it prevents an outage.
How it changes with scale
Adding consumers is cheap, which is the appeal: a new service subscribes without anyone changing the producer. What grows painfully is the cognitive load, because no single place describes what happens when an order is placed, and debugging requires reconstructing a flow from several services' logs. That is what makes distributed tracing effectively mandatory here rather than merely useful.
Where it breaks first
A poison message. One event a consumer cannot process, retried forever, blocks its partition and stalls every event behind it. Without a dead letter queue and a retry limit, one malformed record halts a whole stream, and because the consumer is running and not erroring loudly, the symptom is silence rather than an alert.
Draw this yourself
Open the Diagram tab and describe the system. The agent emits a semantic graph rather than coordinates, so you can edit the components and the layout re-solves instead of drifting.
When the shape is right, Implement in code turns the canvas into a markdown specification, every component, every relationship and the notes, and starts a real turn in the Code tab with it.
Questions about this design
More templates
- URL Shortener System DesignThe canonical read-heavy system: roughly a hundred reads for every write, and a redirect that has to be fast enough that nobody notices it happened.
- Chat Application System DesignLong-lived connections change everything: the hard part is not storing messages, it is knowing which of your servers is holding the socket you need to write to.
- Payment System DesignThe only system on this list where being approximately right is indistinguishable from being wrong, and where the provider, not you, holds the truth.
- RAG Pipeline ArchitectureAlmost every RAG system that disappoints is failing at retrieval, not generation, and the architecture is what decides whether you can tell.