Skip to content

Event store vs. a regular database

An event store is a database designed for one job: appending immutable events in order and reading them back, either for one entity or across the whole log. It does not update or delete rows. It records what happened and keeps it.

A regular database, relational or document, stores the current state of your data and lets you change it in place. An event store stores the history of changes, and the current state is derived from it. Both are databases. They answer different questions and make different promises.

This page compares the two, shows how an event store such as Cratis Chronicle is used, and covers when an ordinary database is the better choice. It builds on what is event sourcing?.

An event store keeps one or more ordered logs of events. In Chronicle these are event sequences, and the main one is the event log. Each event gets a sequence number that never changes. Each event also belongs to an event source, such as one account or one order, so you can read the history of a single entity.

Event store

append

read by event source

observers read in order

Application

Event log

#0 BookAdded

#1 BookBorrowed

#2 BookReturned

Projections and reactors

The store offers a small set of operations:

  • Append an event to an event source, with checks at append time.
  • Read the events of one event source, or from a sequence number onward.
  • Observe the log: projections, reducers and reactors read new events in order and remember how far they have read.

Chronicle also tracks event types, observers and projections in the event store, and it can partition data into namespaces, for example per tenant. It stores everything in a database you provide: MongoDB (the default), PostgreSQL, SQL Server or SQLite. Read the event store concept for the details.

Regular databaseEvent store
What is storedCurrent state of each recordEvery change, as an immutable event
Write operationInsert, update, deleteAppend only
HistoryLost unless you add audit tables or change captureThe data itself
Typical readQuery the table or collection you wrote toQuery a read model built from events
Ad hoc queriesRich, built in (SQL, aggregation)Limited on the log; use read models
Consistency of readsRead-after-write in one placeWrites are validated at once; read models usually catch up shortly after
FitGeneral purposeDomains where change over time is the point

The last row is the real decision. Neither is better in general.

An event store does not replace querying. You almost never run business queries against the raw log. You build read models from it, and those can live in a document or relational database. An event-sourced system therefore often uses an event store and a regular database together. See projections and read models.

In a relational design, a Loans table has a row per book with a BorrowedBy column and a DueDate column. When a book comes back, you clear them. The row now says the book is on the shelf, and nothing says it was ever on loan.

In an event store, the same book’s history looks like this:

#0 BookAdded(Title: "Dune")
#1 BookBorrowed(Member: "Ada", DueDate: 2025-03-01)
#2 BookReturned()
#3 BookBorrowed(Member: "Grace", DueDate: 2025-03-22)

You can still answer “who has the book now?” by folding the events. You can also answer “how often is it borrowed?” and “how long do members keep it?”, which the table cannot say.

In Chronicle you define the events as records and append them to the event log:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
[EventType]
public record BookBorrowed(string Member, DateOnly DueDate);
public class LendingService(IEventLog eventLog)
{
public Task Lend(EventSourceId bookId, string member, DateOnly dueDate) =>
eventLog.Append(bookId, new BookBorrowed(member, dueDate));
}

View C# snippet source on GitHub

This excerpt ignores the AppendResult for brevity. In real code, check IsSuccess as shown in Appending events. It also assumes a registered Chronicle client; Get started with Chronicle covers setup.

To read the history of one book back, ask the event log for the events of that event source and the event types you care about:

using System.Collections.Immutable;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
public class BookHistoryReader(IEventLog eventLog)
{
public Task<IImmutableList<AppendedEvent>> GetBorrowings(EventSourceId bookId) =>
eventLog.GetForEventSourceIdAndEventTypes(
bookId,
[typeof(BookBorrowed).GetEventType()]);
}

View C# snippet source on GitHub

What an event store gives you that a regular database does not

Section titled “What an event store gives you that a regular database does not”
  • Append-only guarantees. Events are not updated or deleted, and each has a fixed place in the order. That is a stronger audit story than a table that anyone with write access can alter.
  • Checks at append time. Chronicle validates each event against its schema and can reject an append that breaks a constraint such as uniqueness, or one that conflicts with a concurrent write.
  • Ordered observation. Projections, reducers and reactors read events in order and resume from where they stopped. Building this on top of an ordinary table, with change data capture and offsets, is possible but you own the work.
  • Replay. A new read model is built by replaying the history. You do not need a special migration for the past.
  • A query language over your business data. Use read models for that.
  • Joins and aggregates on the log. Do them in projections.
  • Instant read models. Views are built from events after the append, so they can lag briefly.
  • Free-form updates. Fixing a wrong record means appending a corrective event, not editing a row.

Choose a regular database when:

  • The data is reference data or settings that nobody audits.
  • The current state is all that matters and no process hangs on the changes.
  • The team needs read-after-write everywhere and cannot design for lagging views.
  • You need ad hoc reporting over arbitrary fields and have no appetite for building read models first.

You can mix both. Chronicle sits happily beside a relational or document store for the parts that are only current state. See when to use event sourcing.

  • Using the event store as a general-purpose database. Store facts about your domain, not arbitrary documents.
  • Querying the log for screens. Serve screens from read models.
  • Making events too generic. An event named RecordChanged is an update statement with a new name. Name the business fact.
  • Forgetting storage planning. The log only grows. Plan retention, backups and the underlying database like any other production datastore.
  • Assuming a message broker is an event store. A broker delivers messages and may discard them after delivery. An event store keeps events as the system of record. See event-driven architecture vs. event sourcing.
  • Skipping idempotency. After a timeout, an append may or may not have been stored. Use concurrency checks or constraints so a retry cannot duplicate it.

It is a specialised database. Some event stores run on top of NoSQL or SQL databases. Chronicle stores its data in MongoDB, PostgreSQL, SQL Server or SQLite, and adds sequence numbers, event types, constraints and observers on top.

Can I use PostgreSQL or SQL Server as an event store?

Section titled “Can I use PostgreSQL or SQL Server as an event store?”

You can build an event log in any relational database with an append-only table, and many teams do. You then own sequencing, concurrency checks, subscriptions and replay. A dedicated event store provides those. Chronicle can also use PostgreSQL or SQL Server as its storage.

On read models. A projection folds events into a read model that is stored in a database, and your queries run there.

Does an event store replace my main database?

Section titled “Does an event store replace my main database?”

Not usually. It is the system of record for events. Read models in a regular database serve queries.

Follow the Chronicle tutorial, or read about event sourcing in .NET.