What CQRS Actually Means (Beyond Separate Databases)
CQRS stands for Command Query Responsibility Segregation. At its core, it means you split your application into two distinct models: one optimized for writing data (commands) and another optimized for reading data (queries). This is not the same as simply having a read replica of your database, although that is a common misconception.
In a traditional CRUD architecture, you have a single model that handles both reads and writes. Your User entity has fields for display purposes, validation logic for mutations, and query methods all bundled together. This works fine for simple domains. But when your read patterns diverge significantly from your write patterns, that single model becomes a compromise that serves neither purpose well.
The command side handles all mutations. Commands are imperative: CreateInvoice, CancelSubscription, ApproveRefund. They pass through validation, enforce business rules, and produce state changes. The command model is normalized, consistent, and optimized for writes. It does not care about how data will be displayed.
The query side handles all reads. It serves denormalized, pre-computed views that are optimized for specific screens or API responses. A dashboard showing "monthly revenue by plan tier" does not need to join six normalized tables at query time. Instead, a projection pre-computes that view and stores it in a shape that maps directly to the UI. The query model can use a completely different storage technology: Elasticsearch for full-text search, Redis for real-time counters, or a flat denormalized table in PostgreSQL.
The critical insight is that reads and writes have fundamentally different scaling characteristics in most SaaS products. A typical B2B application has 10:1 or even 100:1 read-to-write ratios. Your pricing page gets hit thousands of times per minute. Your "change subscription" endpoint gets called a few times per hour. CQRS lets you scale these independently and optimize each for its actual workload.
When You Actually Need CQRS and Event Sourcing
Most SaaS products do not need CQRS or event sourcing. That is not a hedge, it is a fact. If you are building a straightforward CRUD application where reads and writes touch the same data shape, where audit logs are a nice-to-have rather than a regulatory requirement, and where your domain logic fits in a single service, standard CRUD with a relational database will serve you better for years.
But there are specific scenarios where these patterns solve problems that CRUD cannot address cleanly.
Complex Domain Logic with Multiple Decision Points
When your business rules involve state machines, approval workflows, or multi-step processes where the order of operations matters, event sourcing gives you a natural way to model this. An insurance claims system, a loan origination workflow, or a multi-tenant SaaS with complex permission hierarchies all benefit from capturing every state transition as an explicit event rather than overwriting rows in a database.
Regulatory Audit Requirements
Financial services, healthcare, and legal tech products often face regulatory requirements to maintain a complete, tamper-evident history of every change. With CRUD, you bolt on audit tables after the fact. With event sourcing, the audit log is your data model. Every state change is an immutable event with a timestamp, actor, and full payload. You get compliance by default rather than as an afterthought.
High Read-to-Write Ratios with Diverse Read Patterns
If you have a write model that accepts ten types of commands but needs to serve fifty different read patterns (dashboards, reports, search, real-time feeds, export formats), CQRS lets you build specialized read models for each pattern without polluting your write side with query concerns. This is common in analytics-heavy SaaS products, CRM systems, and project management tools.
Collaborative or Multi-User Editing
Products where multiple users modify the same entity simultaneously benefit enormously from event sourcing. Instead of last-write-wins (which loses data) or pessimistic locking (which kills UX), you capture each user's changes as discrete events and merge them. Google Docs, Figma, and Notion all use variations of this pattern. If your SaaS involves shared workspaces where conflicts are common, event sourcing gives you the primitives to handle them gracefully.
Temporal Queries and Time Travel
If your users need to answer questions like "What did this account look like on March 15?" or "Show me every change that happened to this record," event sourcing makes these queries trivial. You replay events up to the desired point in time. With CRUD, you either do not have that data or you are querying expensive audit tables.
Event Sourcing Fundamentals: The Append-Only Event Store
Event sourcing inverts how you think about persistence. Instead of storing the current state of an entity and overwriting it on every change, you store an ordered sequence of events that represent everything that ever happened to that entity. The current state is derived by replaying those events from the beginning.
Consider a bank account. In CRUD, you have a row with a balance field. A deposit updates the balance. A withdrawal updates the balance. You know the current balance, but you cannot reconstruct how you got there without separate transaction logs. In event sourcing, you store AccountOpened, MoneyDeposited($500), MoneyWithdrawn($200), MoneyDeposited($1000). The current balance ($1300) is computed by folding over these events. The events are the source of truth, not the derived balance.
An event store has a few key properties. Events are immutable: once written, they never change. Events are ordered: within a single stream (one aggregate), events have a sequence number that guarantees ordering. Events are append-only: you only ever add new events, never update or delete existing ones. And events are self-describing: each event carries its type, timestamp, metadata, and the full payload of what changed.
Event Streams and Aggregates
Events are grouped into streams, typically one stream per aggregate instance. An aggregate is a DDD concept representing a consistency boundary. For a billing system, each Invoice might be an aggregate with its own event stream: InvoiceCreated, LineItemAdded, LineItemRemoved, InvoiceSent, PaymentReceived, InvoiceMarkedPaid. The stream ID is usually something like "Invoice-abc123".
Event Replay and Rehydration
To load an aggregate's current state, you read all events from its stream and apply them sequentially. This is called rehydration. Your aggregate has an "apply" method for each event type that updates its in-memory state. After replaying all events, you have the current state and can validate new commands against it. This seems expensive, but most aggregates have fewer than a hundred events in their stream, and replay from memory is fast.
The real power of replay emerges when you need to rebuild read models, fix bugs in projections, or add entirely new views of your data. Because you have every event that ever happened, you can replay them through new projection logic to produce new read models without losing information. This is impossible with CRUD unless you have been meticulously logging every change from day one.
Practical Implementation: Tooling and Infrastructure Choices
You have three main options for implementing an event store, each with different trade-offs in complexity, performance, and operational burden.
EventStoreDB
EventStoreDB (formerly Event Store) is a purpose-built database for event sourcing. It handles event streams, subscriptions, projections, and optimistic concurrency natively. You do not need to build any of the event store infrastructure yourself. It supports catch-up subscriptions (for building projections), persistent subscriptions (for consumer groups), and server-side projections in JavaScript.
The catch: EventStoreDB is a specialized database that your ops team needs to learn. It runs on its own cluster, has its own backup and monitoring patterns, and most cloud providers do not offer a managed version (Event Store Cloud exists but is limited in regions). For teams already running PostgreSQL and Kafka, adding another database is a hard sell. But if event sourcing is central to your product, the native support and performance optimizations are worth the operational investment.
PostgreSQL as an Event Store
You can use PostgreSQL as a perfectly capable event store for most SaaS workloads. The schema is straightforward: a table with columns for stream_id, event_type, event_data (JSONB), metadata (JSONB), sequence_number, and created_at. An optimistic concurrency check uses a unique constraint on (stream_id, sequence_number). You read events with a simple SELECT ordered by sequence_number.
This approach has major advantages. Your team already knows PostgreSQL. You already have backups, monitoring, and connection pooling configured. You can use your existing ORM or query builder. And for most SaaS products processing fewer than ten thousand events per second, PostgreSQL handles the load without breaking a sweat. The Marten library (.NET) and Eventide (Ruby) use PostgreSQL under the hood. In Node.js/TypeScript, you can roll your own with 200 lines of code or use libraries like @event-driven-io/emmett.
Event Streaming with Kafka or NATS
For distributing events to projections and external consumers, you need a streaming platform. Kafka is the industry default for high-throughput scenarios. It provides ordered, durable, replayable event streams with consumer groups for parallel processing. NATS JetStream is a lighter-weight alternative that handles tens of thousands of messages per second with simpler operations, lower latency, and a smaller resource footprint.
A common pattern: write events to PostgreSQL (your source of truth) and publish them to Kafka/NATS for downstream consumption. This gives you transactional writes (events and state in the same database transaction via the outbox pattern) while still enabling reactive, real-time projections. The event-driven architecture fundamentals apply directly here.
Cost Comparison
EventStoreDB Cloud starts around $200/month for a production cluster. Self-managed PostgreSQL on RDS costs $50-150/month depending on instance size. Confluent Cloud (managed Kafka) runs $300-800/month for production workloads. NATS on a single VM costs $20-50/month. For a startup, PostgreSQL plus NATS is the sweet spot: $70-200/month total for a production-grade event sourcing infrastructure that handles thousands of events per second.
Projections, Read Models, and Eventual Consistency
Projections are the mechanism that transforms your event stream into queryable read models. A projection subscribes to events, processes them one by one, and updates a read-optimized data store. This is where CQRS meets event sourcing: your command side writes events, and your projections build the read models that serve queries.
How Projections Work
A projection is essentially a fold function over a stream of events. It maintains a checkpoint (the last event position it processed) and applies each new event to update its read model. For example, a "TenantDashboard" projection might listen for SubscriptionCreated, SubscriptionUpgraded, SubscriptionCancelled, and InvoicePaid events, updating a denormalized dashboard table that stores current MRR, active users, churn rate, and plan distribution. The dashboard API reads directly from this table with no joins or aggregations at query time.
Materialized Views in Practice
Read models can take many forms. A PostgreSQL table with pre-computed aggregates for dashboard queries. An Elasticsearch index for full-text search across all entities. A Redis sorted set for real-time leaderboards. A flat JSON file in S3 for export downloads. Each projection produces the exact shape the consumer needs. You can have dozens of read models derived from the same event stream, each serving a different use case.
The Eventual Consistency Trade-off
The catch: projections are eventually consistent. When a command produces an event, there is a delay (typically milliseconds, but potentially seconds under load) before the projection processes it and updates the read model. This means a user who creates an invoice might not see it immediately in their invoice list if the list is served by a projection.
For most SaaS products, this is a non-issue. Users tolerate sub-second delays, and you can use optimistic UI patterns on the frontend to show the expected state immediately. But for some flows (like payment confirmation), you need to either read from the write model directly or use a synchronous projection that updates in the same transaction as the event write.
Strategies for handling eventual consistency in practice: use optimistic UI (show the expected state before confirmation), implement read-your-writes consistency (route queries from the same user to the write model for a few seconds after a command), or accept eventual consistency where the business domain allows it (analytics dashboards, activity feeds, search results).
Projection Rebuilds
One of the biggest advantages of event sourcing is the ability to rebuild projections from scratch. Found a bug in your revenue calculation? Fix the projection logic, delete the read model, and replay all events through the corrected projection. You get accurate historical data without manual correction. This is a superpower during audits or when product requirements change and you need to slice data in ways you did not anticipate at launch.
Snapshotting, Performance, and Real-World SaaS Examples
As event streams grow long, replaying hundreds or thousands of events to rehydrate an aggregate becomes expensive. Snapshotting solves this by periodically saving the aggregate's current state at a known event position. On subsequent loads, you read the snapshot and only replay events after that position.
When to Snapshot
Not every aggregate needs snapshots. If your invoice aggregate has 5-20 events in its lifetime, replay is essentially free. But a long-lived aggregate like a customer account that accumulates thousands of events over years benefits significantly. A good rule of thumb: snapshot when replay takes more than 50ms or when your stream exceeds 200 events. Store snapshots in the same event store (as a special event type) or in a separate key-value store.
Snapshot Strategies
The simplest approach: take a snapshot every N events (e.g., every 100 events). When loading, read the latest snapshot, then replay only events after the snapshot's sequence number. More sophisticated approaches snapshot on every write (so reads are always instant) or snapshot lazily on read (only when the events-since-last-snapshot count exceeds a threshold). For most SaaS applications, snapshotting every 100 events with lazy evaluation on read is the pragmatic choice.
Real-World SaaS Examples
Billing and Subscription Management: Stripe's internal architecture uses event sourcing for payment processing. Every charge attempt, refund, dispute, and settlement is an event. Projections produce the dashboard views, the API responses, and the financial reconciliation reports. This is why Stripe can show you a complete timeline of every payment and why their reporting is so granular. If you are building a billing system, event sourcing is probably the right call.
Collaborative Document Editing: Products like Notion, Coda, and Linear use event-sourced models for collaborative content. Every keystroke, block move, or formatting change is an event. Operational Transformation or CRDTs handle conflict resolution, but the underlying storage is an ordered event log. This enables undo/redo, version history, and real-time collaboration as natural consequences of the architecture.
Financial Ledgers and Accounting: Double-entry bookkeeping is inherently event-sourced. You never modify a ledger entry; you create a new correcting entry. Fintech products that handle money movement (Mercury, Ramp, Brex) use event sourcing because it provides an auditable, immutable record of every transaction and makes reconciliation deterministic.
IoT and Telemetry Platforms: Products ingesting sensor data, application metrics, or usage events naturally fit the append-only model. Each reading is an event. Projections produce time-series aggregations, anomaly detection inputs, and billing calculations. The raw events are retained for replay when new analytics capabilities are added.
Common Pitfalls and Schema Evolution Challenges
Event sourcing and CQRS introduce a class of problems that do not exist in CRUD systems. Being aware of these upfront saves you months of pain.
Premature Adoption
The most common pitfall is adopting these patterns before your domain justifies the complexity. CQRS and event sourcing add at least 3x the code and infrastructure compared to straightforward CRUD. If your SaaS is a simple CRUD application with standard read/write patterns, you are paying that complexity tax without receiving proportional benefits. Start with CRUD. Refactor to CQRS/ES when you hit specific pain points (audit requirements, complex domain logic, diverse read patterns) that these patterns solve directly.
Schema Evolution
Events are immutable. Once written, they cannot change. But your code evolves. Six months from now, your InvoiceCreated event might need a new field, or an existing field might change meaning. This is the schema evolution problem, and it is the hardest ongoing challenge in event-sourced systems.
Strategies for handling schema evolution: upcasting (transforming old events into the current schema shape at read time), versioned event types (InvoiceCreatedV1, InvoiceCreatedV2), and weak schema (treating event data as loosely typed and adding optional fields that old events lack). Upcasting is the most common approach. You write a transformation function for each old schema version that maps it to the current version. This keeps your application code clean at the cost of maintaining a library of upcasters.
Event Granularity
Finding the right granularity for events is tricky. Too coarse (UserUpdated with a full JSON diff) and you lose the semantic meaning of what happened. Too fine (FirstNameChanged, LastNameChanged, EmailChanged) and you end up with enormous streams and complex projections. The right answer is usually domain-driven: what are the meaningful business actions? ProfileUpdated is too vague. EmailVerified, PasswordChanged, BillingAddressUpdated are specific enough to carry meaning but broad enough to keep stream sizes manageable.
Eventual Consistency Confusion
Teams new to CQRS often struggle with eventual consistency in user-facing flows. A user creates a record, gets redirected to a list page, and the record is not there yet because the projection has not caught up. This erodes user trust quickly. You need a deliberate strategy: optimistic UI, read-your-writes routing, or synchronous projections for critical paths. Do not ship eventual consistency to users without an explicit plan for handling the latency window.
Unbounded Event Streams
Some aggregates accumulate events forever. A customer account that receives events for years can have tens of thousands of events in its stream. Without snapshotting, rehydration becomes a bottleneck. Worse, replaying all events through a new projection can take hours or days if your total event count is in the hundreds of millions. Plan for stream growth from the start: implement snapshotting, set up archival for old events, and ensure your projection replay infrastructure can parallelize effectively.
When CRUD Is Actually the Better Choice
After all this discussion of CQRS and event sourcing, here is the honest conclusion: for 80% of SaaS products, especially in their first two years, standard CRUD with a PostgreSQL database is the right architecture. It is simpler to build, simpler to debug, simpler to hire for, and simpler to operate.
CRUD wins when your read and write models are the same shape (or close to it), when your domain logic is straightforward create/update/delete operations, when you do not have regulatory audit requirements, when your team is small (fewer than five engineers) and needs to ship fast, and when eventual consistency would confuse your users more than it helps your scalability.
You can add audit logging to CRUD with a triggers or application-level middleware. You can add caching layers for read performance. You can extract services from your monolith when specific domains get complex enough to justify it. These incremental improvements get you surprisingly far before you need the full weight of CQRS and event sourcing.
The Hybrid Approach
The most pragmatic path for growing SaaS products is selective adoption. Use CQRS and event sourcing for the bounded contexts where they genuinely help (billing, audit-sensitive workflows, collaborative features) and keep everything else as simple CRUD. Your user management service does not need event sourcing. Your settings page does not need CQRS. But your financial ledger, your document collaboration engine, or your compliance workflow might.
This bounded approach limits complexity to the areas where it pays for itself. You get the benefits in the domains that need them without imposing the tax on everything else. It is also a natural migration path: start with CRUD everywhere, identify the pain points as you scale, and introduce CQRS/ES in those specific contexts.
Making the Decision
Ask yourself these questions. Do multiple teams need independent read models from the same data? Do you have regulatory or compliance requirements for complete audit trails? Does your domain involve complex state machines or workflows? Do you need to support temporal queries (point-in-time state reconstruction)? Do you have wildly different read and write scaling requirements? If you answered yes to three or more, CQRS and event sourcing deserve serious evaluation. If you answered yes to one or none, stick with CRUD and revisit in six months.
If you are building a SaaS product and trying to figure out which architecture pattern fits your specific domain and scale, we help teams make this exact decision every week. Book a free strategy call and we will walk through your domain model, identify where CQRS and event sourcing actually pay off, and help you avoid the premature complexity trap that kills velocity.
Need help building this?
Our team has launched 50+ products for startups and ambitious brands. Let's talk about your project.