Tutorial Series¶
This is a hands-on, in-progress series that takes you from "I have heard of event
sourcing" to a running, tested, production-shaped service built with eventsource-py.
The roadmap below sketches a full 21-part arc across four phases; 8 tutorials are
written so far (2, 3, 6, 7, 8, 11, 14, and 16). Start with
Tutorial 2: First Event -- it is the earliest one that exists and
the real entry point today, even though it is not numbered 1.
The series is a learning path, not a lookup table. Every tutorial is written to be typed out and run: you write code, run it, see output, and only then read the explanation of what happened. Nothing here asks you to trust a snippet you have not executed. If you want task-focused recipes instead, read Choose an Event Store Backend; if you want the shape of an API, read the Event Store Protocol reference; if you want the reasoning behind a design choice, read Aggregate Styles, The Decider Pattern, or the ADRs.
The library itself is async-first and built on pydantic v2, SQLAlchemy 2, and orjson. Those three packages are the only required dependencies -- every backend (PostgreSQL, SQLite, Redis, Kafka, RabbitMQ) and OpenTelemetry tracing arrives as an optional extra. The series is sequenced to respect that: the first ten tutorials need nothing but the core install and run entirely in-process, and Docker only appears once you reach the backend tutorials in Phase 3.
You do not need to read all of it, written or planned. Phase 1 alone is enough to model a domain and persist it in memory; Phase 2 adds the patterns most services actually ship with. Phases 3 and 4 are there when you need a real database, real subscriptions, and real operational concerns. Jump straight to a numbered tutorial in the nav sidebar if you already know which piece you need -- the table below shows which numbers exist today.
What You'll Build¶
The series has one running example: a small ordering service. It starts as a single
Python file you can run with python, and by the end it is meant to grow into a
multi-tenant, observable service backed by PostgreSQL with subscriptions, snapshots, and
an outbox. You are not building 21 disconnected toys -- each tutorial adds the next piece
to code you already have.
The domain stays deliberately small so the machinery stays visible. Orders get placed,
shipped, and cancelled. That is it. Three events -- OrderCreated, OrderShipped,
OrderCancelled -- are meant to carry you through the whole arc, so every new concept
lands against a model you already understand.
Status: What's Written, What's Planned¶
| # | Tutorial | Status |
|---|---|---|
| 1 | Introduction / setup | planned |
| 2 | First Event | written |
| 3 | First Aggregate | written |
| 4-5 | Repository, optimistic concurrency | planned |
| 6 | Projections | written |
| 7 | Event Bus | written |
| 8 | Testing | written |
| 9-10 | DLQ, checkpoints | planned |
| 11 | PostgreSQL | written |
| 12-13 | SQLite, advisory locks | planned |
| 14 | Snapshotting | written |
| 15 | Outbox pattern | planned |
| 16 | Multi-Tenancy | written |
| 17-21 | Distributed buses, observability, sagas, live migration | planned |
Below is the roadmap for all four phases, written up front as a design target. The sections and tutorial numbers it references describe the intended shape of the finished series -- treat anything not linked in the table above as not yet written, not as something broken.
Phase 1 (Tutorials 1-5) takes you to a working domain model in memory. You write
DomainEvent subclasses for the three order events and watch them auto-register into
the global EventRegistry; build an OrderAggregate on top of
DeciderAggregate[OrderState] with a decide()/evolve() pair driven by
CreateOrder, ShipOrder, and CancelOrder commands that fold events into a frozen
pydantic state model; append and read those events through the EventStore interface
using InMemoryEventStore; and wire an AggregateRepository in front of the store so
saving an order is one await and concurrent writers collide with a real
OptimisticLockError instead of silently overwriting each other. Nothing is installed
beyond the core package, and nothing is persisted past the end of the process.
Phase 2 (Tutorials 6-10) adds the read side and the safety net. You add an
order-summary projection -- first as a hand-written Projection, then rebuilt as a
ReadModelProjection over a typed read model -- so you can answer queries without
replaying an aggregate. You publish events through InMemoryEventBus to several
independent subscribers (a notifier, an inventory view, an audit log) and see how handler
ordering actually works. You write real tests using eventsource.testing -- the
harness, the assertion helpers, the BDD given/when/then style, the event builder, and the
conformance suites that any backend implementation must pass. Then you make it
survivable: route poison events into a DLQRepository, and record progress in a
CheckpointRepository so a restarted consumer resumes where it stopped instead of
replaying from zero. Still no Docker; the in-memory backends carry all of it.
Phase 3 (Tutorials 11-15) makes the service durable and lets it run unattended. You
swap InMemoryEventStore for PostgreSQLEventStore, apply the SQL schema, and use
PostgreSQL advisory locks to serialize work across processes. You do the same with
SQLiteEventStore and SQLiteSnapshotStore for local development and tests. You stand
up a SubscriptionManager that owns the lifecycle of your projections -- runners, retry
policy, health reporting, checkpointing -- so consumers keep up without you babysitting
them. You add snapshots so an order with thousands of events still loads in constant
time. And you close the gap between "committed to the database" and "published to the
bus" with the outbox pattern, using PostgreSQLOutboxRepository, plus the operational
checklist that goes with deploying any of this. This is the phase where Docker appears:
docker-compose.test.yml brings up the PostgreSQL 15 and Redis 7 services the backend
tutorials use.
Phase 4 (Tutorials 16-21) covers the pieces real deployments need. Tenant isolation
via TenantDomainEvent and the contextvar-based tenant scopes, so one store serves many
customers without leaking rows between them. A distributed event bus -- Redis, Kafka, or
RabbitMQ, whichever fits, each behind the same EventBus interface you already know.
OpenTelemetry traces that follow a command from HTTP request through append, publish,
and projection. And the advanced aggregate patterns: process managers, sagas, and the
live migration tooling in eventsource.application.migration for moving a running
system from one event store to another with dual-write and cutover.
Two things worth setting expectations on. First, these later phases are a menu, not a queue -- you almost certainly do not need all three message buses, and picking one is the point. Second, each phase is designed to leave you with something that runs, once its tutorials exist: Phase 1 alone should give you a tested domain model, Phase 1 and 2 together a complete in-process event-sourced application. Nothing is meant to be left half-wired waiting for a tutorial you never reach -- but check the status table above before assuming a given number is there.
Who This Series Is For¶
You will get the most out of this series if you are a Python developer who has written async code before and wants to build an event-sourced service rather than read about one. Concretely, the series assumes you:
- Are comfortable with
async/await,asyncio.run(), and async context managers. Nearly every store, bus, repository, and projection method ineventsource-pyis a coroutine, and the tutorials await them without stopping to explain the syntax. - Can read and write type-annotated Python. The library ships a
py.typedmarker and is type-checked with mypy; the tutorials use annotations throughout because they are how the API communicates intent. - Have used pydantic, or are willing to pick it up as you go.
DomainEventis a pydantic v2BaseModelwithmodel_config = ConfigDict(frozen=True), so field declaration, validation, and immutability all follow pydantic rules. - Run Python 3.13 or newer. That is the floor declared in
pyproject.toml(requires-python = ">=3.13").
You do not need prior event sourcing experience. Tutorial 2 starts from the concepts -- events as the source of truth, aggregates as consistency boundaries, projections as derived read models -- and no later tutorial assumes you arrived knowing CQRS or DDD vocabulary. Terms are introduced where they are first used.
You also do not need deep expertise in any particular backend. SQLAlchemy is a required
dependency, but Phases 1 and 2 never touch a database: they run against
InMemoryEventStore and the in-memory event bus. When PostgreSQL, SQLite, Redis, Kafka,
and RabbitMQ do appear in Phases 3 and 4, each tutorial covers the setup it needs,
including the Docker services from docker-compose.test.yml.
This series is a poor fit in two cases. If you already run event sourcing in production and just need to know how a specific API behaves, skip to the Event Store Protocol reference and the Choose an Event Store Backend how-to guide -- the tutorials deliberately move slowly and repeat themselves. And if you are evaluating whether event sourcing is the right architecture at all, read Aggregate Styles and the ADRs first; this series shows you how, and largely takes the why as settled.
Prerequisites¶
Before Tutorial 2, get a Python environment, install the package, and -- if you plan to reach Phase 3 -- confirm Docker works. That is the whole list. Everything else the series needs, it installs when it needs it.
Python and Tooling¶
You need Python 3.13 or newer. pyproject.toml declares
requires-python = ">=3.13". Check what you have:
The tutorials use uv for environment and dependency
management, because that is what the repository itself uses. If you would rather use
pip and venv, every command translates directly -- uv add X becomes
pip install X inside an activated virtualenv -- and nothing in the library depends on
uv.
To start a fresh project for the series:
If instead you cloned this repository to follow along against its source and tests, set up the development environment with:
That installs the library plus every optional backend and the dev extra's tooling
(pytest 8, pytest-asyncio, pytest-cov, mypy, ruff, testcontainers, pre-commit). Note that
pytest runs with asyncio_mode = "auto", so async tests in this repository need no
@pytest.mark.asyncio decorator.
You will also want an editor with mypy or Pyright running. The library ships a py.typed
marker and is type-checked under mypy strict, so your editor can tell you that an
aggregate's state type does not match its events before you run anything. The tutorials
never require a type checker, but they are much easier to follow with one.
Installing eventsource-py¶
Core install¶
This pulls in exactly three runtime dependencies, all declared in pyproject.toml:
pydantic (>=2.8.0,<3.0), SQLAlchemy (>=2.0.43,<3.0), and orjson
(>=3.10.7). No database driver, no message broker client, no telemetry SDK. pydantic
gives DomainEvent its validation and frozen-model behaviour; SQLAlchemy provides the
engine and session types the database backends are built on, which is why it is required
even before you connect to a database; orjson is the fast JSON path the serialization
layer wraps directly, with no stdlib fallback.
Confirm the install worked by importing the pieces Tutorial 2 opens with:
Every public name comes from the top-level eventsource package. You will not import
from submodules such as eventsource.adapters.memory in this series -- the tutorials
always use the re-exported top-level path.
The core install is all you need for Tutorials 1 through 10 -- the whole of Phase 1
and Phase 2. Those tutorials run against InMemoryEventStore, InMemoryEventBus,
InMemorySnapshotStore, and the in-memory checkpoint, DLQ, and outbox repositories
(InMemoryCheckpointRepository, InMemoryDLQRepository, InMemoryOutboxRepository), so
they execute in-process with nothing listening on a port and nothing to clean up
afterwards. The trade-off is that those backends hold everything in Python objects:
restart the process and the event store is empty again. That is fine -- and deliberate --
until Phase 3, where durability becomes the point.
If you are working from a clone of the repository rather than a fresh project, uv sync
with no extras gives you the same core-only environment.
Optional extras¶
Every backend and the tracing integration ship as an extra, declared under
[project.optional-dependencies] in pyproject.toml. Install only what the tutorial in
front of you actually asks for -- there is no penalty for adding one later.
| Extra | Installs | First needed in |
|---|---|---|
postgresql |
asyncpg>=0.30 |
Tutorial 11 -- PostgreSQL |
sqlite |
aiosqlite>=0.19 |
Tutorial 12 -- SQLite |
redis |
redis>=8.0,<9.0 |
Tutorial 17 -- Redis |
kafka |
aiokafka>=0.12,<1.0 |
Tutorial 18 -- Kafka |
rabbitmq |
aio-pika>=9.0 |
Tutorial 19 -- RabbitMQ |
telemetry |
opentelemetry-api, opentelemetry-sdk (both >=1.16.0,<2.0) |
Tutorial 20 -- Observability |
Combine them in one command:
Three bundles save you some typing. all-backends is postgresql + sqlite -- a
reasonable target if you intend to work through all of Phase 3. all pulls in every
extra at once, which is what uv sync --all-extras gives you in a clone of the
repository. And kafka-schema-registry adds confluent-kafka on top of aiokafka;
Tutorial 18 mentions it but does not require it.
Two names catch people out. The extra is spelled postgresql, not postgres -- but the
pytest marker for those tests is postgres (uv run pytest -m postgres). And the
OpenTelemetry extra is telemetry, even though the code lives in
eventsource.observability.
What happens if you skip an extra depends on which one:
- Message buses fail loudly at construction. Each bus module sets an availability
flag at import time --
REDIS_AVAILABLE,RABBITMQ_AVAILABLE,KAFKA_AVAILABLE, all exported fromeventsource-- and its constructor checks the flag first. Building aKafkaEventBuswithoutaiokafkaraisesKafkaNotAvailableError; Redis and RabbitMQ raiseRedisNotAvailableErrorandRabbitMQNotAvailableErrorthe same way. You get a named error, not a traceback from somewhere inside the library. - SQLite fails at construction, not import.
SQLiteEventStoreis always re-exported fromeventsource, sofrom eventsource import SQLiteEventStoresucceeds either way;AIOSQLITE_AVAILABLErecords whetheraiosqliteactually imported. Without the extra, constructing the store raises a plainImportErrorat__init__time -- so if a Tutorial 12 example fails when you instantiate the store, that is why. (SQLiteOutboxRepositoryis the one name that really does disappear from the namespace without the extra.) - PostgreSQL fails at connect time. The PostgreSQL store is built on SQLAlchemy's
async engine, so the missing piece is the driver in your URL:
create_async_engine("postgresql+asyncpg://...")cannot resolveasyncpgand SQLAlchemy raises before any query runs. - Tracing degrades silently, by design. Without the
telemetryextraOTEL_AVAILABLEisFalseand the library hands out aNullTracer, so instrumented code paths keep working and simply emit no spans. Nothing breaks -- you just see no traces, which is the one failure mode here you have to notice yourself.
Nothing in this table is needed before Tutorial 11. If you are starting the series today, skip ahead and install extras when a tutorial tells you to.
Docker services for the backend tutorials¶
Phase 3 onward needs real services. The repository's docker-compose.test.yml defines
the two the tutorials use:
- PostgreSQL 15 -- database
eventsource_test, usertest, passwordtest, published on host port 5433 (override withPOSTGRES_PORT). - Redis 7 -- published on host port 6380 (override with
REDIS_PORT).
Both ports are deliberately non-default so the test services do not collide with a PostgreSQL or Redis you already run locally.
Both services declare health checks, so give them a few seconds before connecting; when you are done:
The -v drops the postgres_test_data and redis_test_data volumes, which is what you
want between tutorials -- each backend tutorial assumes an empty event store.
Kafka and RabbitMQ are not in this compose file. Tutorials 18 and 19 cover standing those brokers up themselves, and both tutorials are optional: you only need whichever message bus you actually plan to deploy.
You need nothing from this section until Tutorial 11. If Docker is unavailable to you, Phases 1 and 2 still work end to end, and Tutorial 12 (SQLite) gives you durable persistence with no service to run at all.