Lab
Build an Event-Sourced Order Service
35 min
IntermediateUnlimited 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 sequentialversionnumber - 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
DomainEventinterface 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.,
OrderCreatedincludes 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 amountstotalOrders: count of all orders createdaverageOrderValue: totalRevenue / number of paid ordersordersByStatus: count of orders in each statuscancelledRevenue: total amount of cancelled orders (revenue lost)
- Process events incrementally (do not replay all events each time)
- Implement
processEvent(event)andgetAnalytics()
FILE 6: src/saga/order-saga.ts — Saga Orchestrator
- Implement an orchestration-based Saga for the order workflow with 3 steps:
- Payment: Charge the customer (simulate with async function)
- Inventory: Reserve items (simulate with async function)
- 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):- Load the latest snapshot (if any)
- Get events after the snapshot version
- Apply those events to the snapshot state
- Return the current state
FILE 8: src/index.ts — Main Entry
- Wire everything together
- Run a test scenario that demonstrates:
- Create an order with 2 items
- Add another item to the order
- Process payment
- Ship the order
- Query the order projection for current state
- Query analytics for aggregate metrics
- Demonstrate snapshot save and load
- 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
processedEventIdsset to avoid processing the same event twice - The snapshot manager is essentially a
Map<string, { state: OrderState; version: number }> - Use
console.logwith 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/8Your Solution
Unlimited free attempts