Back to Course|System Design Interview Mastery: Scalable Architecture from Estimation to Production
Lab

Build an Event-Sourced Order Service

35 min
Intermediate
Unlimited free attempts

Instructions

Objective

Build a complete event-sourced order service that demonstrates the core data architecture patterns covered in the lesson: event sourcing, CQRS, the Saga pattern, and snapshot optimization. This lab mirrors the architecture you would propose in a system design interview for an e-commerce order service.

Architecture Overview

Commands                    Event Store (append-only)
   |                              |
   v                              v
Command Handler -----> Events -> [E1, E2, E3, E4, ...]
                                  |              |
                                  v              v
                          Order Projection   Analytics Projection
                          (current state)    (revenue, counts)
                                  |
                                  v
                          Snapshot Manager
                          (periodic snapshots)

Saga Orchestrator
   |
   +---> Payment Step ----> (success) ---> Inventory Step
   |                                           |
   |                                      (success) ---> Shipping Step
   |                                                        |
   +--- Compensation <--- (failure at any step) <-----------+

Requirements

You will implement 8 TypeScript files that work together:

FILE 1: src/events/event-store.ts — Append-Only Event Store

  • Implement an in-memory append-only event store
  • Each event has an aggregateId (e.g., orderId) and a sequential version number
  • Support optimistic concurrency control: when appending events, pass the expected version; reject if there is a version conflict (another write happened concurrently)
  • Implement append(aggregateId, events, expectedVersion): throws on version mismatch
  • Implement getEvents(aggregateId): returns all events for an aggregate in order
  • Implement getEventsAfterVersion(aggregateId, afterVersion): returns events after a given version (used with snapshots)
  • Implement getAllEvents(): returns all events across all aggregates (for analytics projection)

FILE 2: src/events/event-types.ts — Domain Events

  • Define a base DomainEvent interface with: type, aggregateId, version, timestamp, metadata (correlationId, causationId)
  • Define order domain events: OrderCreated, ItemAdded, PaymentProcessed, OrderShipped, OrderCancelled
  • Each event carries the data specific to what happened (e.g., OrderCreated includes customerId and items)
  • Use TypeScript discriminated unions for type safety

FILE 3: src/commands/command-handler.ts — Command Handler

  • Handle commands: PlaceOrder, AddItem, ProcessPayment, ShipOrder, CancelOrder
  • Each command handler: loads current state from the event store, validates business rules, produces new events
  • Business rules to enforce:
    • Cannot add items to a cancelled or shipped order
    • Cannot process payment on a cancelled order
    • Cannot ship an order that has not been paid
    • Cannot cancel an already shipped order
  • Use the event store's optimistic concurrency for safe writes

FILE 4: src/projections/order-projection.ts — Order Read Model

  • Materialize the current order state by replaying events
  • The read model should include: orderId, customerId, items, status, totalAmount, paymentId, shippedAt, cancelledAt, version
  • Statuses: pending, paid, shipped, cancelled
  • Implement getOrder(orderId): returns current order state
  • Implement rebuildFromEvents(events): rebuilds state from a list of events

FILE 5: src/projections/analytics-projection.ts — Analytics Projection

  • Maintain aggregate metrics across all orders:
    • totalRevenue: sum of all paid order amounts
    • totalOrders: count of all orders created
    • averageOrderValue: totalRevenue / number of paid orders
    • ordersByStatus: count of orders in each status
    • cancelledRevenue: total amount of cancelled orders (revenue lost)
  • Process events incrementally (do not replay all events each time)
  • Implement processEvent(event) and getAnalytics()

FILE 6: src/saga/order-saga.ts — Saga Orchestrator

  • Implement an orchestration-based Saga for the order workflow with 3 steps:
    1. Payment: Charge the customer (simulate with async function)
    2. Inventory: Reserve items (simulate with async function)
    3. Shipping: Schedule shipment (simulate with async function)
  • Each step can succeed or fail (use configurable failure simulation)
  • On failure at any step, execute compensation for all previously completed steps in reverse order:
    • Shipping failure: release inventory, refund payment
    • Inventory failure: refund payment
  • Track saga state: pending, payment_completed, inventory_reserved, completed, compensating, compensated, failed
  • Log each step for visibility

