Domain-driven Design: Description language ESDM places model next to the code
The Event-sourced Domain Modeling Language describes event-driven domains as YAML next to the code and makes the model verifiable with the same toolchain.
(Image: GaudiLab/Shutterstock.com)
- Golo Roden
Domain-driven Design (DDD) and Event Sourcing rely on a precise vocabulary. Aggregates, Bounded Contexts, Process Managers, Read Models, Context Mappings: Thinking in these terms provides a common tool for discussing business systems. As long as the discussion takes place live during a workshop, this works reliably. The model is consistent because all participants carry it in their minds simultaneously.
However, as soon as the workshop ends, the problem begins. The model does not leave the room as a shared artifact, but as a collection of fragments: a few slides from the last review, a whiteboard photo, a README.md, scattered code comments, perhaps a diagram in the wiki. Each of these sources is plausible, but none is authoritative. In this article, I introduce the language ESDM (Event-Sourced Domain Modeling), developed by my company the native web, and explain why, in my opinion, it is the right answer to this issue.
Where models disappear once the workshop ends
The real drama unfolds in the weeks after the workshop. A developer submits a refactoring change that renames an aggregate. The code review approves it. No one thinks of the slides, no one opens the wiki, no one retrieves the whiteboard photo. The model on the whiteboard changes, the model in the minds of others remains unchanged, and the drift begins.
Six months later, a new colleague joins the project. She reads the onboarding slides because they are the only coherent description she can find. What she reads no longer matches reality. A Bounded Context has a different name, an event no longer exists, what was once an aggregate has been split into two. The slides are not a model; they are a photo of a model from a specific month.
Videos by heise
In this situation, there are essentially only three possible sources of truth, and none of them is good: the slides are outdated, the wiki is outdated, and the code does not describe the model; it executes it. Anyone who wants to know the business truth must reconstruct from the code what the original idea once was. This is strenuous, error-prone, and not scalable.
The core of the problem is that the model is not a first-class artifact. It has no fixed location, no binding format, no verifiable properties. It is nowhere at home, so it lives a little everywhere, and a little everywhere, in doubt, means nowhere properly.
What a modeling language must actually deliver
From this finding, a fairly precise list of requirements for a solution emerges. The model must be versionable, i.e., exist as text that version control can meaningfully diff. It must live alongside the code in the repository, so that the same pull request that changes the code also includes the model.
It must be verifiable, in the sense that a machine can automatically validate certain structural statements about the model. Whether each aggregate names its events, whether each event has a producer, whether each context mapping refers to an actually existing Bounded Context: such questions should not be answered by the reader, but by the toolchain.
It must be readable by tools, i.e., follow a unique grammar. Once the format is established, validators, generators, IDE integrations, and AI-assisted modeling tools can be built against it. This point sounds like a subordinate clause, but it is the lever that distinguishes a language from a convention.
And it must be free of dialects. A format that can be configured per team ultimately describes not a common language, but a family of related private languages. The value of a shared model collapses as soon as each project cuts its own rules. Anyone who wants to rely on the language needs a language that cannot be negotiated individually by each place.
ESDM places the model next to the code
ESDM, short for Event-Sourced Domain Modeling, is precisely the language that meets these requirements. The YAML-based language with a built-in toolchain is also free for commercial use. Models consist of files with the extension .esdm.yaml; each file contains one or more documents, each document declares an apiVersion and a kind entry, thus describing exactly one element of the domain.
The toolchain is a single binary that runs on macOS, Linux, and Windows. The schema against which the tools validate is embedded in the binary, not loaded from the network. This makes ESDM fully offline-capable: it works in isolated build environments, in CI runners without outgoing network connections, and on a laptop on a train.
The effect of this setup is tangible. The model is no longer in a remote system, but directly in the repository, in a directory next to the code. Anyone editing the code sees the model. Anyone editing the model sees it in the pull request diff. No one needs to remember that the model exists; it is visibly located in the directory tree.
Equally important is how this changes review behavior. As soon as the model is part of the repository, it becomes part of the code review. A refactoring change that renames an aggregate must carry the corresponding .esdm.yaml file in the same change, otherwise the linter will report the violation. Drift is prevented not by discipline, but by the toolchain.
Vocabulary and Extensions
In terms of content, ESDM covers the vocabulary used in Domain-driven Design and Event Sourcing. Domains and subdomains describe the business framework. Bounded Contexts delineate vocabularies from each other. Aggregates, Dynamic Consistency Boundaries (DCBs), and Process Managers carry the consistency rules. Events and Commands carry the facts and intentions. Read Models and Queries describe the read side. Context Mappings describe how Bounded Contexts relate to each other. Actors, Domain Services, Event Handlers, Policies, and External Systems fill the gaps that would otherwise remain implicit in real systems.
Thus, the core covers the write and read path of an event-driven system, from intention to fact to projected view. The model is not executable; it describes the what, not the how. This separation is the prerequisite for the same model to be applicable to different implementations without being tied to a runtime.
Beyond the core, there are extensions that describe artifacts around the actual modeling. The Domain Storytelling extension records discovery stories as a separate document type, with actors, work items, and the activities that connect them. The Given-When-Then extension captures behavioral examples, i.e., preceding events, a triggering action, and the expected outcomes.
Both extensions follow the same conventions as the core and are validated by the same toolchain. However, they do not inject new building blocks into the core, and a core document never validates against an extension schema. This asymmetry is deliberate: it keeps the core lean and yet allows the surface of the language to grow over time without endangering the existing codebase.
The versioning of the schema follows the same discipline you know from Kubernetes API groups. Each document declares an apiVersion, and this version binds the document to a specific schema generation. Non-breaking changes increment the schema revision without touching the apiVersion; breaking changes move to a new major version, and old documents continue to validate against the old schema. Stability is the default, changes are a conscious decision.
The linter keeps the model clean
At the center of the toolchain is the linter. It reads the .esdm.yaml files under a directory, parses them against the embedded schema, checks a fixed list of structural and modeling rules, and reports any violation as a diagnostic with file, line, and column. Clean modeling produces no output and exits the tool with status code 0.
The fixed rule catalog is a deliberate decision. There is no project configuration file, no severity switches, no disabling of individual rules. A rule applies or it does not; this decision is made by the toolchain, not the repository. The price for this is that a rule occasionally seems stricter than the specific situation might require. The gain is that every ESDM model, regardless of the team, guarantees the same properties. A language with switches ultimately describes many languages with the same name, and the value of the shared model is lost.
Diagnostics are locations, not stack traces. Each message refers to a file, a line, and a column and describes the problem in a short sentence that speaks the language of the modelers, not that of the toolchain. There are no error numbers to look up in a table, and no internal call chains that would be visible externally.
The linter's full effect is realized when it runs in the continuous integration pipeline. A pull request that renames an aggregate without carrying the model along fails the CI step and is not merged. A new event without a producer or consumer fails just as easily. The drift that previously had to be prevented by discipline is prevented by a short, precise diagnosis.
Consistent with ESDM
Specifically, this architecture pays off in several scenarios. In an Event Storming workshop or a Domain Storytelling session, ESDM gives discoveries a place where they can survive. Instead of archiving photos after the workshop, you write the events, commands, actors, and Bounded Contexts into .esdm.yaml files and immediately receive feedback on whether the model is internally consistent. An aggregate that has no events, a command that no one publishes, a process manager without a trigger: such gaps are revealed by the linter before they solidify as unspoken assumptions.
For an existing system whose model exists only in the minds of those involved, ESDM can be used as a retrospective description tool. Start with the consistency units: each aggregate, each process manager, each Dynamic Consistency Boundary (DCB), each read model. List the events published by each and the commands accepted. The exercise itself reveals gaps, and at the end, you have a model that can grow with the code instead of lagging it.
Once the model is in place, it becomes the natural format for discussions with business stakeholders. The view command renders the model as a hierarchical overview, from the domain down to subdomains and Bounded Contexts, and further down to consistency units and their events and commands. This overview is available at the push of a button and is guaranteed to match the source state. Domain experts read the same terms in it that they use themselves and can provide feedback without detours through implementation details.
In systems with multiple teams and shared Bounded Contexts, ESDM makes the contracts between teams explicit. A Context Mapping describes the relationship between two Bounded Contexts; a cross-team event reference is a verified fact, not a guess. What has once been agreed upon between two teams and captured in the model is then automatically checked by the toolchain. The business assumption is no longer just a shared memory, but a verifiable statement.
Finally, ESDM fits surprisingly well with AI-assisted workflows. The YAML is simple enough for large language models to read and write directly, and the fixed schema and named vocabulary provide the model with precisely the guardrails it needs to produce something coherent. Sketching a model from a domain discussion or extracting candidates for aggregates and events from a codebase are tasks that can be solved more reliably and verifiably with the ESDM vocabulary than when the model is described in prose.
What ESDM consciously is not
Just as important as describing what ESDM is, is defining what it is not. ESDM is not an event store. It does not store or read events; it does not put anything into a database. ESDM merely describes what events should exist.
ESDM is also not a framework. It does not prescribe a programming style, a class hierarchy, annotations, or a specific service structure. Which language, which framework, which library you use for the actual implementation is entirely up to you. ESDM does not interfere with these decisions.
ESDM is not a code generator or a runtime environment. It does not generate classes, migrations, or API endpoints from the model. It does not execute anything. This decision is not a gap, but intentional: a modeling language that also wants to generate or execute forces runtime decisions upon me, and a runtime that also models buries the model under implementation details.
ESDM keeps the two sides separate. It is a descriptive layer that lives alongside the code and keeps the model honest, and that is exactly what it is, nothing more. This parsimony is the prerequisite for ESDM to be freely combinable with what you actually run in production.
(rme)