01. Events vs Commands — Facts already happened, orders may still fail¶
~14 min read. Name messages badly, and services start arguing.
Built on the ELI5 in 00-eli5.md. The notice — a pinned message everyone can inspect — now splits into facts and instructions.
Start with verb tense¶
An event speaks in past tense. A command speaks in imperative tone. Events say payment_received, order_cancelled, rider_assigned. Commands say charge_card, cancel_order, assign_rider. That tense difference is not grammar decoration. It tells receivers whether they may refuse. Treat the notice as a finished fact before many readers react. When you mix the two, ownership becomes blurry.
Diagram:
┌───────────────┐ ┌───────────────┐
│ ChargePayment │ ─→ │ may reject │
└───────────────┘ └───────────────┘
┌───────────────┐ ┌───────────────┐
│ PaymentCaptured│ ─→ │ cannot unhappen│
└───────────────┘ └───────────────┘
- Ask, "Did this already happen?" before naming a message.
- If yes, prefer an event name with a completed action.
- If no, you are probably sending a command.
Worked example: 1. Checkout validates cart totals and stock first. 2. It sends ChargePayment because permission is still pending. 3. Payments may reject for fraud or insufficient balance. 4. After success, Payments emits PaymentCaptured. 5. Inventory and Email react without negotiating again.
Tense tells you whether the receiver decides, or merely learns.
Rejection changes system shape¶
Commands can be accepted, rejected, delayed, or timed out. Events cannot be un-happened by consumer opinion. That is why commands need a clear owner. One service owns the decision and response. Many services may listen to one event. Late listeners still learn the same past fact. Early failure handling stays near the command receiver. Downstream reactions happen after certainty appears.
Diagram:
Client ─→ Command API ─→ Payments
│
├─ reject → Client sees failure
└─ accept → PaymentCaptured
├→ Email
└→ Ledger
- Put validation and business authority near the command handler.
- Publish events only after that authority commits state.
- This keeps uncertainty local and facts reusable.
Worked example: 1. User clicks register on a learning app. 2. API sends CreateStudentProfile to Accounts. 3. Accounts rejects duplicate email immediately. 4. On success, it emits StudentRegistered. 5. CRM, billing, and welcome email subscribe separately.
A rejectable message should not masquerade as a fact.
Coupling lives in message direction¶
A command usually names the receiver responsibility. SendWelcomeEmail assumes an email service must exist. An event hides future listeners from the producer. UserRegistered does not care who reacts later. This reduces coupling across teams and deployments. New consumers can arrive without producer changes. Old consumers can leave without breaking emitters. That freedom is the real architectural prize.
Diagram:
- Commands create a named dependency on one owner.
- Events create a looser dependency on a shared contract.
- Loose coupling matters when products grow faster than diagrams.
Worked example: 1. Order service emits OrderPlaced after database commit. 2. Search indexer builds a read model. 3. Analytics counts conversion funnels. 4. Notification service sends confirmation messages. 5. Order service never changes for these new listeners.
Use events when many reactions should stay outside producer knowledge.
Design contracts with honest intent¶
Keep commands narrow, actionable, and owned by one boundary. Keep events descriptive, immutable, and rich enough for listeners. Do not turn events into hidden commands by naming tricks. OrderPlacedPleaseReserveInventory is confused design. Also avoid mutable events like OrderMaybePaid. State uncertainty belongs in workflow decisions. Message naming becomes easier when boundaries are honest. Then retries and idempotency become easier later.
Diagram:
Bad: Checkout ─→ PleaseShipOrderSoon
Good: Checkout ─→ OrderPaid
Warehouse ─→ CreateShipment
Shipping ─→ ShipmentCreated
- Prefer one business fact per event.
- Prefer one business decision per command.
- Keep payload fields stable, explicit, and versionable.
Worked example: 1. Restaurant app confirms order payment. 2. It emits OrderPaid with order_id and paid_at. 3. Kitchen service decides whether to start preparation. 4. Delivery service waits for FoodReady later. 5. Each step names one fact or one decision.
Honest names prevent accidental coupling and hidden workflow bugs.
Where this lives in the wild¶
You will hear this distinction in teams that ship across service boundaries daily.
-
Swiggy dispatch platform engineer emits RiderArrivedAtRestaurant for tracking. The matching engine still receives AssignRider as a private command.
-
Razorpay payments backend engineer sends CapturePayment to one authority. Ledger and risk systems later consume PaymentCaptured as an event.
-
Flipkart order orchestration engineer publishes OrderShipped for many listeners. Warehouse allocation still begins from ReserveInventory commands.
-
Netflix playback data engineer pushes PlaybackStarted events into analytics streams. Device control services still accept imperative PauseStream commands.
-
Uber marketplace engineer emits TripCompleted for billing and incentives. Dispatch first issues AcceptTrip commands to one driver app.
Pause and recall¶
Pause here. Check whether the distinction feels natural in your own words.
- Why can one command have exactly one business owner?
- Why does a past-tense event usually reduce producer coupling?
- How would you rename SendReceipt if payment already succeeded?
- What breaks when a rejectable action is published like a fact?
Say the answer aloud before reading ahead tomorrow.
Interview Q&A¶
These are short answers an interviewer can trust quickly.
Q: When should you choose a command over an event? A: Choose a command when one service must decide, validate, or reject the action. Common wrong answer to avoid: "Commands and events are basically the same message with different names."
Q: Why are events usually immutable? A: Because consumers should rely on a stable past fact, not a changing story. Common wrong answer to avoid: "We can update the event later if business meaning changes."
Q: How do commands and events affect coupling? A: Commands couple sender to one owner, while events decouple producer from downstream listeners. Common wrong answer to avoid: "Events remove all coupling completely."
Q: Can a workflow use both commands and events together? A: Yes. Use commands for decisions, then publish events after state commits. Common wrong answer to avoid: "Pick one style globally and ban the other."
Keep answers crisp, then add trade-offs only when asked.
Apply now (5 min)¶
Take one flow from your current project, maybe signup or checkout. List every message crossing service boundaries in that flow. Mark each as rejectable instruction or finished fact. Rename at least two muddy messages using tense and ownership. Then say which service owns each command clearly. Sketch from memory: - one command path with success and rejection arrows, - one event fan-out path with three listeners, - one sentence explaining why producer coupling changed.
Bridge. → 02-queues-vs-streams.md