FILE 7: src/snapshots/snapshot-manager.ts — Snapshot Manager

  • Store periodic snapshots of aggregate state to avoid replaying all events
  • Implement saveSnapshot(aggregateId, state, version): save a snapshot at a given version
  • Implement getLatestSnapshot(aggregateId): return the most recent snapshot
  • Implement shouldSnapshot(eventCount): return true if a snapshot should be taken (e.g., every 5 events for this lab; in production, every 100)
  • Implement loadAggregateState(aggregateId, eventStore, projector):
    1. Load the latest snapshot (if any)
    2. Get events after the snapshot version
    3. Apply those events to the snapshot state
    4. Return the current state

FILE 8: src/index.ts — Main Entry

  • Wire everything together
  • Run a test scenario that demonstrates:
    1. Create an order with 2 items
    2. Add another item to the order
    3. Process payment
    4. Ship the order
    5. Query the order projection for current state
    6. Query analytics for aggregate metrics
    7. Demonstrate snapshot save and load
    8. Run the Saga orchestrator (once with success, once with a simulated failure showing compensation)
  • Print results at each step to demonstrate the full workflow

Hints

  • Use Map<string, DomainEvent[]> for the in-memory event store, keyed by aggregateId
  • For optimistic concurrency, compare the expected version against the current number of events for that aggregate
  • The saga orchestrator can use simple async/await with try/catch for step execution and compensation
  • For the analytics projection, track a processedEventIds set to avoid processing the same event twice
  • The snapshot manager is essentially a Map<string, { state: OrderState; version: number }>
  • Use console.log with clear labels (e.g., [EventStore], [Saga], [Projection]) to show what is happening

What to Submit

Your submission should contain 8 file sections in the editor below, each implementing one of the files described above.


Grading Rubric

Event store with append-only log, versioning, and optimistic concurrency control. The store must assign sequential version numbers, reject appends when the expected version does not match the current stream length (throw ConcurrencyError), and support retrieval by aggregate, by version range, and across all aggregates.15 points
Domain events with proper typing and metadata. All 5 events (OrderCreated, ItemAdded, PaymentProcessed, OrderShipped, OrderCancelled) must extend BaseDomainEvent, include event-specific data fields, form a discriminated union via the type field, and include EventMetadata with correlationId and causationId.10 points
Command handler validating business rules and producing events. Must handle all 5 commands, enforce all 4 business rules (no items after cancel/ship, no payment after cancel, no ship before payment, no cancel after ship), use optimistic concurrency when appending, and return the produced event.15 points
Order projection materializing current state from event replay. The rebuildFromEvents function must correctly handle all 5 event types, maintaining accurate items list, status transitions, totalAmount calculation, and optional fields (paymentId, shippedAt, cancelledAt). The rebuildFromSnapshot function must apply events on top of existing state.15 points
Analytics projection maintaining aggregate metrics. Must incrementally process events (not replay all each time), track totalRevenue, totalOrders, paidOrders, averageOrderValue, ordersByStatus counts, and cancelledRevenue. Must be idempotent (skip already-processed events).10 points
Saga orchestrator with compensation logic on step failure. Must define 3 steps (payment, inventory, shipping) each with execute and compensate functions. On failure at any step, must compensate all previously completed steps in reverse order. Must track saga status through all states and maintain a detailed log of each action.20 points
Snapshot manager for performance optimization. Must save snapshots with aggregate state and version, retrieve the latest snapshot, determine when to snapshot based on event count interval, and load aggregate state by combining snapshot with newer events from the event store.10 points
Main entry demonstrating full workflow. Must wire all components together and run a complete scenario: create order, add item, process payment, ship order, query projection, query analytics, demonstrate snapshot save/load, and run saga (both success and failure with compensation). Output should be clear and labeled.5 points

Checklist

0/8

Your Solution

Unlimited free attempts
FREE WEEKLY NEWSLETTER

Stay on the Nerd Track

One email per week — courses, deep dives, tools, and AI experiments.

No spam. Unsubscribe anytime.