Resources / Blog / ESDM 0.15.0: Last Call Before 1.0
AnnouncementSeptember 21, 202610 min read

ESDM 0.15.0: Last Call Before 1.0

Ten weeks ago we put ESDM under the MIT license and asked you to tell us what works and what doesn't. ESDM 0.15.0, released last week, is what came back. It is a big release, and most of it traces to someone using ESDM on a real model and reporting what happened: a vocabulary that speaks more than one language, a command that turns a model into a browsable set of pages, a stable way to point at any element from outside the model, and a linter that explains every finding it makes.

Some of it will turn a clean model red, and we expect this to be the last release before 1.0. Both facts belong in the same sentence, because together they are the reason to read on: here is what changed, what breaks and how to fix it, and why the next few weeks are the moment to tell us where we got it wrong.

Words in More Than One Language#

The ubiquitous language of a Bounded Context is one term per concept, on purpose: the point of choosing one word is that there is one word. What that could not hold was a team that speaks German with its domain experts and writes English code. Such a team had to drop one of the two words, and in practice it dropped the one the experts say. That need was raised in July, and a proposal for a SKOS-based extension – SKOS being the W3C vocabulary for thesauri – named the three cases that matter: two words for one concept, a translation that quietly misleads, and a term that corresponds to a different term in the next context over.

We declined the proposal's shape, an extension with a second document per context, because the ubiquitous language is not something that sits beside the model – the model is written in it – and two documents for one vocabulary would drift apart. An export to SKOS remains possible later, as a projection of the model rather than its source. The three cases went straight into the design, as fields on the documents that already exist. A status per term, which the proposal also asked for, needs no field: a term in the model is the agreed one, and Git records who decided. A Bounded Context now declares the language its vocabulary is written in, and a term can carry translations: the same concept in another language, with its own definition and its own avoid list, so a translation that would mislead – Kauf for Acquisition, when donations are acquisitions too – is named and refused rather than tolerated.

language: en
ubiquitousLanguage:
  - term: Acquisition
    definition: The process of adding a book to the catalog, whether bought or donated.
    translations:
      - language: de
        term: Erwerb
        definition: Die Aufnahme eines Buchs in den Katalog, ob gekauft oder gespendet.
        avoid:
          - term: Kauf
            reason: Auch Spenden sind ein Erwerb.

Within each language there is still exactly one term, so the principle holds per language rather than being abandoned. Where one context depends on another, the Context Mapping between them names two roles, such as customer and supplier or upstream and downstream, and its new terms list records the correspondence by those roles: what Sales calls a Customer, Billing calls an Account. And esdm glossary --language de renders the whole glossary for the people who speak German, marking every term that has no translation yet, so the gaps stay visible instead of disappearing.

A Page for Every Element, and an Address for Each#

In early July, esdm documentation went on our list, and two reports from the same weeks show why. Someone wanted to link from a specification to individual elements of the model, and had resorted to one file per element so that a file path could serve as the address. Someone asked why esdm glossary on a full model produced an empty page. The glossary shows definitions, not names, and that model had no terms declared yet – but the question was reaching for a browsable list of the model's elements, and the closest thing to one was the transient output of esdm view. Both point at the same thing: a rendering of the whole model that a person can browse and a document can point into.

esdm documentation is the first half. It writes the model as a tree of Markdown pages, one per element, containers as directories with a README.md, everything cross-linked with relative links and ready to be committed next to the code or read on GitHub. It renders the same tree esdm view renders, so the two cannot disagree about where an element sits. The reference notation is the second half: a URI such as esdm:domain=library/bounded-context=catalog/aggregate=book that walks the model from the Domain inward and names the kind and the name at every step. It is a name, not a location, so it survives every file move, split, and merge, and the page for an element follows from its reference by a fixed rule: drop the esdm: prefix, replace each equals sign with an underscore, and end a container in README.md, so esdm:domain=library/bounded-context=catalog/aggregate=book is the page domain_library/bounded-context_catalog/aggregate_book/README.md.

The Linter Explains Itself#

ESDM's linter is opinionated, and one of its opinions was contested in July: an Event bound to an Aggregate should not repeat the Aggregate's name, so instrument-cataloged under instrument draws a warning. The rule is right – every name derived from the model, a class in code or an event type in EventSourcingDB, would otherwise double the Aggregate, as in InstrumentInstrumentCataloged – but the reason was written nowhere. The linter had a rule ID and a message, and no place a reader could go to learn what the rule protects.

0.15.0 closes that gap for every rule at once. The documentation gains Linter Rules reference pages, one entry per rule and per pipeline diagnostic – the errors raised before any rule runs – each saying what it checks and why. Every finding now ends in a see line that links to its entry, and the JSON output carries the same address as documentationUrl, so your tooling can point people there too. An opinionated linter owes its reasons, and now it pays them.

