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
Choreography or orchestration?
Choreography, where services react to each other's events, is simpler for short flows and becomes very hard to follow beyond about three steps. Orchestration puts the sequence in one place you can read, at the cost of a component that knows about everyone. Long workflows with compensation are almost always better orchestrated.
Why an outbox instead of publishing directly?
Because writing to the database and publishing to the broker are two operations that cannot share a transaction, so a crash between them either loses the event or publishes something that was rolled back. The outbox makes the event as durable as the state change that caused it.
How do you query across services?
Either call the owner, accepting the coupling and the latency, or build a read model that consumes events from several services. The read model is eventually consistent, which has to be acceptable to whoever is reading it.
Is this over-engineering for a small team?
Frequently, yes. The costs, sagas, schema governance, tracing, are paid immediately, while the benefits arrive when independent deployment and scaling actually matter. A modular monolith with the same boundaries gets much of the design benefit at a fraction of the operational cost.
More templates
- LLM Inference Service DesignGPUs are the budget, so almost every decision here is about keeping them busy without letting the queue destroy tail latency.
- CI/CD Pipeline ArchitectureTwo things decide whether people trust a pipeline: how fast it is, and whether a red build actually means something is broken.
- Ride Sharing System DesignEvery driver streams their position every few seconds whether or not anyone is riding, which makes this one of the few consumer systems where writes dominate reads.
- Video Streaming System DesignAround ninety-nine percent of the bytes never touch your servers, which means most of this design is about preparing files so the CDN can do the work.
Last updated