Loading content...
You've learned what event sourcing is, how it works, and the tradeoffs involved. Now comes the practical question: Should you use event sourcing for your project?
This isn't a question with a universal answer. Event sourcing is a powerful pattern that introduces real complexity. The goal is not to use it everywhere, but to use it where it provides outsized value relative to its costs.
This page provides:
By the end of this page, you will have a concrete decision framework for evaluating event sourcing, understand domain characteristics that indicate good fit, see real-world examples of event sourcing in production, and know practical adoption strategies for introducing event sourcing to your organization.
Evaluate these criteria for your specific project. Not all criteria need to favor event sourcing—but the more that do, the stronger the case.
Tier 1: Strong Indicators (High Weight)
These factors, if present, make a compelling case for event sourcing.
| Criterion | Pro-ES Signal | Anti-ES Signal | Your Assessment |
|---|---|---|---|
| Audit Requirements | Regulatory mandate for complete history | No audit requirements | |
| Temporal Queries | Core business need for point-in-time state | Only current state matters | |
| Domain Model | Naturally event-driven; processes as flows | Simple CRUD entities | |
| Team Experience | Prior event sourcing experience | First ES project under deadline | |
| Consistency Tolerance | Eventual consistency acceptable | Immediate consistency required everywhere |
Tier 2: Supporting Factors (Medium Weight)
These strengthen the case but aren't decisive on their own.
| Criterion | Pro-ES Signal | Anti-ES Signal |
|---|---|---|
| Integration Complexity | Many downstream consumers | Single monolith, few integrations |
| Debugging Difficulty | Production issues are hard to reproduce | Issues are straightforward to diagnose |
| Schema Stability | Domain model is well-understood | Rapid prototyping, unclear requirements |
| Scale Expectations | High write volume with read scaling | Low scale, simple CRUD sufficient |
| Future Flexibility | Unknown future query patterns likely | Requirements are fixed and known |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
/** * Event Sourcing Decision Evaluator * A structured approach to assessing fit */ interface CriterionScore { criterion: string; weight: 'high' | 'medium' | 'low'; score: -2 | -1 | 0 | 1 | 2; // Strong anti, weak anti, neutral, weak pro, strong pro notes: string;} interface DecisionResult { recommendation: 'yes' | 'no' | 'consider'; totalScore: number; maxPossibleScore: number; percentageScore: number; criticalBlockers: string[]; strongIndicators: string[];} function evaluateEventSourcingFit(scores: CriterionScore[]): DecisionResult { const weights = { high: 3, medium: 2, low: 1 }; let totalScore = 0; let maxScore = 0; const criticalBlockers: string[] = []; const strongIndicators: string[] = []; for (const item of scores) { const weight = weights[item.weight]; totalScore += item.score * weight; maxScore += 2 * weight; // Max possible is +2 per criterion // Identify critical blockers (high weight + strongly negative) if (item.weight === 'high' && item.score <= -2) { criticalBlockers.push(`${item.criterion}: ${item.notes}`); } // Identify strong indicators (high weight + strongly positive) if (item.weight === 'high' && item.score >= 2) { strongIndicators.push(`${item.criterion}: ${item.notes}`); } } const percentage = ((totalScore + maxScore) / (2 * maxScore)) * 100; // Decision logic let recommendation: 'yes' | 'no' | 'consider'; if (criticalBlockers.length > 0) { recommendation = 'no'; } else if (strongIndicators.length >= 2 && percentage > 65) { recommendation = 'yes'; } else if (percentage > 50) { recommendation = 'consider'; } else { recommendation = 'no'; } return { recommendation, totalScore, maxPossibleScore: maxScore, percentageScore: percentage, criticalBlockers, strongIndicators, };} // Example evaluation for a financial services projectconst financialServicesEvaluation: CriterionScore[] = [ { criterion: 'Audit Requirements', weight: 'high', score: 2, notes: 'SOX compliance mandates complete transaction history', }, { criterion: 'Temporal Queries', weight: 'high', score: 2, notes: 'Regulators require point-in-time account state for audits', }, { criterion: 'Domain Model', weight: 'high', score: 2, notes: 'Trading is naturally event-driven (orders, fills, settlements)', }, { criterion: 'Team Experience', weight: 'high', score: 1, notes: 'Some team members have prior ES experience', }, { criterion: 'Consistency Tolerance', weight: 'high', score: 0, notes: 'Needs strong consistency for balance checks', }, { criterion: 'Integration Complexity', weight: 'medium', score: 2, notes: 'Multiple downstream systems (risk, reporting, notifications)', },]; const result = evaluateEventSourcingFit(financialServicesEvaluation);// Result: { recommendation: 'yes', strongIndicators: [...], ... }A single critical blocker (like team inexperience under tight deadline, or hard requirement for immediate consistency everywhere) can override many positive signals. Be honest about blockers—they won't disappear because of enthusiasm for the pattern.
Some domains are naturally event-oriented—the business processes they model are inherently sequences of things happening. In these domains, event sourcing aligns with how domain experts already think.
Signals That Your Domain Is Event-Oriented:
| Domain | Event-Oriented? | Key Events | Why ES Fits (or Doesn't) |
|---|---|---|---|
| Financial Trading | Yes | OrderPlaced, Filled, Settled, Cancelled | Trades are events; regulatory audit required; temporal queries mandatory |
| Healthcare Records | Yes | DiagnosisRecorded, PrescriptionIssued, TreatmentStarted | Medical history is sacred; changes must be explained; point-in-time state critical |
| E-commerce Orders | Moderate | OrderPlaced, Shipped, Delivered, Returned | Order lifecycle is event-driven but product catalog is CRUD |
| Content Management | Low | ArticlePublished, ArticleEdited | Content is mutable state; history less important than current version |
| User Profiles | Low | ProfileUpdated, SettingsChanged | Simple CRUD; history rarely queried; ES is overkill |
| IoT Sensor Data | Yes | ReadingCaptured, ThresholdExceeded, CalibrationPerformed | Time-series data is inherently event-based; append-only fits perfectly |
The Bounded Context Question
Event sourcing doesn't have to be all-or-nothing. In a larger system, some bounded contexts may benefit greatly while others don't. Consider:
Choose the right persistence strategy for each bounded context based on its specific requirements.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
/** * Per-bounded-context persistence strategy analysis */ interface BoundedContextAnalysis { name: string; characteristics: { auditRequired: boolean; temporalQueriesNeeded: boolean; complexLifecycle: boolean; integrationHeavy: boolean; readWriteRatio: 'write-heavy' | 'balanced' | 'read-heavy'; consistencyRequirement: 'strong' | 'eventual-ok'; }; recommendedStrategy: 'event-sourcing' | 'cqrs-no-es' | 'crud'; reasoning: string;} const ecommerceSystemAnalysis: BoundedContextAnalysis[] = [ { name: 'Order Management', characteristics: { auditRequired: true, temporalQueriesNeeded: true, // "What was the order status on day X?" complexLifecycle: true, // Draft → Placed → Paid → Shipped → Delivered integrationHeavy: true, // Inventory, Payments, Shipping, Notifications readWriteRatio: 'balanced', consistencyRequirement: 'eventual-ok', }, recommendedStrategy: 'event-sourcing', reasoning: 'Complex lifecycle with audit requirements and heavy integration needs - perfect ES fit', }, { name: 'Product Catalog', characteristics: { auditRequired: false, temporalQueriesNeeded: false, complexLifecycle: false, // Products are just updated integrationHeavy: false, // Read by other systems, rarely triggers events readWriteRatio: 'read-heavy', // Millions of reads per update consistencyRequirement: 'eventual-ok', }, recommendedStrategy: 'crud', reasoning: 'Simple CRUD with heavy read caching - ES complexity not justified', }, { name: 'Inventory Management', characteristics: { auditRequired: true, // Track who/when for inventory discrepancies temporalQueriesNeeded: true, // "What was stock level when order placed?" complexLifecycle: false, // Just quantity changes integrationHeavy: true, // Order service needs to check/reserve readWriteRatio: 'write-heavy', // Every order affects inventory consistencyRequirement: 'strong', // Must prevent overselling }, recommendedStrategy: 'event-sourcing', reasoning: 'Audit needs plus integration needs, but requires strong consistency for reservations', }, { name: 'Customer Accounts', characteristics: { auditRequired: false, // Basic profile changes don't need full audit temporalQueriesNeeded: false, complexLifecycle: false, integrationHeavy: false, readWriteRatio: 'read-heavy', consistencyRequirement: 'eventual-ok', }, recommendedStrategy: 'cqrs-no-es', reasoning: 'Read-heavy with simple writes - CQRS for read scaling without ES complexity', }, { name: 'Payment Processing', characteristics: { auditRequired: true, // Financial compliance temporalQueriesNeeded: true, // Dispute resolution complexLifecycle: true, // Authorized → Captured → Settled integrationHeavy: true, // Payment providers, fraud detection readWriteRatio: 'balanced', consistencyRequirement: 'strong', // Double-charge prevention }, recommendedStrategy: 'event-sourcing', reasoning: 'Critical audit needs, complex lifecycle, strong consistency via aggregate boundaries', },]; // System overviewfunction generateSystemOverview(contexts: BoundedContextAnalysis[]): void { console.log('=== Persistence Strategy Overview ===\n'); const grouped = { 'event-sourcing': contexts.filter(c => c.recommendedStrategy === 'event-sourcing'), 'cqrs-no-es': contexts.filter(c => c.recommendedStrategy === 'cqrs-no-es'), 'crud': contexts.filter(c => c.recommendedStrategy === 'crud'), }; for (const [strategy, items] of Object.entries(grouped)) { console.log(`${strategy.toUpperCase()}:`); for (const item of items) { console.log(` - ${item.name}: ${item.reasoning}`); } console.log(); }}Let's examine real-world examples of event sourcing in production—both successful implementations and cases where it wasn't the right choice.
Successful event sourcing implementations share a pattern: the domain is genuinely event-oriented, the team has relevant experience (or time to learn), and the benefits (audit, temporal queries, integration) are genuine requirements—not theoretical future possibilities.
If you've decided event sourcing is right for your context, how do you introduce it safely? Here are proven adoption strategies.
| Approach | Description | When to Use | Risk Level |
|---|---|---|---|
| Greenfield Context | Apply ES to a new bounded context with no legacy | New business capability being built | Low |
| Parallel Running | Run ES system alongside existing; compare results | High-stakes migration needing validation | Medium |
| Strangler Pattern | Gradually migrate functionality from legacy to ES | Large existing system being modernized | Medium |
| Event Enablement First | Add event publishing to existing system before full ES | Want integration benefits now, full ES later | Low |
| Big Bang Migration | Full cutover to ES system at once | Small system, greenfield-like simplicity | High |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
/** * Illustration of the Strangler Pattern for ES adoption */ // Phase 1: Existing CRUD system continues; add event publishingclass Phase1LegacyOrderService { constructor( private database: LegacyDatabase, private eventBus: EventBus // NEW: Add event publishing ) {} async placeOrder(order: OrderData): Promise<Order> { // Existing CRUD logic unchanged const newOrder = await this.database.insert('orders', order); // NEW: Publish event for downstream systems await this.eventBus.publish('OrderPlaced', { orderId: newOrder.id, customerId: order.customerId, items: order.items, timestamp: new Date(), }); return newOrder; }} // Phase 2: New consumers use events; legacy reads continueclass Phase2OrderQueryService { constructor( private legacyDb: LegacyDatabase, // Still used for reads private eventProjection: EventProjectionService // NEW: Can use projection too ) {} async getOrder(orderId: string): Promise<Order> { // Try new projection first (eventually consistent) const projected = await this.eventProjection.getOrder(orderId); if (projected) return projected; // Fall back to legacy DB return this.legacyDb.query('orders', orderId); }} // Phase 3: New writes go to event store; legacy reads phased outclass Phase3EventSourcedOrderService { constructor( private eventStore: EventStore, private legacyDb: LegacyDatabase // Still needed during transition ) {} async placeOrder(command: PlaceOrderCommand): Promise<Order> { // NEW: Write to event store const order = new OrderAggregate(command.orderId); order.placeOrder(command.customerId, command.items); await this.eventStore.append( `order-${command.orderId}`, 'no_stream', order.getUncommittedEvents() ); // TRANSITIONAL: Also write to legacy DB for backward compatibility await this.legacyDb.insert('orders', this.toLegacyFormat(order)); return order.toOrder(); }} // Phase 4: Full event sourcing; legacy DB retiredclass Phase4FullEventSourcing { constructor( private eventStore: EventStore, private orderProjection: OrderProjection ) {} // Commands go to event store via aggregates async placeOrder(command: PlaceOrderCommand): Promise<Order> { const order = new OrderAggregate(command.orderId); order.placeOrder(command.customerId, command.items); await this.eventStore.append( `order-${command.orderId}`, 'no_stream', order.getUncommittedEvents() ); return order.toOrder(); } // Queries go to projections (or aggregate for consistency) async getOrder(orderId: string): Promise<Order | null> { return this.orderProjection.getOrder(orderId); } async getOrderWithConsistency(orderId: string): Promise<Order | null> { // When strong consistency needed, reconstitute aggregate const events = await this.eventStore.readStream(`order-${orderId}`); if (events.events.length === 0) return null; const order = new OrderAggregate(orderId); order.loadFromHistory(events.events); return order.toOrder(); }} /** * Migration timeline example */const migrationPlan = { phase1: { duration: '4-6 weeks', goals: [ 'Add event publishing to existing system', 'Set up event infrastructure (Kafka/EventStore)', 'Validate events are flowing correctly', ], rollbackPlan: 'Remove event publishing code; minimal risk', }, phase2: { duration: '6-8 weeks', goals: [ 'Build projections consuming events', 'Route some reads to projections', 'Monitor for consistency issues', ], rollbackPlan: 'Route all reads back to legacy DB', }, phase3: { duration: '8-12 weeks', goals: [ 'New writes go to event store', 'Dual-write to legacy for compatibility', 'Migrate remaining reads to projections', ], rollbackPlan: 'Switch writes back to legacy; events become secondary', }, phase4: { duration: '4-6 weeks', goals: [ 'Stop dual-writes to legacy', 'Archive legacy data', 'Full event sourcing operational', ], rollbackPlan: 'Replay events to rebuild legacy DB if needed', },};Don't try to event-source your entire system at once. Pick one bounded context—preferably one with strong audit or temporal requirements—and fully implement ES there. Learn from that experience before expanding. Success in one context builds team capability and organizational confidence.
Event sourcing introduces concepts that may be unfamiliar to teams experienced only with traditional CRUD. Assess your team's readiness and plan for the learning curve.
Key Competencies Needed:
| Area | Green Light | Yellow Light | Red Light |
|---|---|---|---|
| ES Experience | Team has production ES experience | Some members have ES exposure | No one has used ES before |
| DDD Knowledge | Team practices DDD actively | Familiar with concepts | New to DDD |
| Distributed Systems | Team operates distributed systems | Some exposure, learning | New to distributed systems |
| Time for Learning | Dedicated ramp-up period planned | Some time for learning | Tight deadline, no learning time |
| Expert Access | ES expert on team or available | Can hire consultant | No access to expertise |
Learning Investment
If your team is new to event sourcing, plan for a learning investment:
The investment pays off. Teams that rush into ES without preparation often spend more time debugging fundamental mistakes than they would have spent learning properly.
Underestimating the expertise needed is the most common mistake. Event sourcing looks simple in tutorials but has subtle complexities in production—projection ordering issues, event schema evolution conflicts, debugging production state derivation. Plan for the learning curve, or pay the debugging tax later.
Event sourcing requires infrastructure that may be new to your organization. Evaluate your platform readiness.
| Need | EventStoreDB | Kafka | PostgreSQL | DynamoDB Streams |
|---|---|---|---|---|
| ES-Native Design | Yes | Partial | Adapter | Partial |
| Operational Familiarity | Lower | Medium | High | Medium (AWS) |
| Scale (events/sec) | 10K+ | Millions | 10K-100K | Millions |
| Managed Offering | Cloud | Confluent | Any cloud | AWS Native |
| Projection Support | Built-in | External | External | Lambda-based |
| Global Ordering | Yes | Per-partition | Yes | Per-shard |
Operational Considerations
For many teams, starting with PostgreSQL as an event store (with proper schema) is pragmatic. It leverages existing expertise and infrastructure. You can migrate to a specialized event store like EventStoreDB later if needed. Don't over-engineer infrastructure before validating the approach.
Before committing to event sourcing, work through this final checklist with your team and stakeholders.
• 7+ checklist items are confidently checked • No blockers identified in high-weight criteria • Team has relevant experience or learning time • Domain is genuinely event-oriented • Stakeholders understand the tradeoffs
• Multiple checklist items are unchecked • Critical blockers present (deadline, consistency, inexperience) • Domain is primarily CRUD • Event sourcing is chosen for 'future-proofing' without concrete present needs
We've provided a complete framework for deciding when event sourcing is the right choice. Let's consolidate the key decision factors:
Module Complete
You've now completed the Event Sourcing module. You've learned:
Event sourcing is a powerful pattern for the right context. Use this knowledge to make informed decisions—adopting event sourcing where it shines, and avoiding it where simpler approaches suffice.
Congratulations! You've completed Event Sourcing. You now have the knowledge to evaluate, design, and implement event-sourced systems—and equally important, to know when not to. The next module in Chapter 16 (Saga Pattern) will build on these concepts, showing how to coordinate business processes across event-sourced aggregates.