Event Sourcing in Python with KurrentDB
July 23, 2026 ยท View on GitHub
Please note: following the rebranding of EventStoreDB to KurrentDB, this package is
the rebranding of eventsourcing-eventstoredb. Please
migrate your code to use the eventsourcing-kurrentdb
package when you are ready.
Event Sourcing in Python with KurrentDB
This is an extension package for the Python eventsourcing library that provides a persistence module for KurrentDB and EventStoreDB. It uses the kurrentdbclient package to communicate with KurrentDB via the gRPC interface. It is tested with KurrentDB 25.1, 26.0, 26.1, across Python versions 3.11 to 3.14.
Installation
Use pip to install the stable distribution from the Python Package Index.
$ pip install eventsourcing-kurrentdb
Please note, it is recommended to install Python packages into a Python virtual environment.
Getting started
Define aggregates and applications in the usual way. Please note, "streams"
in KurrentDB are constrained to start from position 0, and this
package expects the originator_version of the first event in an aggregate sequence
to be 0, so you must set INITIAL_VERSION on your aggregate classes to 0.
from typing import TypedDict, Any
from uuid import uuid4
from eventsourcing.domain import event
from eventsourcing.pydantic import Aggregate, AggregatesApplication, Decision
# Event model, expressed as pure business decisions.
class DogRegistered(Decision):
name: str
class TrickAdded(Decision):
trick: str
# Decision model, expressed as an event-sourced aggregate.
class Dog(Aggregate):
INITIAL_VERSION = 0 # for KurrentDB
@staticmethod
def create_id() -> str:
return f"dog-{uuid4()!s}"
@event(DogRegistered)
def __init__(self, name: str) -> None:
self.name = name
self.tricks: list[str] = []
@event(TrickAdded)
def add_trick(self, trick: str) -> None:
self.tricks.append(trick)
# Command and query handlers, expressed as module-level functions.
def register_dog(app: AggregatesApplication, name: str) -> tuple[str, int]:
dog = Dog(name=name)
recordings = app.save(dog)
return (dog.id, recordings[-1].notification.id)
def add_trick(app: AggregatesApplication, dog_id: str, trick: str) -> int:
dog = app.repository.get(dog_id, Dog)
dog.add_trick(trick)
recordings = app.save(dog)
return recordings[-1].notification.id
def get_dog_details(app: AggregatesApplication, dog_id: str) -> DogDetails:
dog = app.repository.get(dog_id, Dog)
return {"name": dog.name, "tricks": tuple(dog.tricks)}
class DogDetails(TypedDict):
name: str
tricks: tuple[str, ...]
# Binding to infrastructure, expressing the name of the bounded context.
class TrainingSchool(AggregatesApplication):
name = "training_school"
In this example, the commands and queries are defined with module-level functions. If you prefer, you can nest the functions under the application class as object methods, or alternatively define command handler and query handler classes.
Configuring the application to use KurrentDB
We need to configure the application infrastructure to use KurrentDB.
You can configure an application with environment variables by setting them in the
operating system environment, or by using the application constructor argument env,
or by setting the application class attribute env.
Set PERSISTENCE_MODULE to 'eventsourcing_kurrentdb'. Also set KURRENTDB_URI to a
KurrentDB connection string URI. This value will be used as the uri argument when
the KurrentDBClient class is constructed by this package.
kurrentdb_env = {
"TRAINING_SCHOOL_PERSISTENCE_MODULE": "eventsourcing_kurrentdb",
"TRAINING_SCHOOL_KURRENTDB_URI": "esdb://localhost:2113?Tls=false",
}
If you are connecting to a "secure" KurrentDB server, and if
the root certificate of the certificate authority used to generate the
server's certificate is not installed locally, then also set environment
variable KURRENTDB_ROOT_CERTIFICATES to an SSL/TLS certificate
suitable for making a secure gRPC connection to the KurrentDB server(s).
This value will be used as the root_certificates argument when the
KurrentDBClient class is constructed by this package.
kurrentdb_env[
"TRAINING_SCHOOL_KURRENTDB_ROOT_CERTIFICATES"
] = "<PEM encoded SSL/TLS root certificates>"
Please refer to the kurrentdbclient documentation for details about starting a "secure" or "insecure" KurrentDB server, and the "kdb" and "kdb+discover" KurrentDB connection string URI schemes, and how to obtain a suitable SSL/TLS certificate for use in the client when connecting to a "secure" KurrentDB server.
After configuring environment variables, construct the application.
app = TrainingSchool(kurrentdb_env)
Call application methods from tests and user interfaces. The returned
commit_position values can be used to wait for eventually-consistent
read models.
(dog_id, commit_position) = register_dog(app, "Fido")
commit_position = add_trick(app, dog_id, "roll over")
commit_position = add_trick(app, dog_id, "play dead")
dog_details = get_dog_details(app, dog_id)
assert dog_details["name"] == "Fido"
assert dog_details["tricks"] == ("roll over", "play dead")
To check the events have been durably saved in KurrentDB, rather then just in the application Python object, we can construct another instance of the application and get Fido's details again.
app = TrainingSchool(kurrentdb_env)
dog_details = get_dog_details(app, dog_id)
assert dog_details["name"] == "Fido"
assert dog_details["tricks"] == ("roll over", "play dead")
Eventually-consistent materialised views
To project the state of an event-sourced application "write model" into a
materialised view "read model", first define an interface for the materialised view
using the TrackingRecorder class from the eventsourcing library.
The example below defines methods to count dogs and tricks for the TrainingSchool
application
from abc import abstractmethod
from eventsourcing.persistence import Tracking, TrackingRecorder
class MaterialisedViewInterface(TrackingRecorder):
@abstractmethod
def incr_dog_counter(self, tracking: Tracking) -> None:
pass
@abstractmethod
def incr_trick_counter(self, tracking: Tracking) -> None:
pass
@abstractmethod
def get_dog_counter(self) -> int:
pass
@abstractmethod
def get_trick_counter(self) -> int:
pass
The MaterialisedViewInterface can be implemented as a concrete view class using a durable database such as PostgreSQL.
The example below counts dogs and tricks in memory, using "plain old Python objects".
from eventsourcing.popo import POPOTrackingRecorder
class InMemoryMaterialiseView(POPOTrackingRecorder, MaterialisedViewInterface):
def __init__(self) -> None:
super().__init__()
self._dog_counter = 0
self._trick_counter = 0
def incr_dog_counter(self, tracking: Tracking) -> None:
with self._database_lock:
self._assert_tracking_uniqueness(tracking)
self._insert_tracking(tracking)
self._dog_counter += 1
def incr_trick_counter(self, tracking: Tracking) -> None:
with self._database_lock:
self._assert_tracking_uniqueness(tracking)
self._insert_tracking(tracking)
self._trick_counter += 1
def get_dog_counter(self) -> int:
return self._dog_counter
def get_trick_counter(self) -> int:
return self._trick_counter
Define how events will be processed using the Projection class from the eventsourcing library.
The example below processes Dog events. The DogRegistered events are processed
by calling incr_dog_counter() on the materialised view. The TrickAdded events
are processed by calling incr_trick_counter().
from typing import Any
from eventsourcing.projection import Projection
from eventsourcing.utils import get_topic
class CountProjection(Projection[MaterialisedViewInterface]):
topics = (
get_topic(DogRegistered),
get_topic(TrickAdded),
)
def process_event(self, envelope: Any, tracking: Tracking) -> None:
match envelope.decision:
case DogRegistered():
self.view.incr_dog_counter(tracking)
case TrickAdded():
self.view.incr_trick_counter(tracking)
Run the projection with the ProjectionRunner class from the eventsourcing library.
The example below shows that when the projection is run, the materialised view is updated
by processing the event of the upstream event-sourced TrainingSchool application. It
also shows that when tricks are subsequently added to the application's aggregates,
events continue to be processed, such that the trick counter is incremented in the
downstream materialised view "read model".
import os
from eventsourcing.projection import ProjectionRunner
with ProjectionRunner(
application_class=TrainingSchool,
projection_class=CountProjection,
view_class=InMemoryMaterialiseView,
env=kurrentdb_env,
) as runner:
# Get "read model" instance from runner, because
# state of materialised view is stored in memory.
materialised_view = runner.projection.view
# Wait for the existing events to be processed.
materialised_view.wait(
application_name=app.name,
notification_id=commit_position,
timeout=5.0,
)
# Query the "read model".
dog_count = materialised_view.get_dog_counter()
trick_count = materialised_view.get_trick_counter()
# Record another event in "write model".
commit_position = add_trick(app, dog_id, "sit and stay")
# Wait for the new event to be processed.
materialised_view.wait(
application_name=app.name,
notification_id=commit_position,
timeout=5.0,
)
# Expect one trick more, same number of dogs.
assert dog_count == materialised_view.get_dog_counter()
assert trick_count + 1 == materialised_view.get_trick_counter()
# Write another event.
commit_position = add_trick(app, dog_id, "jump hoop")
# Wait for the new event to be processed.
materialised_view.wait(
application_name=app.name,
notification_id=commit_position,
timeout=5.0,
)
# Expect two tricks more, same number of dogs.
assert dog_count == materialised_view.get_dog_counter()
assert trick_count + 2 == materialised_view.get_trick_counter()
See the Python eventsourcing package documentation for more information about
projecting the state of an event-sourced application into materialised views
that use a durable database such as SQLite and PostgreSQL.
More information
For more information, please refer to the Python eventsourcing library, the Python kurrentdbclient package, and the KurrentDB website.
Contributors
Clone the GitHub repo and the use the following make commands.
Install Poetry.
$ make install-poetry
Install packages.
$ make install
Start UmaDB.
$ make start-kurrentdb
Run tests.
$ make test
Stop UmaDB.
$ make stop-kurrentdb
Check the formatting of the code.
$ make lint
Reformat the code.
$ make fmt
Tests belong in ./tests.
Edit package dependencies in pyproject.toml. Update installed packages (and the
poetry.lock file) using the following command.
$ make update