A ToDo app built to show CQRS, event sourcing and the outbox pattern in one readable codebase
Complete working example of using Domain Driven Design (DDD), Hexagonal Architecture, CQRS, Event Sourcing (ES), Event Driven Architecture (EDA), Behaviour Driven Development (BDD) using TypeScript and NestJS. Like what you see? Don't forget to star! ⭐ ^^^
At a glance
- What is it?
- bitloops/ddd-hexagonal-cqrs-es-eda is a reference implementation that puts hexagonal architecture, DDD, CQRS, event sourcing, EDA and BDD into a NestJS project with PostgreSQL, NATS JetStream and Keycloak. The value is in the wiring you can read, and the cost is that the example application is deliberately heavier than the problem it solves.
- Who is it for?
- Adopt this repository as a reading and workshop reference if your team is deciding how to structure a NestJS service with CQRS, event sourcing and an outbox, and you have Docker, Node 24 and a few hours to run the stack. Do not adopt it as a starting template for a small application, because the README itself calls the result an over-engineered ToDo app, and the operational surface includes Keycloak, NATS JetStream, Jaeger, Grafana and Prometheus.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 17 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 6, 2026, and from our analysis. They are not legal advice.
Star history
- bitloops/ddd-hexagonal-cqrs-es-eda1,452 ★ · +2
| Repository | 2026-10-17 | 2026-11-06 |
|---|---|---|
| bitloops/ddd-hexagonal-cqrs-es-eda | 1.5k | 1.5k |
2 Stars in the last 20 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
What the project is actually for
The repository presents itself as a complete working example rather than a framework, and the distinction decides who should read it. The stated objective is to provide a reference implementation of how to design and create maintainable software, and the README says the concepts and patterns are not bound to any specific technologies even though the code is TypeScript on NodeJS with NestJS.
The application is a ToDo app, and the README is upfront about the result being over-engineered. That word is the most useful thing in the document for a reviewer. Most architecture samples hide their cost behind a small domain; this one names it, and the business requirements are correspondingly plain. Users register, log in, add todos, complete them, uncomplete them, change a title and view their list.
The one piece of business logic that is not obvious is the requirement that completing the first todo sends a congratulation email, a rule the README attributes mostly to the marketing team's needs. That detail exists to give the event driven side something to do, and it is the kind of deliberate, slightly artificial requirement that makes a sample legible. Read it as teaching material and it works. Read it as a product and it is a to-do list with a mail hook.
One PostgreSQL transaction carries the events, the projection and the outbox
The persistence section is the most technically specific part of the documentation, and it is the part worth reading twice. PostgreSQL is the sole application database. IAM and Marketing use relational tables, while the Todo aggregate is rehydrated from an append-only event stream.
The mechanism is the transactional outbox. Each Todo command writes its events, its query projection and its outbox messages in one PostgreSQL transaction. A separate outbox relay then publishes domain events to NATS JetStream with retry and at-least-once delivery semantics.
This is the design decision that separates the sample from a naive event store. Writing the domain event and the read model in the same transaction as the outbox row means there is no window where a crash leaves a committed event with no message to publish, and no window where a message was sent for an event that was rolled back. The cost is that delivery becomes at-least-once rather than exactly-once, which forces consumers to be idempotent. That trade-off is explicit in the documentation, and the backend architecture document is pointed at for the transaction, concurrency and consumer guarantees.
For a reader who wants one thing to take away from this repository, this is it. The pattern transfers to any language and any broker.
NATS JetStream carries the events, server-sent events carry them onward
The messaging stack is NATS, with JetStream providing the message streaming layer. The compose file shows a single NATS service started with the `-js` and `-m` flags, monitoring on port 8223, with a persistent volume mounted at the data path.
The compose file also shows the service topology, and it is worth reading as a map of the whole example. The backend depends on PostgreSQL and NATS starting, and on Keycloak reporting healthy, which tells you the application will not start before its identity provider is up. The frontend is a Vite build with three environment variables injected at build time: the API base URL, the OIDC authority and the OIDC client id.
External communication is REST plus server-sent events. The README lists realtime client events as a technical feature, and server-sent events are the mechanism, which is a sensible choice over websockets for a sample because the server-to-client direction is all that is needed to push state changes after a command.
The compose file exposes a lot of ports to the host, including the database port 5432, the NATS client and monitoring ports, and the Keycloak port. For a local learning environment that is convenient. It is also a reminder that this stack is not shaped like a hardened deployment, and the repository keeps a separate `k8s/` directory and an `apply_k8s_files.sh` script for anyone who wants to see the other deployment path.
Keycloak and OpenID Connect with PKCE do the authentication work
Authentication is handled by Keycloak through OpenID Connect Authorization Code Flow with PKCE, and the project ships a `keycloak/` directory with its own IAM setup. There is a separate document for the Keycloak IAM architecture and a roadmap for it.
Three features in the technical list are worth noting because they are where a sample either stops at the login screen or goes further. Authorization is applied even at the repository level, so access control is expressed in the domain rather than only in the controller. OpenID Connect session renewal is handled, which is the kind of detail that is invisible until it breaks. And the API side is configured with an issuer, an audience, a client id, a JWKS URI and a flag for requiring a verified email, all passed to the backend as environment variables.
The consequence for a reader is that this example spends real effort on identity. The domain events, the aggregate and the outbox are the interesting part, and the identity layer is a significant share of the surface area. A team that already has an identity provider and a house pattern for token validation will find much of the Keycloak setup redundant, and the point of the repository for them moves to the transaction boundary in PostgreSQL.
Running the stack: Docker, Node 24 and pnpm
The documented prerequisites for running the application are Docker and docker-compose. The README lists both, noting that a Docker installation may not bring compose along. The compose file at the repository root defines the frontend, backend, NATS, PostgreSQL and Keycloak services, and the repository also contains a `start.sh` script at the root.
The JavaScript side is a pnpm workspace, with `pnpm-workspace.yaml`, a `pnpm-lock.yaml` and separate `backend/` and `frontend/` directories. The root `package.json` pins the package manager and the Node engine range, and defines four scripts that each fan out to the workspace packages.
"scripts": {
"build": "pnpm --dir backend run build && pnpm --dir frontend run build",
"lint": "pnpm --dir backend run lint && pnpm --dir frontend run lint",
"test": "pnpm --dir backend run test && pnpm --dir frontend/tests run test",
"check:frontend": "pnpm --dir frontend run lint && pnpm --dir frontend run build && pnpm --dir frontend/tests run test"
}Note the asymmetry in that test script. The backend test target is the backend package, while the frontend tests live in a separate `frontend/tests` package, so the two suites are not the same kind of thing even though one command runs both. A team that copies the root script into its own monorepo will have to make the same choice about where frontend tests live.
Event storming and BDD are part of the deliverable, not just the code
The README has a design process section covering event storming, and the project lists Behaviour Driven Development among the patterns it teaches. The repository root contains a CHANGELOG, a CONTRIBUTING file, a SECURITY file and a `docs/` directory whose files include the backend architecture document and the Keycloak IAM roadmap.
The claim that the project teaches BDD is worth examining. Testing is done with JEST, and the observable behaviours the requirements describe, such as the first completed todo triggering a congratulation email, are the kind of rule that reads well as a scenario. What the documentation does not show is a directory of feature files, and there is no claim in the README about a specific Gherkin runner, so the honest reading is that the BDD influence is in how the domain is decomposed and written, not necessarily in the test file format.
The event storming section matters more than it sounds for a reader deciding whether to study the repository. It signals that the domain events in the code came from a modelling session rather than from a diagram drawn after the fact, and it gives you the vocabulary to ask questions about the aggregate. A separate `telemetry-consumer/` directory at the root suggests the code is also used to demonstrate consumption of event streams from outside the application boundary.
Observability is a first-class part of the example
The technical features list leads with observability, and the repository carries dedicated directories for the pieces of it: `prometheus/` for metrics configuration, `grafana/` for dashboards and a `telemetry-consumer/` at the root. Tracing uses Jaeger, and the README states that tracing tracks requests that span multiple modules and microservices.
That last phrase is the interesting one. In a modular monolith, a request that crosses module boundaries is not automatically visible, so tracing has to be wired through the application rather than added at the edge. The project also lists easy switching between a modular monolith and microservices among its features, which is a claim about the same seam: if the boundary carries a port interface and publishes events, the same code can run in one process or in several.
Logging and realtime client events sit alongside that. The combination describes a fairly complete operational story for a sample, which is exactly why the repository is long to read. A team that wants the architecture patterns should read the transaction boundary, the port interfaces and the event payloads first, and treat the dashboards as decoration. A team that wants a working demo has everything it needs, provided it also accepts running Keycloak, NATS, PostgreSQL, Prometheus and Grafana at the same time.
Release state, licence and the upgrade bill
The project reached 1.0.0 on 2026-07-31, followed by v1.0.1 the same day and v1.0.2 on 2026-08-02. The last push to the repository was on 2026-09-28, and the repository is not archived. The version number in the root `package.json` is 1.0.2, which is the same release the README points at as current.
The licence is MIT, which is about the least restrictive option available for a reference codebase, and for a project whose purpose is to be read, copied and adapted, that is the right call. The README asks readers to star the repository, which is a distribution strategy rather than a technical property, and the project invites questions through a Discord channel.
The upgrade bill for a consumer is worth naming. The Node engine range is narrow, `>=24 <25`, so the sample tracks a specific major version of Node rather than a range of them, and the package manager is pinned to `[email protected]`. Both pins make the example reproducible and both make it less forgiving. If you copy this project into your own repository, you inherit a decision to move to Node 25 when the range changes, and a workflow that assumes pnpm rather than npm. The `pnpm-lock.yaml` in the repository is what keeps the install reproducible in the meantime.
A dependency update bot is enabled for the project, which is a small sign that the example is kept working against upstream changes rather than frozen at a point in time.
Editorial conclusion
Adopt this repository as a reading and workshop reference if your team is deciding how to structure a NestJS service with CQRS, event sourcing and an outbox, and you have Docker, Node 24 and a few hours to run the stack. Do not adopt it as a starting template for a small application, because the README itself calls the result an over-engineered ToDo app, and the operational surface includes Keycloak, NATS JetStream, Jaeger, Grafana and Prometheus. Verify first that a Todo command writes its events, its query projection and its outbox rows in one PostgreSQL transaction, since that guarantee is the piece worth copying regardless of whether you keep the rest.
Frequently asked questions
What does the ddd-hexagonal-cqrs-es-eda repository demonstrate?
It is a working NestJS and TypeScript example of Domain Driven Design, hexagonal architecture, CQRS, event sourcing, event driven architecture and BDD, built around a ToDo application with a rule that sends a congratulation email on the first completed todo.
How does the project guarantee that domain events are published?
Each Todo command writes its events, its query projection and its outbox messages in one PostgreSQL transaction. An outbox relay then publishes to NATS JetStream with retry and at-least-once delivery semantics, so consumers must be idempotent.
What do I need to run the example application?
Docker and docker-compose are the documented prerequisites. The root package.json also requires Node 24, pinned as >=24 <25, with [email protected] as the package manager.
Which databases and messaging system does the example use?
PostgreSQL is the only application database, with IAM and Marketing on relational tables and the Todo aggregate rehydrated from an append-only event stream. NATS with JetStream carries the published domain events.
How is authentication handled in the example?
Authentication uses Keycloak through OpenID Connect Authorization Code Flow with PKCE, with authorization applied even at the repository level and OpenID Connect session renewal handled by the backend.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/bitloops-ddd-hexagonal-cqrs-es-eda)Star history chart for your README
[](https://hysenlabs.com/projects/bitloops-ddd-hexagonal-cqrs-es-eda)Was this useful?