Where the tooling was wrong rather than unexplained, it is corrected. A field report from a model of about 175 documents described two name collisions that esdm view seemed to hide, and chasing them turned up more than the report could see: esdm view never rendered Entities or free-standing Events at all, and esdm view <path> showed only the first element matching the last segment rather than all of them. Both are fixed, and a Given-When-Then Feature now sits under the Aggregate, Dynamic Consistency Boundary, Process Manager, or Read Model it is about rather than under the Domain.

Three smaller corrections came the same way. event-without-publisher no longer fires when a Dynamic Consistency Boundary publishes an Aggregate's Event, because publisher resolution now lives in one place rather than three. Field references such as identifiedBy.field accept userId as well as user-id. And the update hint stays quiet when esdm completion runs from a shell profile, where it had greeted one user on every new terminal.

If you run ESDM today, update to 0.15.0, run esdm lint, and then try esdm documentation --output docs on your own model. The update guide has the steps, and the next section has what to do if the linter comes back red.

What Breaks, and What to Do About It#

Three changes turn a model that is clean under 0.14.0 into one that fails under 0.15.0, and one new warning may appear. Each shows up in the output – the first two with a few follow-on errors from the schema validator that disappear once the cause is fixed – and each fix stays inside the model.

A Bounded Context that declares a ubiquitousLanguage must now declare its language. The linter reports missing required field "language" on the context. Add the language the terms are written in as a tag, language: en or language: de – BCP 47, the same tags the web uses – and the finding is gone. A context without terminology is unaffected.

A Given-When-Then rejection must name an invariant. The free-form then.rejection.reason is gone from the schema; every rejection is { invariant: <name> }, so its cause is a rule the unit actually declares. A Scenario that used reason now fails validation, with unknown field "reason" among the errors. Replace it with the name of the invariant the Scenario protects, and add that invariant to the unit if it was never written down – which is usually what the free-form text was hiding. A new warning, uncovered-invariant, points the other way: on an Aggregate or Dynamic Consistency Boundary that has Features, an invariant no Scenario exercises is reported as untested.

No two of the five domain types may share a name in one Bounded Context. In code, Aggregates, Dynamic Consistency Boundaries, Entities, Value Objects, and Domain Services end up as the types of one module under their own names – nothing is composed into them the way an Aggregate is composed into the names of its Events – so an Aggregate and a Dynamic Consistency Boundary both called loan would collide there, and are now an error, esdm/structure/ambiguous-name, reported at the second definition with a note at the first. Rename one of them and its references. Every other pair may still share a name – an Aggregate and a Read Model both called order are one concept on the write and the read side – and the Bounded Context page draws the line.

One step touches no model but belongs here. If your project has a schemas/ directory from esdm add-schema, the linter stops with schemas-directory-drift until you run esdm update-schema, because the schemas embedded in the new binary differ from the ones on disk, and the linter refuses to run with two sets of rules that disagree.

Still 0.x#

ESDM is a 0.x release, and the number means what it says: until 1.0, a release may still change the schema and the rules in ways that turn a clean model red. When we wrote in May that ESDM's core language is stable, and in July that the core had settled, that was a statement about pace – the schema stopped moving every week – not a promise that nothing would move before 1.0. Three things do move in 0.15.0, and the previous section says what to do about each. From 1.0 on, a different contract holds: a breaking change then means a new major of the apiVersion, and documents written against the old one keep validating. That is the mechanism the design principles describe, and 1.0 is where it takes over.

We expect 0.15.0 to be the last release before 1.0. That makes the coming weeks unusual: a change to the schema now costs you a line in a model, and after 1.0 it costs us a new apiVersion. If a shape in this release is wrong – if translations does not fit how your team actually works, if the reference notation lacks a kind you need, if a rule fires where it should not – this is the cheapest moment there will ever be to say so.

Feedback does not mean every request lands, and it should not. We said no to distributing ESDM through package managers, because a single binary you can pin per project and download into a CI job is a deliberate choice, and Homebrew plus its counterparts on Windows and Linux are more release work than we can carry right now. We said no to an OOUX extension – Object-Oriented UX, a method that structures user interfaces around domain objects – because ESDM describes the domain and a UI structure sits one layer further out. We said no to citation fields in the schema, because where a modeler's knowledge comes from is information about the model rather than about the domain.

Each answer is in the issue with its reasons, and each can be reopened by a case we have not seen. A no with a reason is still an answer, and every request gets one.

Say It Before 1.0#

Ten weeks of your feedback shaped this release more than any plan of ours did: the multilingual vocabulary, the reference notation, the rule pages, and most of the fixes each started as an issue about a real model. The next few weeks decide what 1.0 looks like, and that works the same way.

If you use ESDM, update and tell us what you find – open an issue on GitHub or write to hello@thenativeweb.io, including what worked better than you expected. If you have not tried ESDM yet, Your First Model takes you from install to a lint-clean model, and 0.15.0 is a good version to start with: it is the one 1.0 will be measured against.

Written by
Golo Roden

CTO and founder at the native web. www.thenativeweb.io