System design template

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.

Download for macOS
v0.1.33 · .dmg · Apple Silicon & Intel
Signed & notarized by Apple · opens without a Gatekeeper warning
sha256 698955a0187bc039f4c74f5d05a9f10fbb27376a45788a0a241d1326b73873c7
Download for Windows instead
$curl -fsSL https://lucidtrain.com/install.sh | sh
Event-driven microservices architecture. 12 components across 4 tiers.
Event-driven microservices architecture. Rendered by the same ELK layout engine the app runs: the agent emits components, tiers and edges, and the engine places them, so the boxes cannot overlap.

The components

Every row below is read from the graph that produced the diagram above, so the two cannot disagree.

ComponentTierWhy it is there
ClientClientSupporting component
API gatewayEdgeSupporting component
OrdersApplicationPublishes, never calls the others directly
InventoryApplicationSupporting component
ShippingApplicationSupporting component
BillingApplicationSupporting component
Saga orchestratorApplicationCompensating actions, because there is no distributed transaction
Outbox relayApplicationPublishes only what committed
KafkaDataPartitioned by aggregate id to keep ordering
Schema registryDataBackward compatibility is enforced here or nowhere
Per-service DBsDataNo shared database, that is the whole point
Dead letter queueDataSupporting 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.

shell
$ Diagram event-driven microservices: an orders service publishing through a transactional outbox to Kafka, inventory, shipping and billing consuming, a saga orchestrator handling compensation, a schema registry, per-service databases and a dead letter queue.

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.

FAQ

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