Software Design Reference Master Designer · System Trade-Offs

Lesson 0031 · Master Designer · Module 2

Synchronous vs Asynchronous Systems

Sync is simple until it isn't fast; async is fast until it isn't simple. The Master's move: choose per workflow, not per system — and know the failure modes you're buying.

Mission tie-in: the first System Trade-Off — where lesson 0025's event-driven machinery becomes a measured decision instead of a default.

Knowledge: the ledger of the two modes

SynchronousAsynchronous
CallerGets the result nowGets an acknowledgment; result later
FlowVisible in one call stackDistributed across handlers (lesson 0025's honest cost)
FailureFails in your face, retryable by the callerOrdering, retries, dead letters, visibility — a new failure zoo
WinsSimplicity, debuggability, consistencyDecoupling, resilience, latency hiding, scale

The deciding question is about the workflow: does the caller need the result to continue? Confirming an order needs the payment result now — sync. Sending the confirmation email doesn't gate anything — async. Most systems are a mix:

def place_order(cart: Cart, gateway: PaymentGateway) -> Order:
    charge = gateway.charge(cart.total, cart.token)      # sync: caller needs it
    order = order_service.confirm(cart, charge.id)
    bus.publish(OrderConfirmed(order))                   # async: reactions don't gate
    return order

The async failure modes you must be able to name before you adopt it: ordering (handlers may see events out of order), at-least-once delivery (idempotency needed), dead letters (what happens when a handler keeps failing), and observability (the flow isn't in one stack trace). If you cannot answer "what happens when the handler dies mid-way?" — the workflow isn't ready for async.

The asymmetric default: sync unless a concrete reason demands async. Async-everywhere is the pattern that turns every feature into a distributed-systems problem (lesson 0025's cost, now at system scale).
Field notes · the async risk inventory, answered
Failure modeThe question you must answer firstA concrete answer
out-of-order deliveryWhat if OrderShipped arrives before OrderConfirmed?Key handlers by order id; ignore events older than the state.
at-least-once deliveryWhat happens when this handler runs twice?An idempotency key, or INSERT … ON CONFLICT DO NOTHING.
a handler that keeps failingWhere does message five go after three attempts?A dead-letter queue that a human actually watches.
no single stack traceHow do you follow one order across six handlers?A correlation id on every event and every log line.

Skill: which mode for this workflow?

The caller must know the payment result before confirming. The payment call should be:

The confirmation email doesn't gate anything. It should be:

Before adopting async, you must be able to answer:

Practice on your own code

List the workflows in your system. For each, mark: does the caller gate on the result? Where failure modes exist (ordering, duplicates, dead handlers), write the answer to "what happens if the handler dies mid-way?" — on paper, before you build more async.

Reveal: a workflow-by-workflow decision

An e-commerce system: place order — sync (payment gates confirmation). Send receipt — async (event, idempotent by order id). Update inventory — async but ordered per SKU, with a dead-letter queue for the warehouse sync. Analytics — async, best-effort, no dead-letter (loss is acceptable). Four workflows, three modes, each decision written down with its failure answer.

Your win

You can classify each workflow by its gating requirement, and you carry the async failure inventory — ordering, at-least-once, dead letters, observability — as a checklist before adopting it.

Read and watch deeper

Walk your workflow list past your agent-teacher and classify each one together.