Event Sourcing in System Design: A Beginner’s Guide
-
Last Updated: September 17, 2026
-
By: javahandson
-
Series

Event sourcing in system design explained simply — learn the event store, replay, snapshots, projections, and CQRS, plus when to use the pattern and when to skip it.
Many applications only keep the latest information. For example, your bank balance shows just one number. Your shopping cart displays only a few items at a time. When something changes, the old value gets erased and disappears completely. This seems normal and works well for many systems. However, event sourcing changes this idea. Once you understand it, many difficult problems become easier to solve.
This article is a beginner-friendly walk through event sourcing, one of the more powerful ideas in system design. We will not assume you know anything about it yet. We start with why storing only the current state can hurt you, then build up the pattern piece by piece. Along the way we cover the event store, replaying events, snapshots, projections, and the close cousin called CQRS. Everyday analogies and small examples keep things grounded.
By the end, you will know what event sourcing is and when to use it. You will also see when it might be too much for your needs. This knowledge will help you explain it clearly in a system design interview, where this topic is becoming more common.

Think about how a normal database table works. You have an accounts table with a balance column. A customer deposits money, so you run an update and the balance goes up. They withdraw money, so you update again and it goes down. At any moment the table shows one thing: the balance right now.
This is called state-oriented storage. It is simple and fast. But it quietly throws away something valuable. Every update destroys the previous value. Once you overwrite the balance, the old balance is lost. You know the account has 500 dollars, but you have no idea how it got there.
For a lot of apps that is acceptable. For others it is a serious problem. Consider a few situations where the missing history really bites:
In each case, the current state alone is not enough. You need the story of how the data got here, not just where it ended up. That story is exactly what event sourcing preserves.
Event sourcing is a method for storing data by recording each change as an event rather than replacing the existing value. This way, the complete series of events serves as the main source of truth. Instead of directly storing the current state, you recreate it by playing back the events in the order they happened.
Let me put that in plain words. Rather than saving “the balance is 500”, you save the list of things that happened: account opened, deposited 300, deposited 400, withdrew 200. Add those up and you get 500. The number 500 is derived, not stored. The events are the truth.
Each event describes something that already happened, so events are always written in the past tense. AccountOpened, MoneyDeposited, MoneyWithdrawn, OrderShipped. This naming is not just style. It reflects a core rule: an event is a fact about the past, and facts do not change.
Once an event is recorded, it cannot be changed or deleted. This is the main idea. An event states, “this happened at this time,” and you cannot change what has already happened.
So what if a customer was charged by mistake? You do not edit or remove the original event. Instead, you add a new event that corrects it, such as PaymentRefunded. Both events stay in the log forever. The mistake and the fix are both part of the history, which is honest and fully traceable.
Events are recorded in a log that only adds new information. Each new event goes at the end, and nothing from the past is changed. You can think of it like a diary written in pen. You can add a new entry each day, but you cannot remove or rewrite an old page.
This append-only nature brings real benefits. Writes are simple and fast, since you only ever add to the end. There are no complex update locks fighting over the same row. And the log gives you a perfect, ordered record of everything that ever occurred.
The bank account is the classic teaching example, and for good reason. It makes the whole pattern click. Let us walk through it slowly.
In a traditional system, the account is one row with a balance. In an event-sourced system, the account is a stream of events. Here is what that stream might look like for one customer over a few days:
Notice the last column is not stored anywhere. It is calculated by walking through the events one by one. Start at zero, apply each event, and you arrive at the current balance of 400. This process of rebuilding state from events has a name, and it is worth knowing.
| Order | Event | Data | Running Balance |
|---|---|---|---|
| 1 | AccountOpened | owner: Ravi | 0 |
| 2 | MoneyDeposited | amount: 300 | 300 |
| 3 | MoneyDeposited | amount: 400 | 700 |
| 4 | MoneyWithdrawn | amount: 200 | 500 |
| 5 | MoneyWithdrawn | amount: 100 | 400 |
Replaying means reading events in the order they happened and using each one to create the current state. This process is sometimes called rehydration because you are filling an empty object with events until it is complete and up to date.
The logic is refreshingly simple. You start with a blank account. For each event you ask, what does this event do to the state? A deposit adds to the balance. A withdrawal subtracts from it. By the time you reach the last event, the account holds the correct current value.
Code: Rebuilding an account balance by replaying its events
// Each event is a simple fact about what happened.
public record AccountEvent(String type, long amount) {}
// Replay: start from zero and apply every event in order.
public long currentBalance(List<AccountEvent> events) {
long balance = 0;
for (AccountEvent e : events) {
switch (e.type()) {
case "MoneyDeposited" -> balance += e.amount();
case "MoneyWithdrawn" -> balance -= e.amount();
// AccountOpened sets up the account; no balance change.
}
}
return balance; // e.g. 400 for the stream above
}The event store is where all these events are kept. It acts as the database for an event-sourced system, but it operates differently than a normal database. Its main purpose is to add events and to read them back in order.
An event store is append-only and grouped into streams. A stream is just the ordered list of events for one entity, such as one account or one order. When you want to load an account, you ask the store for that account’s stream and replay it.
A single stored event is more than just its type. To be useful and reliable, it usually carries a few standard fields. Here are the common ones you will see:
The sequence number is important because it ensures a strict order in a stream. It helps manage situations where two people might try to change the same account at the same time. We will discuss this more when we cover concurrency.
You do not always need special software to get started. Many teams begin with a plain relational table that has a stream id, a sequence number, an event type, and a JSON payload column. That is enough to learn the pattern and even to run modest systems.
As needs increase, specialized tools can help. EventStoreDB is made specifically for this purpose. Many teams use Apache Kafka as a main event system because it only adds new information. Some may opt for a document store or a simple cloud object store. The best choice depends on your scale and how much built-in support you want.
Replaying events works well for short streams. However, if an account has been active for ten years and has hundreds of thousands of events, replaying all of them to show the balance can be very slow. This concern is a major issue with event sourcing, and snapshots provide a solution.
A snapshot is a saved copy of an entity’s state at a certain point in its event stream. Instead of replaying from the very beginning, you load the latest snapshot and then replay only the events that came after it. The difference in work can be enormous.
Say you take a snapshot every 500 events. An account with 10,000 events would have its most recent snapshot at event 9,500 or so. To load the account, you read that snapshot and replay only the last 500 events. That is 500 events instead of 10,000, a twentyfold saving.
Snapshots are an optimization, not a source of truth. This is a subtle but important point. You can always throw every snapshot away and rebuild them from the events, because the events remain the real record. A snapshot is just a shortcut that you can safely regenerate.
| 💡 Interview Insight A very common interview question is: does not replaying millions of events make event sourcing too slow? The strong answer names snapshots directly. Explain that you take a snapshot of state every N events, load the newest snapshot, and replay only what came after. Then add the key line that snapshots are a cache you can rebuild from the log, so they never become a second source of truth. That last point separates a memorized answer from real understanding. |
There is one important issue we need to address. Replaying events is effective for loading one account, but it is not good for getting information from many accounts. For example, if you want to see a list of every account with a balance over 1,000 dollars, replaying every stream in the system to create that list would be very inefficient.
This is where projections come in. A projection reads the stream of events and builds a separate, query-friendly view of the data. This view is called a read model. It is shaped exactly for how the application needs to read, not for how it writes.
A projection is a small process that listens to events and updates a plain table as they arrive. When a MoneyDeposited event flows past, the projection finds that account’s row in a simple summary table and adds to its balance column. The result is an ordinary table you can query fast with normal SQL.
The powerful part is that you can build many projections from the same events. One projection feeds the account list screen. Another counts daily transactions for analytics. A third pushes data into a search index. They all draw from one event log, each shaping the data for its own purpose.
Because the event log is the source of truth, a projection is disposable. If a read model gets corrupted, or you want to change its shape, you simply delete it and replay the events to build it fresh. Try doing that with a traditional database where the current state is all you have.
This rebuild ability is a quiet superpower. Need a brand-new report on last year’s data? Write a new projection and replay history through it. The information was there in the events all along, waiting to be asked a new question.
| 💡 Interview Insight Interviewers love to probe the difference between the write side and the read side. Be ready to say that the event store is optimized for writing facts, while projections are optimized for reading. Mention that projections are eventually consistent, meaning there is a short lag between an event being written and the read model catching up. Acknowledging that lag, rather than pretending everything is instant, shows you understand the real trade-off. |
You cannot read far about event sourcing without meeting CQRS, so let us clear up what it is. CQRS stands for Command Query Responsibility Segregation. That is a mouthful, but the idea behind it is simple: separate the part of your system that changes data from the part that reads data.
In many conventional systems, a single model is responsible for both reading and writing operations. However, Command Query Responsibility Segregation (CQRS) introduces a separation between these two functions. In this approach, commands are used to modify application state and generate events, while queries focus on reading data from projections. This division allows the two components to utilize different data storage solutions, scale independently according to their specific needs, and be optimized for their individual responsibilities.
Event sourcing and CQRS are separate ideas, but they pair naturally. The event store is the perfect write model, since commands simply append events to it. The projections are the perfect read model, since queries hit those fast, shaped tables. One side writes events, the other side reads views, and the event log connects them.
You can use either pattern without the other. But when they come together, each covers the other’s weak spot. Event sourcing gives CQRS a clean, ordered write side. CQRS gives event sourcing a fast, flexible read side. That is why the two names so often appear in the same breath.
Let us trace one action end to end so the flow feels concrete. A user clicks a button to withdraw 100 dollars. Here is the path that request takes through a CQRS event-sourced system:
Notice that the command validates against the current state before writing. That is how event sourcing enforces rules like “no overdrafts”, even though it stores history rather than state.
We have seen the mechanics, so now let us gather the payoffs in one place. These are the reasons teams take on the extra effort. When one of these benefits matches a real need, the pattern earns its keep.
This is the headline benefit. Because every change is an event and no event is ever deleted, you get a perfect history for free. Every deposit, every withdrawal, every address change is preserved in order. For banks, healthcare, and any regulated field, this audit trail is often reason enough to adopt the pattern.
You can go back and look at any event that happened before. This means you can see exactly what the system looked like at any time in the past. For example, if you want to check what an account looked like last Tuesday at noon, just replay the events until that time. This makes fixing problems easier because you can recreate the exact situation that caused a bug.
The events capture rich detail that a current-state table would have thrown away. Later, when the business asks a new question, the answer is often already sitting in the log. You just write a new projection and replay history through it. Traditional systems cannot answer questions about data they never kept.
Event sourcing is powerful, but it is not free. It brings real complexity, and using it in the wrong place causes more pain than it cures. An honest look at the downsides matters as much as the upsides.
This is the big one. Event sourcing is a different way of handling data, and the whole team needs to learn it. Concepts like replay, projections, and eventual consistency are new for developers who are used to simple tables. For a basic app, this learning curve usually isn’t worth it.
Because read models are updated after events are written, there is a small delay before a query reflects the latest change. A user might withdraw money and, for a fraction of a second, still see the old balance on a list screen. Most of the time this lag is tiny, but your design has to account for it, and users sometimes have to be told.
Events last forever, which raises an important question: What happens when an event needs to change? An event you wrote five years ago still needs to be understandable today. Managing these older versions, often called event versioning, is one of the more challenging aspects of running event-sourced systems over time.
You keep every event forever, so storage only grows. For most systems this is manageable, since events are small and storage is cheap. Still, it is a factor to plan for, and very high-volume systems need a clear strategy for archiving cold streams.
The most important skill here is knowing where the pattern fits. Event sourcing is a specialist tool, not a default choice. Reach for it when the shape of your problem matches its strengths.
Some domains are almost made for event sourcing. If your problem looks like one of these, the pattern is worth serious thought:
Just as often, the pattern is the wrong call. Avoid it when the extra machinery buys you nothing. These are the classic cases where a normal database is the better choice:
| 💡 Interview Insight When an interviewer asks whether you would use event sourcing for a given design, resist the urge to say yes just because it sounds advanced. The senior move is to weigh it. Say something like: I would use event sourcing here because the audit trail is a hard requirement, or I would avoid it here because this is a simple CRUD service and the complexity is not justified. Showing that you can say no to a pattern is often more impressive than showing you know it exists. |
One more practical topic rounds out the fundamentals: what happens when two requests try to change the same entity at the same time? Say two withdrawals hit the same account in the same instant. Without care, both could pass the balance check and cause an overdraft.
Event sourcing uses a simple method based on something called the sequence number. When you load an account, you take note of the sequence number of the most recent event.
When you want to add a new event, you tell the data store to only accept it if the last sequence number matches the one you recorded. This method is known as optimistic concurrency control. It helps ensure that updates to the account are accurate and prevents old changes from being made. If the sequence number has changed because of another operation, the new event will be rejected. This means you need to refresh the account’s state and try again. This way, event sourcing keeps a clear and correct history of the account.
If another request slipped in first, the sequence number will have moved, and your append is rejected. Your code then reloads the account, re-checks the rule against the newer state, and tries again. This simple check keeps the log consistent even under heavy parallel traffic, with no heavy locking required.
Let us step back and see the whole picture as one flow, because the parts make more sense together than apart. A command comes in and is checked against current state. If valid, it appends an event to the event store, which is the append-only source of truth.
From there, two things happen. The write side can take snapshots so future loads stay fast. Meanwhile, projections consume the same events and update read models that queries hit directly. Commands flow into events, events flow into both snapshots and projections, and queries read from the projections.
That single loop, command to event to projection, is the beating heart of an event-sourced system. Every concept in this article plugs into it. Snapshots speed up the write side, CQRS formalizes the split between the two sides, and concurrency control keeps the event log honest. Master this loop and the rest is detail.
A: Event sourcing stores every change as an immutable event in an append-only log, instead of overwriting the current value. The current state is rebuilt by replaying those events in order, so the full history is always preserved.
A: No, because of snapshots. You save the state every N events, load the newest snapshot, and replay only the events after it. Snapshots are a rebuildable cache, not a second source of truth, so the event log stays authoritative.
A: They are separate patterns that pair well. Event sourcing is about how you store data as events. CQRS separates the write side (commands producing events) from the read side (queries hitting projections). The event log makes an ideal write model and projections make an ideal read model.
A: A projection reads the event stream and builds a separate, query-friendly read model, such as a summary table. You can build many projections from the same events, and rebuild any of them from scratch by replaying history.
A: Avoid it for simple CRUD apps, small projects, or systems where you cannot point to a real need for history, audit, or replay. The extra complexity and eventual consistency are only worth it when the audit trail or historical reconstruction genuinely matters.
To go deeper into the ideas covered here, these well-regarded sources are worth your time: