<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://www.eventfoundation.io/">
  <id>https://www.eventfoundation.io/resources/blog</id>
  <title>Blog – Event Foundation</title>
  <link href="https://www.eventfoundation.io/resources/blog/feed.xml" rel="self"></link>
  <link href="https://www.eventfoundation.io/resources/blog"></link>
  <updated>2026-09-21T00:00:00+02:00</updated>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/esdm-0-15-0-last-call-before-1-0</id>
    <title>ESDM 0.15.0: Last Call Before 1.0</title>
    <link href="https://www.eventfoundation.io/resources/blog/esdm-0-15-0-last-call-before-1-0"></link>
    <published>2026-09-21T00:00:00+02:00</published>
    <updated>2026-09-21T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">Ten weeks ago we put ESDM under the MIT license and asked you to tell us what works and what doesn&#39;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.</summary>
    <content type="html">&lt;p&gt;Ten weeks ago we &lt;strong&gt;&lt;a href=&#34;/resources/blog/esdm-is-now-open-source&#34;&gt;put ESDM under the MIT license&lt;/a&gt;&lt;/strong&gt; and asked you to tell us what works and what doesn&#39;t. &lt;strong&gt;ESDM 0.15.0, released last week, is what came back.&lt;/strong&gt; 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.&lt;/p&gt;&#xA;&lt;p&gt;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.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;words-in-more-than-one-language&#34;&gt;Words in More Than One Language&lt;a class=&#34;anchor&#34; href=&#34;#words-in-more-than-one-language&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;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. &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm/issues/10&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;That need&lt;/a&gt;&lt;/strong&gt; was raised in July, and &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm/issues/45&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;a proposal for a SKOS-based extension&lt;/a&gt;&lt;/strong&gt; – 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.&lt;/p&gt;&#xA;&lt;p&gt;We declined the proposal&#39;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. &lt;strong&gt;A Bounded Context now declares the &lt;code&gt;language&lt;/code&gt; its vocabulary is written in&lt;/strong&gt;, and a term can carry &lt;code&gt;translations&lt;/code&gt;: the same concept in another language, with its own definition and its own &lt;code&gt;avoid&lt;/code&gt; list, so a translation that would mislead – &lt;code&gt;Kauf&lt;/code&gt; for &lt;code&gt;Acquisition&lt;/code&gt;, when donations are acquisitions too – is named and refused rather than tolerated.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;language&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;en&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;ubiquitousLanguage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;term&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Acquisition&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;definition&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;The process of adding a book to the catalog, whether bought or donated.&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;translations&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;language&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;de&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;term&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Erwerb&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;definition&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Die Aufnahme eines Buchs in den Katalog, ob gekauft oder gespendet.&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;avoid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;term&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Kauf&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;reason&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;Auch Spenden sind ein Erwerb.&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Within each language there is still exactly one term, so &lt;strong&gt;the principle holds per language rather than being abandoned&lt;/strong&gt;. Where one context depends on another, the &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io/concepts/context-mapping/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Context Mapping&lt;/a&gt;&lt;/strong&gt; between them names two roles, such as customer and supplier or upstream and downstream, and its new &lt;code&gt;terms&lt;/code&gt; list records the correspondence by those roles: what Sales calls a Customer, Billing calls an Account. And &lt;code&gt;esdm glossary --language de&lt;/code&gt; renders the &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io/getting-started/running-esdm-glossary/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;whole glossary&lt;/a&gt;&lt;/strong&gt; for the people who speak German, marking every term that has no translation yet, so the gaps stay visible instead of disappearing.&lt;/p&gt;&#xA;&lt;h2 id=&#34;a-page-for-every-element-and-an-address-for-each&#34;&gt;A Page for Every Element, and an Address for Each&lt;a class=&#34;anchor&#34; href=&#34;#a-page-for-every-element-and-an-address-for-each&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;In early July, &lt;code&gt;esdm documentation&lt;/code&gt; went on our list, and two reports from the same weeks show why. Someone wanted to &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm/issues/8&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;link from a specification to individual elements&lt;/a&gt;&lt;/strong&gt; of the model, and had resorted to one file per element so that a file path could serve as the address. Someone asked why &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm/issues/26&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;esdm glossary&lt;/code&gt; on a full model produced an empty page&lt;/a&gt;&lt;/strong&gt;. 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&#39;s elements, and the closest thing to one was the transient output of &lt;code&gt;esdm view&lt;/code&gt;. Both point at the same thing: &lt;strong&gt;a rendering of the whole model that a person can browse and a document can point into.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;https://www.esdm.io/getting-started/running-esdm-documentation/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;esdm documentation&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; is the first half. It writes the model as a tree of Markdown pages, one per element, containers as directories with a &lt;code&gt;README.md&lt;/code&gt;, everything cross-linked with relative links and ready to be committed next to the code or read on GitHub. It renders the same tree &lt;code&gt;esdm view&lt;/code&gt; renders, so the two cannot disagree about where an element sits. The &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io/reference/reference-notation/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;reference notation&lt;/a&gt;&lt;/strong&gt; is the second half: a URI such as &lt;code&gt;esdm:domain=library/bounded-context=catalog/aggregate=book&lt;/code&gt; that walks the model from the Domain inward and names the kind and the name at every step. &lt;strong&gt;It is a name, not a location&lt;/strong&gt;, 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 &lt;code&gt;esdm:&lt;/code&gt; prefix, replace each equals sign with an underscore, and end a container in &lt;code&gt;README.md&lt;/code&gt;, so &lt;code&gt;esdm:domain=library/bounded-context=catalog/aggregate=book&lt;/code&gt; is the page &lt;code&gt;domain_library/bounded-context_catalog/aggregate_book/README.md&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-linter-explains-itself&#34;&gt;The Linter Explains Itself&lt;a class=&#34;anchor&#34; href=&#34;#the-linter-explains-itself&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;ESDM&#39;s linter is opinionated, and one of its opinions &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm/issues/27&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;was contested&lt;/a&gt;&lt;/strong&gt; in July: an Event bound to an Aggregate should not repeat the Aggregate&#39;s name, so &lt;code&gt;instrument-cataloged&lt;/code&gt; under &lt;code&gt;instrument&lt;/code&gt; 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 &lt;code&gt;InstrumentInstrumentCataloged&lt;/code&gt; – 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.&lt;/p&gt;&#xA;&lt;p&gt;0.15.0 closes that gap for every rule at once. The documentation gains &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io/reference/linter-rules/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Linter Rules&lt;/a&gt;&lt;/strong&gt; 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 &lt;code&gt;see&lt;/code&gt; line that links to its entry, and the JSON output carries the same address as &lt;code&gt;documentationUrl&lt;/code&gt;, so your tooling can point people there too. &lt;strong&gt;An opinionated linter owes its reasons, and now it pays them.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Where the tooling was wrong rather than unexplained, it is corrected. A field report from &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm/issues/19&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;a model of about 175 documents&lt;/a&gt;&lt;/strong&gt; described two name collisions that &lt;code&gt;esdm view&lt;/code&gt; seemed to hide, and chasing them turned up more than the report could see: &lt;code&gt;esdm view&lt;/code&gt; never rendered Entities or free-standing Events at all, and &lt;code&gt;esdm view &amp;lt;path&amp;gt;&lt;/code&gt; 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.&lt;/p&gt;&#xA;&lt;p&gt;Three smaller corrections came the same way. &lt;code&gt;event-without-publisher&lt;/code&gt; no longer fires when a Dynamic Consistency Boundary publishes an Aggregate&#39;s Event, because publisher resolution now lives in one place rather than three. Field references such as &lt;code&gt;identifiedBy.field&lt;/code&gt; &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm/issues/52&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;accept &lt;code&gt;userId&lt;/code&gt; as well as &lt;code&gt;user-id&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;. And the update hint &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm/issues/23&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;stays quiet&lt;/a&gt;&lt;/strong&gt; when &lt;code&gt;esdm completion&lt;/code&gt; runs from a shell profile, where it had greeted one user on every new terminal.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;If you run ESDM today, update to 0.15.0&lt;/strong&gt;, run &lt;code&gt;esdm lint&lt;/code&gt;, and then try &lt;code&gt;esdm documentation --output docs&lt;/code&gt; on your own model. The &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io/getting-started/updating-esdm/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;update guide&lt;/a&gt;&lt;/strong&gt; has the steps, and the next section has what to do if the linter comes back red.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-breaks-and-what-to-do-about-it&#34;&gt;What Breaks, and What to Do About It&lt;a class=&#34;anchor&#34; href=&#34;#what-breaks-and-what-to-do-about-it&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;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.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;A Bounded Context that declares a &lt;code&gt;ubiquitousLanguage&lt;/code&gt; must now declare its &lt;code&gt;language&lt;/code&gt;.&lt;/strong&gt; The linter reports &lt;code&gt;missing required field &amp;quot;language&amp;quot;&lt;/code&gt; on the context. Add the language the terms are written in as a tag, &lt;code&gt;language: en&lt;/code&gt; or &lt;code&gt;language: de&lt;/code&gt; – BCP 47, the same tags the web uses – and the finding is gone. A context without terminology is unaffected.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;A Given-When-Then rejection must name an invariant.&lt;/strong&gt; The free-form &lt;code&gt;then.rejection.reason&lt;/code&gt; is gone from the schema; every rejection is &lt;code&gt;{ invariant: &amp;lt;name&amp;gt; }&lt;/code&gt;, so its cause is a rule the unit actually declares. A Scenario that used &lt;code&gt;reason&lt;/code&gt; now fails validation, with &lt;code&gt;unknown field &amp;quot;reason&amp;quot;&lt;/code&gt; 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, &lt;code&gt;uncovered-invariant&lt;/code&gt;, points the other way: on an Aggregate or Dynamic Consistency Boundary that has Features, an invariant no Scenario exercises is reported as untested.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;No two of the five domain types may share a name in one Bounded Context.&lt;/strong&gt; 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 &lt;code&gt;loan&lt;/code&gt; would collide there, and are now an error, &lt;code&gt;esdm/structure/ambiguous-name&lt;/code&gt;, 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 &lt;code&gt;order&lt;/code&gt; are one concept on the write and the read side – and the &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io/concepts/bounded-context/#one-namespace-for-the-domain-types&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Bounded Context&lt;/a&gt;&lt;/strong&gt; page draws the line.&lt;/p&gt;&#xA;&lt;p&gt;One step touches no model but belongs here. &lt;strong&gt;If your project has a &lt;code&gt;schemas/&lt;/code&gt; directory&lt;/strong&gt; from &lt;code&gt;esdm add-schema&lt;/code&gt;, the linter stops with &lt;code&gt;schemas-directory-drift&lt;/code&gt; until you run &lt;code&gt;esdm update-schema&lt;/code&gt;, 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.&lt;/p&gt;&#xA;&lt;h2 id=&#34;still-0x&#34;&gt;Still 0.x&lt;a class=&#34;anchor&#34; href=&#34;#still-0x&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;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 &lt;strong&gt;&lt;a href=&#34;/resources/blog/one-year-of-eventsourcingdb-and-the-language-that-belongs-beside-it&#34;&gt;May&lt;/a&gt;&lt;/strong&gt; that ESDM&#39;s core language is stable, and in &lt;strong&gt;&lt;a href=&#34;/resources/blog/esdm-is-now-open-source&#34;&gt;July&lt;/a&gt;&lt;/strong&gt; 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 &lt;code&gt;apiVersion&lt;/code&gt;, and documents written against the old one keep validating. That is the mechanism the &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io/introduction/design-principles/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;design principles&lt;/a&gt;&lt;/strong&gt; describe, and 1.0 is where it takes over.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;We expect 0.15.0 to be the last release before 1.0.&lt;/strong&gt; 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 &lt;code&gt;apiVersion&lt;/code&gt;. If a shape in this release is wrong – if &lt;code&gt;translations&lt;/code&gt; 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.&lt;/p&gt;&#xA;&lt;p&gt;Feedback does not mean every request lands, and it should not. We said no to &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm/issues/22&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;distributing ESDM through package managers&lt;/a&gt;&lt;/strong&gt;, 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 &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm/issues/24&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;an OOUX extension&lt;/a&gt;&lt;/strong&gt; – 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 &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm/issues/9&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;citation fields in the schema&lt;/a&gt;&lt;/strong&gt;, because where a modeler&#39;s knowledge comes from is information about the model rather than about the domain.&lt;/p&gt;&#xA;&lt;p&gt;Each answer is in the issue with its reasons, and each can be reopened by a case we have not seen. &lt;strong&gt;A no with a reason is still an answer, and every request gets one.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;say-it-before-10&#34;&gt;Say It Before 1.0&lt;a class=&#34;anchor&#34; href=&#34;#say-it-before-10&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;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. &lt;strong&gt;The next few weeks decide what 1.0 looks like&lt;/strong&gt;, and that works the same way.&lt;/p&gt;&#xA;&lt;p&gt;If you use ESDM, update and tell us what you find – &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm/issues&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;open an issue on GitHub&lt;/a&gt;&lt;/strong&gt; or write to &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt;, including what worked better than you expected. If you have not tried ESDM yet, &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io/getting-started/your-first-model/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Your First Model&lt;/a&gt;&lt;/strong&gt; 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.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/there-is-no-row-to-lock</id>
    <title>There Is No Row to Lock</title>
    <link href="https://www.eventfoundation.io/resources/blog/there-is-no-row-to-lock"></link>
    <published>2026-08-27T00:00:00+02:00</published>
    <updated>2026-08-27T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">Optimistic locking is one of those techniques you learn once and then use everywhere. A version column, an UPDATE ... WHERE version = 7, and whoever arrives second finds zero rows affected and has to try again. It is cheap, it needs no coordination, and it has kept relational systems correct for decades.</summary>
    <content type="html">&lt;p&gt;Optimistic locking is one of those techniques you learn once and then use everywhere. A version column, an &lt;code&gt;UPDATE ... WHERE version = 7&lt;/code&gt;, and whoever arrives second finds zero rows affected and has to try again. It is cheap, it needs no coordination, and it has kept relational systems correct for decades.&lt;/p&gt;&#xA;&lt;p&gt;Then you move to Event Sourcing, and the technique has nothing to attach itself to. &lt;strong&gt;There is no row that represents the current state, so there is no row to lock.&lt;/strong&gt; What you would need instead is a condition over a set of records that a query describes – and that turns out to be the one thing a relational database has no single statement for.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;how-it-works-with-rows&#34;&gt;How It Works With Rows&lt;a class=&#34;anchor&#34; href=&#34;#how-it-works-with-rows&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Start with the mechanism that does work, because it is the one worth measuring against. You keep a version alongside your data, you read it, you compute your change, and you write it back with the version you saw as part of the condition. If somebody else got there first, the version has moved and your update matches nothing.&lt;/p&gt;&#xA;&lt;p&gt;The event-sourced variant of that same idea is a unique constraint on stream name and version. Two writers who both believe they are appending version 7 cannot both succeed: one commits, the other collides with the constraint, and you translate that collision into a conflict your domain understands. As &lt;strong&gt;&lt;a href=&#34;/resources/blog/postgresql-is-not-an-event-store&#34;&gt;PostgreSQL Is Not an Event Store&lt;/a&gt;&lt;/strong&gt; puts it, &lt;strong&gt;plenty of production systems run on exactly this&lt;/strong&gt;, and they are not wrong to.&lt;/p&gt;&#xA;&lt;p&gt;What makes it work is that the condition and the data live in the same place: the version sits in the row you are writing, so checking it costs nothing extra and cannot be skipped by accident. &lt;strong&gt;The technique is not clever, it is well-placed.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;there-is-nothing-to-lock&#34;&gt;There Is Nothing to Lock&lt;a class=&#34;anchor&#34; href=&#34;#there-is-nothing-to-lock&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now take a decision from an event-sourced system. May this book be lent out? That does not depend on one row. It depends on every event recorded for that book: acquired, borrowed, returned, withdrawn. Replay them in order and you have your answer.&lt;/p&gt;&#xA;&lt;p&gt;But the answer is not what you guard. &lt;strong&gt;What you guard is the reasoning&lt;/strong&gt; – that the book was acquired and never withdrawn, and that nobody has borrowed it since you looked. A version number on the state you derived would tell you none of that, because the state you derived is not stored anywhere.&lt;/p&gt;&#xA;&lt;p&gt;You could put the version on the stream instead and be done with it, and for many decisions that is exactly right. But the interesting decisions do not respect stream boundaries. May this reader borrow another book depends on events across every book they currently hold. Whether this email address is still free depends on every registration ever recorded. &lt;strong&gt;The set of events your decision rests on is described by a query, not by an identifier.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;And that set does not stand still. Between the moment you read it and the moment you write, other writers are appending – and what they append is precisely what might invalidate the decision you just made. &lt;strong&gt;What you need to guard is not a row against modification, but a query against having gained new answers.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-condition-you-actually-want&#34;&gt;The Condition You Actually Want&lt;a class=&#34;anchor&#34; href=&#34;#the-condition-you-actually-want&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Translate that back into relational terms and the shape of the problem becomes clear. What you would like to say is: write this row, but only if the set of rows that this &lt;code&gt;SELECT&lt;/code&gt; returns has not changed since I last ran it.&lt;/p&gt;&#xA;&lt;p&gt;Say it out loud and it sounds reasonable. Try to implement it and you find three approaches. One gets you there and does not fit in a single statement; one gets you there only on some engines, and only at a price; the third fits in a single statement and does not get you there on its own.&lt;/p&gt;&#xA;&lt;p&gt;You can raise the isolation level. In PostgreSQL, &lt;code&gt;SERIALIZABLE&lt;/code&gt; detects the conflict for you and then hands you the consequence: a serialization failure is not a domain error, it is a transaction the database refuses to commit, so every path that touches the set needs a retry loop – and that loop has to be safe to run twice, because the first attempt may have already sent an email or charged a card. Some engines reach &lt;code&gt;SERIALIZABLE&lt;/code&gt; by locking rather than by detecting, and there you get blocking on top of the retries, because a deadlock victim has to be tried again as well.&lt;/p&gt;&#xA;&lt;p&gt;You can take the locks yourself with &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;. That locks the rows your query returned, which is the smaller half of the problem: &lt;strong&gt;in PostgreSQL it cannot lock rows that do not exist yet.&lt;/strong&gt; The registration that arrives while you are deciding is not among the rows you locked, and it is exactly the one that breaks your decision. Some engines do reach it: InnoDB takes gap locks at &lt;code&gt;REPEATABLE READ&lt;/code&gt; and above, given an index it can use, at the price of locking stretches of a key range nobody has written to yet. &lt;strong&gt;Either way you are locking rows, or the space where rows would go, to guard a question about a set.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Or you can fold the check into the statement, as &lt;code&gt;INSERT ... SELECT ... WHERE NOT EXISTS (...)&lt;/code&gt;. Under &lt;code&gt;READ COMMITTED&lt;/code&gt; or &lt;code&gt;REPEATABLE READ&lt;/code&gt; the subquery reads a snapshot, so two concurrent statements can both find nothing and both insert. It gives you the guarantee only with a unique index behind it to catch the case it lets through – which means the real work is being done by the constraint, and you are back to conditions a constraint can express.&lt;/p&gt;&#xA;&lt;p&gt;None of this is impossible, and all of it is yours to build and keep correct. &lt;strong&gt;The version column worked because it sat where the data sat; none of these three does.&lt;/strong&gt; In every one of them, the condition lives in the writing code rather than in the write. A new service, a migration script, a well-meant background job – each has to reconstruct the same guard, and nothing checks that it did.&lt;/p&gt;&#xA;&lt;h2 id=&#34;sending-the-condition-along&#34;&gt;Sending the Condition Along&lt;a class=&#34;anchor&#34; href=&#34;#sending-the-condition-along&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;A store built for Event Sourcing can put the condition somewhere else: into the write request itself.&lt;/p&gt;&#xA;&lt;p&gt;A &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;/api/v1/write-events&lt;/code&gt; carries an &lt;code&gt;events&lt;/code&gt; array – &lt;strong&gt;&lt;a href=&#34;/resources/blog/not-an-event-yet&#34;&gt;the candidates you want stored&lt;/a&gt;&lt;/strong&gt; – and, optionally, a &lt;code&gt;preconditions&lt;/code&gt; array beside it, holding any of four condition types.&lt;/p&gt;&#xA;&lt;p&gt;Here is a write that appends one event to &lt;code&gt;/books/42&lt;/code&gt;. That path is the &lt;strong&gt;subject&lt;/strong&gt;, which is what this post has been calling a stream: the identifier that says which thing the event is about and gathers that thing&#39;s events. The condition says that the subject&#39;s most recent event must still be the one we read:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;events&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;source&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://library.eventsourcingdb.io&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;subject&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;/books/42&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;io.eventsourcingdb.library.book-borrowed&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nt&#34;&gt;&amp;#34;borrowedBy&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;/readers/23&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;preconditions&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;isSubjectOnEventId&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nt&#34;&gt;&amp;#34;payload&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nt&#34;&gt;&amp;#34;subject&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;/books/42&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nt&#34;&gt;&amp;#34;eventId&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;23&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;The preconditions are evaluated in the same transaction that appends the events.&lt;/strong&gt; If they all hold, everything is written. If a single one fails, nothing is written at all and the request comes back as &lt;code&gt;409 Conflict&lt;/code&gt;. There is no window between the check and the append, because there is no check separate from the append.&lt;/p&gt;&#xA;&lt;p&gt;Nothing limits you to one condition. A single request can carry several preconditions over several subjects, alongside events for several subjects, and the whole thing lands or does not land as one unit – borrowing a book while recording it against the reader&#39;s account, with a condition on each. &lt;strong&gt;&lt;a href=&#34;/resources/blog/ddd-back-to-basics&#34;&gt;DDD, Back to Basics&lt;/a&gt;&lt;/strong&gt; works that all-or-nothing case through.&lt;/p&gt;&#xA;&lt;p&gt;That is the difference worth naming, and &lt;strong&gt;&lt;a href=&#34;/resources/blog/postgresql-is-not-an-event-store&#34;&gt;PostgreSQL Is Not an Event Store&lt;/a&gt;&lt;/strong&gt; named it in one sentence: &amp;quot;The difference from a purpose-built store is not who has to remember – both make every writing path declare its expectation – but what you can declare.&amp;quot; A unique constraint can only check the number it is handed. A precondition can describe the state of the store. The &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/preconditions&#34;&gt;preconditions documentation&lt;/a&gt;&lt;/strong&gt; has all four types with examples: because the expectation travels inside the request, the store is what enforces it, so a second writer cannot slip between your read and your write no matter which service they came from.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-everyday-case&#34;&gt;The Everyday Case&lt;a class=&#34;anchor&#34; href=&#34;#the-everyday-case&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Most of the time you do not need anything elaborate. You read a subject, you remember the ID of the last event you saw, you make your decision, and you write with &lt;code&gt;isSubjectOnEventId&lt;/code&gt; naming that ID. Event IDs are assigned by the store as one gap-free sequence across everything it holds, which means the IDs inside a single subject are not consecutive – so you name the ID you actually saw rather than computing the one you expect to come next. Checking it means looking at the end of one subject, which the store can do directly. &lt;strong&gt;&lt;a href=&#34;/resources/blog/start-with-one-table&#34;&gt;Start With One Table&lt;/a&gt;&lt;/strong&gt; shows the code that picks it. &lt;strong&gt;It is the version column, except the version is an event ID and the condition travels in the request.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Does the guarantee hold under contention? We put it to the test: twenty-five writes at once against the same fresh subject, every one of them carrying a precondition that the subject must still be empty. &lt;strong&gt;One write succeeded, twenty-four came back with &lt;code&gt;409&lt;/code&gt;, and exactly one event was in the subject afterwards.&lt;/strong&gt; Three rounds, three different winners – which is the point, because nothing about the order is arranged and the guarantee does not depend on it.&lt;/p&gt;&#xA;&lt;p&gt;What the store does not do is make the conflict disappear. Those twenty-four writers each have to notice the &lt;code&gt;409&lt;/code&gt;, read the subject again, redo the decision against what is now there, and send a new request – the same loop that &lt;code&gt;SERIALIZABLE&lt;/code&gt; would have imposed. The difference is what the loop is told: a &lt;code&gt;409&lt;/code&gt; from a precondition is a real conflict about your data, whereas a serialization failure can be a false positive that the database could not rule out.&lt;/p&gt;&#xA;&lt;p&gt;Two details are worth knowing before you need them. The &lt;code&gt;409&lt;/code&gt; body is a short text, &lt;code&gt;state conflict: precondition failed&lt;/code&gt;, and it does not say which precondition failed – fine with one, something to plan for with five. And the read still happens, the decision is still yours, and the retry is still your code. &lt;strong&gt;What moves into the store is the check, not the workflow around it.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;two-shortcuts-for-the-ends-of-a-subject&#34;&gt;Two Shortcuts for the Ends of a Subject&lt;a class=&#34;anchor&#34; href=&#34;#two-shortcuts-for-the-ends-of-a-subject&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Two more of the four types cover the situations where you have no last event ID, because you are at one end of a subject&#39;s life.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;isSubjectPristine&lt;/code&gt; requires the subject to hold no events at all, which is what you want when you create something: acquiring a book, registering a reader. &lt;code&gt;isSubjectPopulated&lt;/code&gt; is its mirror image and requires at least one event, which is what you want when you modify something and want to be sure it was ever created. &lt;strong&gt;Both express an intent that an event ID cannot:&lt;/strong&gt; not &amp;quot;the state I read is still current&amp;quot; but &amp;quot;this thing does not exist yet&amp;quot; and &amp;quot;this thing exists&amp;quot;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;when-the-set-is-genuinely-arbitrary&#34;&gt;When the Set Is Genuinely Arbitrary&lt;a class=&#34;anchor&#34; href=&#34;#when-the-set-is-genuinely-arbitrary&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Which leaves the case we started with: a decision resting on a set that no single subject describes. For that there is &lt;code&gt;isEventQlQueryTrue&lt;/code&gt;, the fourth type, which takes an &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/eventql&#34;&gt;EventQL&lt;/a&gt;&lt;/strong&gt; query and requires it to evaluate to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;That is exactly the condition from &lt;em&gt;The Condition You Actually Want&lt;/em&gt;, now expressible in one request. It is also the mechanism behind Dynamic Consistency Boundaries, as the interview &lt;strong&gt;&lt;a href=&#34;/resources/blog/kill-aggregate-an-interview-on-dynamic-consistency-boundaries&#34;&gt;Kill Aggregate&lt;/a&gt;&lt;/strong&gt; describes: append this event, but only if no other event matching a given query has appeared since a given position. You can express uniqueness with it, or a limit across several subjects, or any invariant you can phrase as a query. &lt;strong&gt;It is the most flexible of the four, and the only one whose cost you have to think about.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;The reason is in the shape of the question. The three cheap types ask about one place – the start of a subject, its end – and the store can look there directly. A query that counts has to consider every event that might match, and there is no index over event payloads for it to use, so the work grows with the number of events stored. We measured it against version 1.2.0 in Docker, taking the median of seven single writes: at twenty-four thousand events, a write carrying such a precondition costs on the order of a hundred times a write carrying none, while all three cheap types stayed within the noise of a write with no precondition at all. Every measurement sits below &lt;strong&gt;&lt;a href=&#34;/products/eventsourcingdb/licensing&#34;&gt;the free tier&#39;s ceiling of twenty-five thousand events&lt;/a&gt;&lt;/strong&gt;, so the figure is what we could observe rather than a curve out to a million – but the mechanism says plainly which way it goes.&lt;/p&gt;&#xA;&lt;p&gt;And that write does not pay alone. While such a precondition is evaluated, other writes wait: we measured a plain write to an unrelated subject taking around half a second instead of six milliseconds. &lt;strong&gt;An expensive precondition is not a slow request, it is a slow store.&lt;/strong&gt; This is the other half of the bill, and it is the part a comparison against &lt;code&gt;SERIALIZABLE&lt;/code&gt; usually leaves out: in this engine every write takes the same store-wide lock anyway, so what an expensive precondition changes is not whether the others wait, but how long.&lt;/p&gt;&#xA;&lt;p&gt;One consequence is worth spelling out, because it is easy to get backwards. &lt;strong&gt;Restricting the query to a single subject does not make it cheaper.&lt;/strong&gt; A &lt;code&gt;WHERE&lt;/code&gt; clause on the subject is another predicate in the same scan, not a way to narrow it. So if your condition concerns one subject, express it with &lt;code&gt;isSubjectOnEventId&lt;/code&gt; and not as a query – the same guarantee, none of the cost. And when the invariant really is global, &lt;strong&gt;&lt;a href=&#34;/resources/blog/email-uniqueness-in-event-sourcing&#34;&gt;Email Uniqueness in Event Sourcing&lt;/a&gt;&lt;/strong&gt; works through why a query precondition is usually the wrong answer there, and what to reach for instead.&lt;/p&gt;&#xA;&lt;h2 id=&#34;from-a-row-to-a-question&#34;&gt;From a Row to a Question&lt;a class=&#34;anchor&#34; href=&#34;#from-a-row-to-a-question&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Optimistic locking on a row works because the row is the state. In an event-sourced system the state is a question you ask of the recorded events, and what can invalidate your decision is a new answer to that question. So the guard has to be the question, not a row.&lt;/p&gt;&#xA;&lt;p&gt;That is what a precondition is: your expectation, written down, sent along, and checked where the append happens rather than in the code that calls it. The four types are not four features. &lt;strong&gt;They are four points on a scale from cheap and specific to expensive and general&lt;/strong&gt; – and the guarantee they enforce is the same, while what they can express, and what it costs, is not.&lt;/p&gt;&#xA;&lt;p&gt;If you have been carrying that guard around in your application code, it is worth seeing what changes when the store takes the check over: the read stays, the retry stays, and the race disappears. And if you want the reasoning behind all of this rather than the mechanics, &lt;strong&gt;&lt;a href=&#34;/resources/blog/thinking-in-events&#34;&gt;Thinking in Events&lt;/a&gt;&lt;/strong&gt; makes the case that a write should land only if the past still looks the way your decision assumed.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/not-an-event-yet</id>
    <title>Not an Event Yet</title>
    <link href="https://www.eventfoundation.io/resources/blog/not-an-event-yet"></link>
    <published>2026-08-24T00:00:00+02:00</published>
    <updated>2026-08-24T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">Every event stored in EventSourcingDB is a CloudEvent. What your application sends in order to write one is not. It is missing seven of the eleven fields a stored event carries, and if you try to supply them yourself, the database rejects the whole request.</summary>
    <content type="html">&lt;p&gt;Every event stored in EventSourcingDB is a CloudEvent. &lt;strong&gt;What your application sends in order to write one is not.&lt;/strong&gt; It is missing seven of the eleven fields a stored event carries, and if you try to supply them yourself, the database rejects the whole request.&lt;/p&gt;&#xA;&lt;p&gt;That gap has a name. Internally we call it an &lt;strong&gt;event candidate&lt;/strong&gt; – not an event yet, more like an application to become one. From the outside this looks like pedantry. It is the reason everything else can be trusted. So let&#39;s look at what a candidate brings, what the server adds, and why we closed nearly every freedom the CloudEvents standard was generous enough to leave open.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;a-format-everyone-can-agree-on&#34;&gt;A Format Everyone Can Agree On&lt;a class=&#34;anchor&#34; href=&#34;#a-format-everyone-can-agree-on&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;When services exchange events, the hard part is rarely the transport. It is that every service describes an event slightly differently, so every new integration means another piece of translation code – and the number of those pieces grows with every pairing, not with every service: ten services that all talk to each other need ninety translations, not ten.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;CloudEvents exists to make that translation unnecessary.&lt;/strong&gt; It is an open specification maintained by the &lt;strong&gt;&lt;a href=&#34;https://www.cncf.io/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Cloud Native Computing Foundation&lt;/a&gt;&lt;/strong&gt;, and it does one thing: it fixes the structure in which an event describes itself. What kind of event this is, where it came from, what it refers to, when it happened, and what the payload contains. Everything else – which protocol carries it, which language reads it – is left to you. Azure Event Grid and Google Eventarc both accept and emit it as a first-class format.&lt;/p&gt;&#xA;&lt;p&gt;The specification is deliberately transport-agnostic. The same event can travel over HTTP, AMQP, MQTT or Kafka, and each of those bindings only describes how the fields are mapped onto that protocol&#39;s headers and body. &lt;strong&gt;That independence is a large part of why the standard spread&lt;/strong&gt; – and, as we will see, it is also the source of the freedoms we had to take away again.&lt;/p&gt;&#xA;&lt;p&gt;When we built EventSourcingDB, we could have invented our own event format. Every database has some latitude here, and a purpose-built format would have fit our storage engine a little more snugly. &lt;strong&gt;We took the standard instead, because an event that outlives the application that wrote it needs to be readable by tools nobody has written yet.&lt;/strong&gt; A proprietary format makes that a matter of hope. An open one makes it a matter of reading the specification.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-you-send-is-not-an-event&#34;&gt;What You Send Is Not an Event&lt;a class=&#34;anchor&#34; href=&#34;#what-you-send-is-not-an-event&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now look at what actually goes over the wire when you write one. Following the &lt;strong&gt;&lt;a href=&#34;/learn/tutorials/first-steps-with-eventsourcingdb/writing-events&#34;&gt;Writing Events&lt;/a&gt;&lt;/strong&gt; guide, you assemble four fields: the &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/sources&#34;&gt;source&lt;/a&gt;&lt;/strong&gt; identifying the application that emits the event, the &lt;code&gt;subject&lt;/code&gt; naming the thing the event is about (here one particular book), the &lt;code&gt;type&lt;/code&gt; saying what happened, and the &lt;code&gt;data&lt;/code&gt; carrying the details:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;source&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://library.eventsourcingdb.io&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;subject&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;/books/42&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;io.eventsourcingdb.library.book-acquired&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nt&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;2001 – A Space Odyssey&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nt&#34;&gt;&amp;#34;author&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Arthur C. Clarke&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nt&#34;&gt;&amp;#34;isbn&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;978-0756906788&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Compare that against the CloudEvents specification and it does not qualify. There is no &lt;code&gt;specversion&lt;/code&gt;, no &lt;code&gt;id&lt;/code&gt;, no &lt;code&gt;time&lt;/code&gt;, no &lt;code&gt;datacontenttype&lt;/code&gt;. &lt;strong&gt;Judged as a CloudEvent, this thing is incomplete&lt;/strong&gt; – and it is not incomplete because we were sloppy about the standard, but because completing it is not the client&#39;s job.&lt;/p&gt;&#xA;&lt;p&gt;The SDKs carry the distinction into their type systems: in C# you construct an &lt;code&gt;EventCandidate&lt;/code&gt;, in Elixir an &lt;code&gt;%EventSourcingDB.EventCandidate{}&lt;/code&gt;. The &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/mcp-server/working-with-events&#34;&gt;MCP server&lt;/a&gt;&lt;/strong&gt;, which lets a language model read and write events for you, describes its input the same way. &lt;strong&gt;The name is doing work. It says that what you are holding is a proposal, and that something has to happen to it before it becomes a fact.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;There is one exception, and it covers two fields. You may also send &lt;code&gt;traceparent&lt;/code&gt; and &lt;code&gt;tracestate&lt;/code&gt;, which carry &lt;strong&gt;&lt;a href=&#34;https://opentelemetry.io/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;OpenTelemetry&lt;/a&gt;&lt;/strong&gt; context for distributed tracing. &lt;strong&gt;Anything beyond those six fields is refused, and the write fails as a whole rather than quietly dropping what it did not understand.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-a-candidate-cannot-promote-itself&#34;&gt;Why a Candidate Cannot Promote Itself&lt;a class=&#34;anchor&#34; href=&#34;#why-a-candidate-cannot-promote-itself&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The seven missing fields are not busywork we could have delegated to the client to save ourselves the trouble. Each one is something the client is structurally unable to know or unable to be trusted with.&lt;/p&gt;&#xA;&lt;p&gt;Take &lt;code&gt;id&lt;/code&gt;. In EventSourcingDB it is not a random identifier but a &lt;strong&gt;position in a global, gap-free sequence&lt;/strong&gt; across the entire store, starting at zero. No client can know its own position, because that depends on every other write happening at the same moment. Take &lt;code&gt;time&lt;/code&gt;. It is the moment the database learned about the event, not the moment your business process thinks something happened – which is exactly why &lt;strong&gt;&lt;a href=&#34;/resources/blog/time-is-of-the-essence&#34;&gt;Time Is of the Essence&lt;/a&gt;&lt;/strong&gt; tells you to put business timestamps into your event data instead.&lt;/p&gt;&#xA;&lt;p&gt;Then there are &lt;code&gt;hash&lt;/code&gt; and &lt;code&gt;predecessorhash&lt;/code&gt;, which chain every event in the store to its predecessor and make tampering detectable, as &lt;strong&gt;&lt;a href=&#34;/resources/blog/proving-without-revealing-merkle-trees&#34;&gt;Proving Without Revealing&lt;/a&gt;&lt;/strong&gt; works through and &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/auditing-the-event-store&#34;&gt;Auditing the Event Store&lt;/a&gt;&lt;/strong&gt; puts to use. A chain whose links the sender may bring along is not a chain. The same goes for the &lt;code&gt;signature&lt;/code&gt;, which requires the server&#39;s signing key by definition.&lt;/p&gt;&#xA;&lt;p&gt;That leaves &lt;code&gt;specversion&lt;/code&gt; and &lt;code&gt;datacontenttype&lt;/code&gt;, and those two are constants: &lt;code&gt;1.0&lt;/code&gt; and &lt;code&gt;application/json&lt;/code&gt;. Accepting them from a client would mean accepting that they could say something else, and there is no other value we would store.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;You state what happened; the server attests when it learned of it, in what order, and that nothing has changed since.&lt;/strong&gt; A candidate is an application. An event is a certificate. And whoever issues their own certificate has none.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-server-fills-in-the-rest&#34;&gt;The Server Fills In the Rest&lt;a class=&#34;anchor&#34; href=&#34;#the-server-fills-in-the-rest&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Send the candidate above to a fresh store, then read it back, and this is what comes out:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;specversion&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;1.0&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;time&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;...&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;source&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://library.eventsourcingdb.io&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;subject&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;/books/42&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;io.eventsourcingdb.library.book-acquired&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;datacontenttype&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nt&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;2001 – A Space Odyssey&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nt&#34;&gt;&amp;#34;author&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Arthur C. Clarke&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nt&#34;&gt;&amp;#34;isbn&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;978-0756906788&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;predecessorhash&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;0000000000000000000000000000000000000000000000000000000000000000&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;hash&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;...&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nt&#34;&gt;&amp;#34;signature&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;null&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Four fields went in, eleven came out. The &lt;code&gt;id&lt;/code&gt; is &lt;code&gt;&amp;quot;0&amp;quot;&lt;/code&gt; because this is the first event in the store, which is also why the predecessor hash is all zeros – there is nothing behind it yet. The &lt;code&gt;signature&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; because this instance was started without a signing key; configure one and it is filled in. Two values are left as &lt;code&gt;...&lt;/code&gt; here for the same reason the documentation leaves them out: the &lt;code&gt;time&lt;/code&gt; is an RFC 3339 timestamp with nanosecond precision taken at the moment of the write, and the &lt;code&gt;hash&lt;/code&gt; is 64 hexadecimal characters over that event, so both differ on every run.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;And this, finally, is a CloudEvent in the full sense of the specification.&lt;/strong&gt; Whatever you read out of EventSourcingDB, and every event that reaches you on an &lt;strong&gt;&lt;a href=&#34;/learn/tutorials/first-steps-with-eventsourcingdb/observing-events&#34;&gt;observe connection&lt;/a&gt;&lt;/strong&gt; as it is written, carries the complete structure. The candidate has become an event, and from here on it is interoperable with everything that speaks the standard. If you want the field-by-field account, the &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/cloud-events&#34;&gt;CloudEvents documentation&lt;/a&gt;&lt;/strong&gt; has it: because every event carries the same standardized shape, your tooling can read our events without a converter in between, which means an event written today is still legible to a system built years from now.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-freedoms-we-took-away&#34;&gt;The Freedoms We Took Away&lt;a class=&#34;anchor&#34; href=&#34;#the-freedoms-we-took-away&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Here is the part that surprises people who know the specification well. CloudEvents is deliberately permissive in places, and &lt;strong&gt;we took away nearly every one of those choices.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;The standard prescribes no format for &lt;code&gt;id&lt;/code&gt; beyond it being a string; we make it a gap-free sequence number. It allows any payload format through &lt;code&gt;datacontenttype&lt;/code&gt;; we fix it to &lt;code&gt;application/json&lt;/code&gt;. It lets &lt;code&gt;data&lt;/code&gt; be whatever the content type permits; we require a JSON object at the top level and reject &lt;code&gt;null&lt;/code&gt;, arrays and scalars – an empty object is fine, for events whose type alone carries the meaning. Reverse domain notation for &lt;code&gt;type&lt;/code&gt; is a convention in the specification; for us it is a rule, so &lt;code&gt;io.eventsourcingdb.library.book-acquired&lt;/code&gt; is accepted and &lt;code&gt;book acquired&lt;/code&gt; is not.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/subjects&#34;&gt;subject&lt;/a&gt;&lt;/strong&gt; is the sharpest case. CloudEvents lists it as optional and treats it as free-form; we make it mandatory, because it identifies the event stream the event belongs to, and we require a path that starts with a slash and whose segments hold nothing but letters, digits, underscores and hyphens. A subject with a dot or a space in it is rejected. And where the standard says &lt;code&gt;time&lt;/code&gt; should be the moment the occurrence happened, falling back to another time only when that cannot be determined, we always record the moment the write reached us.&lt;/p&gt;&#xA;&lt;p&gt;Each of these looks like a restriction and works like a guarantee. &lt;strong&gt;A format built for messages in transit can afford wiggle room, because a message is read once, soon, by a recipient who was written against the same assumptions.&lt;/strong&gt; An event store keeps facts for decades and hands them to readers nobody has met yet. Every choice we had left open would eventually have been answered differently by two teams in the same company, and the store would keep both answers forever.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-question-we-get-asked-most-often&#34;&gt;The Question We Get Asked Most Often&lt;a class=&#34;anchor&#34; href=&#34;#the-question-we-get-asked-most-often&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The question runs like this: could there be a &lt;code&gt;metadata&lt;/code&gt; field at the top level, so that application frameworks have somewhere to put correlation IDs, causation IDs and their own bookkeeping? The standard even seems to invite it, since it allows custom extension attributes.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The answer is no, and the reason is the namespace.&lt;/strong&gt; The specification requires every attribute name to consist of lower-case letters and digits, nothing else, and asks that names not exceed twenty characters. There is no prefix, no vendor segment, no dot to hide behind – every extension attribute in the world shares one flat space, and the specification compares them to custom HTTP headers. Whoever puts a name up there is betting that nothing else will ever claim it, and &lt;code&gt;metadata&lt;/code&gt; is the most generic name anyone could pick for that bet.&lt;/p&gt;&#xA;&lt;p&gt;You lose such a bet quietly, because a collision does not announce itself when you make it. It surfaces years later, in events that are immutable by then, so every one already written keeps the attribute, and the only way out is a migration over the entire store.&lt;/p&gt;&#xA;&lt;p&gt;And a migration over an event store is not a schema change. Because the events are chained by hash, rewriting even one of them means recomputing every hash after it and signing all of them again – and any hash somebody recorded elsewhere as evidence, in an audit report or a counterparty&#39;s system, stops matching. &lt;strong&gt;The cheapest moment to avoid that is before the first event is written&lt;/strong&gt;, which is the moment we are talking about here.&lt;/p&gt;&#xA;&lt;p&gt;We are aware that this cuts both ways, because &lt;strong&gt;we use three extension attributes ourselves&lt;/strong&gt;: &lt;code&gt;predecessorhash&lt;/code&gt;, &lt;code&gt;hash&lt;/code&gt; and &lt;code&gt;signature&lt;/code&gt;. The difference is not that our names are better. It is that there was nowhere else to put them: a proof that an event has not been altered has to sit on the event itself, outside the payload it protects, and none of the three says anything about your business. &lt;strong&gt;For metadata there is somewhere else, and it is the field you already own.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;For everything else, the argument is simpler than it looks. &lt;strong&gt;If a framework wants to store something, it has a domain reason for wanting it&lt;/strong&gt; – and anything with a domain reason belongs in &lt;code&gt;data&lt;/code&gt;, where you own the namespace and can nest a &lt;code&gt;metadata&lt;/code&gt; property beneath it without colliding with anyone. Versioning ends up in the same place for a related reason: CloudEvents has no version attribute at all, so the version travels inside the &lt;code&gt;type&lt;/code&gt;, as &lt;strong&gt;&lt;a href=&#34;/resources/blog/versioning-events-without-breaking-everything&#34;&gt;Versioning Events Without Breaking Everything&lt;/a&gt;&lt;/strong&gt; works through.&lt;/p&gt;&#xA;&lt;h2 id=&#34;from-candidate-to-fact&#34;&gt;From Candidate to Fact&lt;a class=&#34;anchor&#34; href=&#34;#from-candidate-to-fact&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;So what you send is not an event, and what you get back is. The distance between the two is seven fields, and every one of them is there because you cannot credibly supply it about yourself.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The strictness is not bureaucracy. It is the reason the result means anything.&lt;/strong&gt; A store where clients set their own ordering, their own timestamps and their own hashes would hold the same data and prove nothing about it. The four fields you fill in are exactly the ones where you are the authority – what happened, to what, from where, with which details. The rest is the store&#39;s testimony, and testimony you write yourself is worth what it cost you.&lt;/p&gt;&#xA;&lt;p&gt;If you want to see the transformation for yourself, write one event and read it back: &lt;strong&gt;&lt;a href=&#34;/learn/tutorials/first-steps-with-eventsourcingdb/writing-events&#34;&gt;Writing Events&lt;/a&gt;&lt;/strong&gt; covers the first half, &lt;strong&gt;&lt;a href=&#34;/learn/tutorials/first-steps-with-eventsourcingdb/reading-events&#34;&gt;Reading Events&lt;/a&gt;&lt;/strong&gt; the second, and all eleven fields are there at the end. And if you have ever wanted that top-level &lt;code&gt;metadata&lt;/code&gt; field, we would like to hear what you were trying to put there: &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt;. What people reach for says more about a format than any specification does.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/start-with-one-table</id>
    <title>Start With One Table</title>
    <link href="https://www.eventfoundation.io/resources/blog/start-with-one-table"></link>
    <published>2026-08-20T00:00:00+02:00</published>
    <updated>2026-08-20T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <author>
      <name>Alexander Kampf</name>
    </author>
    <summary type="text">You have been circling Event Sourcing for a while now: long enough to be convinced by the idea, not long enough to have built anything with it. Today you finally stopped reading and picked one table to start with, the one holding who pays you for which plan. Which is when a colleague asks you why. The table works. The reports run. Nobody has complained. And when you are done, it holds the same values it held that morning, so from the outside, you spent an afternoon achieving nothing.</summary>
    <content type="html">&lt;p&gt;You have been circling Event Sourcing for a while now: long enough to be convinced by the idea, not long enough to have built anything with it. Today you finally stopped reading and picked one table to start with, the one holding who pays you for which plan. Which is when a colleague asks you why. The table works. The reports run. Nobody has complained. And when you are done, it holds the same values it held that morning, so from the outside, you spent an afternoon achieving nothing.&lt;/p&gt;&#xA;&lt;p&gt;Eighteen months later, your product owner turns up with a question nobody could have answered before, and you have the answer in twenty minutes, because of what you did that afternoon. This post is both halves of that: first the work that looks like wasted time, in full, with the code, and then what it was for. Everything in it is invented, from the company to the customers to the numbers, but the code is real, and it ran exactly as shown.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;the-table-that-works-fine&#34;&gt;The Table That Works Fine&lt;a class=&#34;anchor&#34; href=&#34;#the-table-that-works-fine&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Here is the table, in SQL Server. These are customer subscriptions in the billing sense, not subscriptions to a stream of events. One row per paying customer, holding the plan they are on and what it costs them. Nothing in it is unusual, and if your own looks roughly like this, that is the point. Two dozen subscriptions sit behind the output further down, sixteen of which existed on the day of the switch; the rest started in the eighteen months after. They are made up as well, and generating them is not part of this post, because what matters is the shape of what comes out rather than the rows that went in.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dbo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;subscriptions&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;NVARCHAR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;64&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;PRIMARY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;KEY&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;customer_id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;NVARCHAR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;64&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tier&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;NVARCHAR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cadence&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;     &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;NVARCHAR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;amount&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;INT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;status&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;NVARCHAR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;started_on&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;DATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;         &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;canceled_on&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;DATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;         &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Eight columns, and every one of them earns its place. You can look up a subscription by its ID in a millisecond, count the active ones, sum the monthly revenue, list everyone who left last quarter. For the questions this table was built to answer, there is nothing better.&lt;/p&gt;&#xA;&lt;p&gt;Now take any two customers who are both on &lt;code&gt;pro&lt;/code&gt;. One of them worked their way up from &lt;code&gt;basic&lt;/code&gt;. The other came down from &lt;code&gt;enterprise&lt;/code&gt;. &lt;strong&gt;Both rows say &lt;code&gt;tier = &#39;pro&#39;&lt;/code&gt;, and nothing in the table tells them apart&lt;/strong&gt; – because when the plan changed, the application ran an &lt;code&gt;UPDATE&lt;/code&gt;, and the previous value stopped existing at that moment.&lt;/p&gt;&#xA;&lt;p&gt;You could add a column. A &lt;code&gt;previous_tier&lt;/code&gt;, maybe a &lt;code&gt;tier_changed_on&lt;/code&gt;. That answers the next question and only the next one, and it answers it from the day you deploy it: for everything before, the column is &lt;code&gt;NULL&lt;/code&gt;. A year later somebody asks whether customers who switch from yearly to monthly billing leave more often, and you are back where you started, adding another column that begins empty.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The table is not wrong. It is just the only thing you have&lt;/strong&gt;, and it holds exactly one version of the truth: the current one.&lt;/p&gt;&#xA;&lt;h2 id=&#34;before-you-touch-anything&#34;&gt;Before You Touch Anything&lt;a class=&#34;anchor&#34; href=&#34;#before-you-touch-anything&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The usual objection arrives here, and it is fair: Event Sourcing sounds like a decision about the whole system, and nobody rewrites a working system on a hunch. We have described &lt;strong&gt;&lt;a href=&#34;/resources/blog/four-ways-to-migrate-to-event-sourcing&#34;&gt;four ways to migrate&lt;/a&gt;&lt;/strong&gt;, and they range from a big bang to taking one bounded context at a time. This is smaller than all of them, and it is not a migration at all.&lt;/p&gt;&#xA;&lt;p&gt;The plan: the &lt;code&gt;subscriptions&lt;/code&gt; table keeps its schema and its place. The write path in front of it starts recording what happens as events, and a projection builds the table back out of those events. Everything that reads the table carries on, unaware. Nothing is cut over, and the events become a second source that can answer things the table cannot.&lt;/p&gt;&#xA;&lt;p&gt;One assumption is doing work there, so name it now: the writes have to go through one place. If four different jobs write into that table directly, all four have to move at once, and the afternoon stops being an afternoon. When that is the situation, it is not an argument against the idea – &lt;strong&gt;it is the sign that this is the wrong table to start with.&lt;/strong&gt; Pick one whose writes already run through a single service.&lt;/p&gt;&#xA;&lt;p&gt;Three things need to be clear before any code, because all three are easy to get backwards.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The events do not go into your relational database.&lt;/strong&gt; The previous post made that case at length: &lt;strong&gt;&lt;a href=&#34;/resources/blog/postgresql-is-not-an-event-store&#34;&gt;PostgreSQL is not an event store&lt;/a&gt;&lt;/strong&gt; – not because the table does not work, but because append-only storage, gap-free ordering and being notified of new events as they arrive are guarantees you would have to build and then own yourself. Events go into an event store. The &lt;code&gt;subscriptions&lt;/code&gt; table becomes a read model, and that is a job a relational database is genuinely good at: as that same post puts it, looking up current state by a known key is its home ground.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Rebuilding the table is not the goal, it is the proof.&lt;/strong&gt; We have argued that &lt;strong&gt;&lt;a href=&#34;/resources/blog/your-aggregate-is-not-a-table&#34;&gt;your aggregate is not a table&lt;/a&gt;&lt;/strong&gt; – meaning the write side should not be shaped like a row just because the read side is. That is not what happens here. Eight columns answering one question is a perfectly good read model, and &lt;strong&gt;&lt;a href=&#34;/resources/blog/the-read-model-zoo&#34;&gt;the read model zoo&lt;/a&gt;&lt;/strong&gt; says as much: for lookups by a known key, the SQL table earns its place. Reproducing it exactly is how you show that nothing was lost.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;And small scope is not the same as diluted rules.&lt;/strong&gt; &amp;quot;Start small&amp;quot; gets misread as &amp;quot;take the parts you like&amp;quot;. Everything below holds without exception: events are immutable, they are the source of truth for the write path, and the read model is derived, never edited. What shrinks is how much of your system takes part – not which rules apply to the part that does.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-actually-happens-to-a-subscription&#34;&gt;What Actually Happens to a Subscription&lt;a class=&#34;anchor&#34; href=&#34;#what-actually-happens-to-a-subscription&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Start by not looking at the columns. Ask what people say happens to a subscription, in the words they use in a meeting: somebody signs up, somebody moves to a bigger plan, somebody moves back down, somebody switches from monthly to yearly, somebody pauses over the summer, somebody leaves.&lt;/p&gt;&#xA;&lt;p&gt;That list is the event model, and it maps onto the columns only loosely. That is the useful part. &lt;strong&gt;&lt;a href=&#34;/resources/blog/naming-events-beyond-crud&#34;&gt;Naming events beyond CRUD&lt;/a&gt;&lt;/strong&gt; is the difference between a log of what the database did and a log of what the business did – and it is very concrete here, because &lt;code&gt;tier&lt;/code&gt; is one column, while moving up and moving down are two different things that happen for two different reasons.&lt;/p&gt;&#xA;&lt;p&gt;Written down, that becomes eight event types. One of them was not in the meeting: &lt;code&gt;Imported&lt;/code&gt;, which only makes sense in a moment and which the next section is about.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;EventTypes&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Imported&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;io.example.billing.subscription-imported&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Started&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;io.example.billing.subscription-started&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Upgraded&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;io.example.billing.subscription-upgraded&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Downgraded&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;io.example.billing.subscription-downgraded&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;BillingCycleChanged&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;io.example.billing.subscription-billing-cycle-changed&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Paused&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;io.example.billing.subscription-paused&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Resumed&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;io.example.billing.subscription-resumed&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Canceled&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;io.example.billing.subscription-canceled&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Each event carries what happened, including what it replaced:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;record&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SubscriptionStarted&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CustomerId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Cadence&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DateOnly&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;StartedOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;record&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SubscriptionUpgraded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PreviousTier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PreviousAmount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DateOnly&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;EffectiveOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;record&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SubscriptionDowngraded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PreviousTier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PreviousAmount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DateOnly&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;EffectiveOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;record&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SubscriptionCanceled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Reason&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DateOnly&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CanceledOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// SubscriptionBillingCycleChanged carries the previous cadence and amount in the same&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// way. SubscriptionPaused and SubscriptionResumed carry only a reason and a date -&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// there is no previous value to keep, because pausing replaces nothing.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Splitting one plan change into two types is the safer direction – fine-grained events can always be read as a coarser view later, while a single &lt;code&gt;SubscriptionTierChanged&lt;/code&gt; could never be split apart afterwards. What is genuinely irreversible is what you leave out: &lt;strong&gt;&lt;a href=&#34;/resources/blog/your-progress-bar-doesnt-belong-in-the-event-store&#34;&gt;everything you do not write down is gone for good&lt;/a&gt;&lt;/strong&gt;, and &lt;code&gt;PreviousTier&lt;/code&gt; is a field you cannot add retroactively to events already written. That is the decision being made here, and it is worth making deliberately rather than by omission.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-day-you-switch-over&#34;&gt;The Day You Switch Over&lt;a class=&#34;anchor&#34; href=&#34;#the-day-you-switch-over&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now the part most write-ups skip, and the one that decides how honest the rest of this is.&lt;/p&gt;&#xA;&lt;p&gt;Your table already has rows in it, and there are no events behind them. A subscription that started in 2024 was upgraded, downgraded and paused long before you started this, and each of those steps was an &lt;code&gt;UPDATE&lt;/code&gt; that overwrote its predecessor. &lt;strong&gt;You cannot backfill what the table no longer contains.&lt;/strong&gt; What you can do is write down, once, what you actually know:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;record&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SubscriptionImported&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;CustomerId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Cadence&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Status&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;DateOnly&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;StartedOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DateOnly&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ImportedOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;One event per row, holding the state on the day you switch over and nothing about how it came about. Giving it its own type rather than dressing it up as a &lt;code&gt;subscription-started&lt;/code&gt; matters: every later reader can tell an inherited state from a real beginning, and nobody ends up building a statistic on a start date that was never observed.&lt;/p&gt;&#xA;&lt;p&gt;Three consequences follow, and all three belong in the plan.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Your history begins on migration day, not at the beginning of time.&lt;/strong&gt; Any question you ask later reaches back to that day and no further. That is exactly the property you just held against adding a column – with one difference, which is the whole point: the column answers the question you thought of today, while the events also answer the ones you have not thought of yet. Both start collecting now. Only one of them stops being useful when the question changes.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Anything you measure will be skewed for a while.&lt;/strong&gt; A cancellation whose downgrade happened before migration day looks, in the events, like a cancellation out of nowhere. Every rate you compute in the first months understates what is really going on, and it keeps understating it until the oldest events are older than the pattern you are looking for.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;And you do not have to take everything.&lt;/strong&gt; In the example, only the subscriptions still running are imported – the ones already canceled are finished, and nothing further will happen to them. That is a scope decision with a visible consequence: the rebuilt table below has fifteen rows where the original had sixteen, because &lt;code&gt;sub-0004&lt;/code&gt; was canceled back in 2024 and therefore never received an event. In a real system you would go one of two ways: import those rows too, as one closing event each, or point the projection at the existing table instead of a fresh one, so the finished rows simply stay where they are.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-write-path&#34;&gt;The Write Path&lt;a class=&#34;anchor&#34; href=&#34;#the-write-path&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Everything from here runs against a local instance of EventSourcingDB. &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/running-eventsourcingdb&#34;&gt;Running it&lt;/a&gt;&lt;/strong&gt; for development is a single &lt;code&gt;docker run&lt;/code&gt; with a handful of flags – an API token, a temporary data directory, and HTTP instead of HTTPS so you do not have to deal with certificates first. The API is HTTP, so any language that can make a request is already a client; the &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/sdks/dotnet&#34;&gt;.NET SDK&lt;/a&gt;&lt;/strong&gt; used below is one &lt;code&gt;dotnet add package EventSourcingDb&lt;/code&gt; away and saves you writing that plumbing yourself. Either way you can be writing your first event within minutes of deciding to try.&lt;/p&gt;&#xA;&lt;p&gt;The write path has two halves, following the shape we have described as &lt;strong&gt;&lt;a href=&#34;/resources/blog/decide-evolve-repeat&#34;&gt;decide, evolve, repeat&lt;/a&gt;&lt;/strong&gt;. &lt;code&gt;Evolve&lt;/code&gt; folds the events of one subscription into whatever state a decision needs:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SubscriptionState&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Evolve&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SubscriptionState&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Event&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Type&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;switch&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Imported&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Apply&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;GetData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SubscriptionImported&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;()!),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Started&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Apply&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;GetData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SubscriptionStarted&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;()!),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Upgraded&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Apply&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;GetData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SubscriptionUpgraded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;()!),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Downgraded&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Apply&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;GetData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SubscriptionDowngraded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;()!),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;BillingCycleChanged&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Apply&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;GetData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SubscriptionBillingCycleChanged&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;()!),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Paused&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Status&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SubscriptionStatus&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Paused&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Resumed&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Status&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SubscriptionStatus&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Active&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Canceled&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Status&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SubscriptionStatus&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Canceled&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;Decide&lt;/code&gt; takes that state and a command and returns the events it produces. It builds the subject (the path an event is stored under) and dispatches to one method per command:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;IReadOnlyList&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;EventCandidate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Decide&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;SubscriptionState&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Command&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subscriptionId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subject&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;$&amp;#34;/subscriptions/{subscriptionId}&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Status&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SubscriptionStatus&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Canceled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;throw&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;InvalidOperationException&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;A canceled subscription can not be changed.&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;command&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;is&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;not&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;StartSubscription&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;or&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ImportSubscription&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;DoesExist&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;throw&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;InvalidOperationException&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;This subscription does not exist yet.&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;command&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;switch&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;Command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ChangeTier&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;changeTier&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DecideChangeTier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;changeTier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// ... one per command&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And here is the one place in the whole exercise where you have to say something the table never said:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;IReadOnlyList&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;EventCandidate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DecideChangeTier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;SubscriptionState&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ChangeTier&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[];&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;amount&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PriceList&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;AmountFor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Cadence&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;isUpgrade&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PriceList&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;RankOf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PriceList&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;RankOf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;    [&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;        isUpgrade&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;            ? new EventCandidate(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;                Source, subject, EventTypes.Upgraded,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;                new SubscriptionUpgraded(command.Tier, state.Tier, amount, state.Amount, command.EffectiveOn))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;            : new EventCandidate(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;                Source, subject, EventTypes.Downgraded,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;                new SubscriptionDowngraded(command.Tier, state.Tier, amount, state.Amount, command.EffectiveOn))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;na&#34;&gt;    ]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;PriceList.RankOf&lt;/code&gt; looks the tier up in an ordered array of &lt;code&gt;free&lt;/code&gt;, &lt;code&gt;basic&lt;/code&gt;, &lt;code&gt;pro&lt;/code&gt; and &lt;code&gt;enterprise&lt;/code&gt;, and &lt;code&gt;PriceList.AmountFor&lt;/code&gt; returns the price in cents, where a yearly plan costs ten monthly ones rather than twelve. That is why &lt;code&gt;pro yearly&lt;/code&gt; shows up as &lt;code&gt;29000&lt;/code&gt; further down. &lt;strong&gt;One comparison decides whether this was a step up or a step down, and that is the entire difference.&lt;/strong&gt; Your application already knows it at that moment: the customer just clicked something, or a sales rep just agreed to something. Today it knows, writes an &lt;code&gt;UPDATE&lt;/code&gt;, and forgets. Now it knows and says so.&lt;/p&gt;&#xA;&lt;p&gt;The handler ties the halves together – load the events for one subscription, decide, append:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;public&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;async&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Task&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;HandleAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Client&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;client&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subscriptionId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Command&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subject&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;$&amp;#34;/subscriptions/{subscriptionId}&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SubscriptionState&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Initial&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;string?&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lastEventId&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;null&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;foreach&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;client&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ReadEventsAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;subject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ReadEventsOptions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Recursive&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)))&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Subscription&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Evolve&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;lastEventId&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;newEvents&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Subscription&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Decide&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subscriptionId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;newEvents&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Count&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;precondition&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lastEventId&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;is&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;null&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Precondition&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;IsSubjectPristinePrecondition&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;subject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Precondition&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;IsSubjectOnEventIdPrecondition&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;subject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lastEventId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;client&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;WriteEventsAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;newEvents&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;precondition&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/preconditions&#34;&gt;precondition&lt;/a&gt;&lt;/strong&gt; at the end is the part people skip, and it deserves a moment. It says: append these events only if this subscription has not changed since I read it. Without it, two concurrent plan changes both read the same state and both succeed – and unlike the table, where the second &lt;code&gt;UPDATE&lt;/code&gt; would simply win, here you keep both events, each claiming the same predecessor. Nothing is lost and the history is wrong, which is worse: every answer derived from it later inherits the error.&lt;/p&gt;&#xA;&lt;p&gt;Three practical notes for C#. The SDK&#39;s &lt;code&gt;Client&lt;/code&gt; lives in &lt;code&gt;EventSourcingDb&lt;/code&gt;, while everything else used here lives in &lt;code&gt;EventSourcingDb.Types&lt;/code&gt;: &lt;code&gt;Event&lt;/code&gt;, &lt;code&gt;EventCandidate&lt;/code&gt;, &lt;code&gt;ReadEventsOptions&lt;/code&gt;, &lt;code&gt;ObserveEventsOptions&lt;/code&gt;, &lt;code&gt;Bound&lt;/code&gt;, &lt;code&gt;BoundType&lt;/code&gt; and &lt;code&gt;Precondition&lt;/code&gt;. You need both &lt;code&gt;using&lt;/code&gt; directives. The &lt;code&gt;Source&lt;/code&gt; in the code above is the CloudEvents field, not the &amp;quot;source of truth&amp;quot; sense used earlier in this post, and it has to be a &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/sources&#34;&gt;URI reference&lt;/a&gt;&lt;/strong&gt; rather than a free-form label. And the &lt;code&gt;Client&lt;/code&gt; is constructed with &lt;code&gt;JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }&lt;/code&gt;; without it, your C# property names go into the store as they are written, and everyone reading those events later gets &lt;code&gt;PreviousTier&lt;/code&gt; where they expected &lt;code&gt;previousTier&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-table-rebuilt&#34;&gt;The Table, Rebuilt&lt;a class=&#34;anchor&#34; href=&#34;#the-table-rebuilt&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The projection has to do two things a one-off replay does not: survive a restart, and keep up afterwards. Both are ordinary requirements for any read model, and &lt;strong&gt;&lt;a href=&#34;/concepts/event-handlers&#34;&gt;building event handlers&lt;/a&gt;&lt;/strong&gt; covers them in general. Together they cost roughly thirty lines: a second table, two small helpers to read and write the checkpoint, and one branch on the options. Everything below ran against SQL Server 2025 through &lt;code&gt;Microsoft.Data.SqlClient&lt;/code&gt;, and the output in this post is what that run printed.&lt;/p&gt;&#xA;&lt;p&gt;Surviving a restart means remembering where you got to:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dbo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;projection_checkpoint&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;          &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;NVARCHAR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;64&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;PRIMARY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;KEY&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;last_event_id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;NVARCHAR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;64&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Keeping up means observing rather than reading, starting after whatever you handled last:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lastEventId&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ReadCheckpointAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;connection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ProjectionName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;options&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lastEventId&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;is&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;null&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ObserveEventsOptions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Recursive&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ObserveEventsOptions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Recursive&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;LowerBound&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Bound&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;lastEventId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;BoundType&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Exclusive&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;Console&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;WriteLine&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;lastEventId&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;is&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;null&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;Starting from the beginning of the history.&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;$&amp;#34;Resuming after event {lastEventId}.&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;foreach&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;client&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ObserveEventsAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;/subscriptions&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;options&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;cancellationToken&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ApplyAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;connection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;WriteCheckpointAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;connection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ProjectionName&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;WriteCheckpointAsync&lt;/code&gt; is the same &lt;code&gt;UPDATE&lt;/code&gt;-then-&lt;code&gt;INSERT&lt;/code&gt; shape as the one below, against &lt;code&gt;projection_checkpoint&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;ObserveEventsAsync&lt;/code&gt; replays the history first and then stays on the connection for whatever arrives next, so there is no catch-up path, no live path, and no handover between them to get wrong.&lt;/p&gt;&#xA;&lt;p&gt;Applying an event is one statement per type. The subject carries the ID, and delivery is at-least-once, because a crash between applying an event and storing the checkpoint replays that event. So each statement has to end in the same state whether it runs once or twice:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;async&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Task&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ApplyAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SqlConnection&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;connection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Event&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subscriptionId&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Subject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Subject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;LastIndexOf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;sc&#34;&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)..];&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;switch&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Imported&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;GetData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SubscriptionImported&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;()!;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UpsertAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;connection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subscriptionId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CustomerId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Cadence&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Status&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;StartedOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Started&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;GetData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SubscriptionStarted&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;()!;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;UpsertAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;connection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subscriptionId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CustomerId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Cadence&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;active&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;StartedOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Upgraded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;GetData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SubscriptionUpgraded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;()!;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ExecuteAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;connection&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;s&#34;&gt;&amp;#34;UPDATE dbo.subscriptions SET tier = @tier, amount = @amount WHERE id = @id&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;@tier&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Number&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;@amount&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;Text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;@id&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subscriptionId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;64&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Downgraded is identical to Upgraded. Canceled sets status and canceled_on,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;c1&#34;&gt;// Paused and Resumed set status, BillingCycleChanged sets cadence and amount.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;default&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;throw&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;InvalidOperationException&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;s&#34;&gt;$&amp;#34;Unknown event type &amp;#39;{@event.Type}&amp;#39; at event {@event.Id}.&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That &lt;code&gt;default&lt;/code&gt; arm matters more than it looks. Without it, an event type this projection does not know yet (one that was deployed slightly ahead of it, which happens) would be skipped silently while the checkpoint moved past it, and it would never be applied. &lt;strong&gt;Refusing to continue is the safer default for something whose whole claim is that it can be rebuilt from the events.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;Text&lt;/code&gt; and &lt;code&gt;Number&lt;/code&gt; build a typed &lt;code&gt;SqlParameter&lt;/code&gt;, and &lt;code&gt;UpsertAsync&lt;/code&gt; is an &lt;code&gt;UPDATE&lt;/code&gt; followed by a conditional &lt;code&gt;INSERT&lt;/code&gt;; both are worth a closer look, but they can wait until the thing has run.&lt;/p&gt;&#xA;&lt;p&gt;Note instead that &lt;code&gt;Downgraded&lt;/code&gt; does exactly what &lt;code&gt;Upgraded&lt;/code&gt; does – &lt;strong&gt;in the read model the two collapse back into one &lt;code&gt;UPDATE&lt;/code&gt;, precisely as before. The distinction lives in the events, and the table is none the poorer for it.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Start it up on the day of the switch, and the fifteen import events produce this:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Starting from the beginning of the history.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;id         customer    tier        cadence   amount  status    started     canceled&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;-----------------------------------------------------------------------------------&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0001   cust-0001   pro         yearly     29000  active    2024-01-15&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0002   cust-0002   pro         monthly     2900  active    2024-02-03&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0003   cust-0003   enterprise  yearly     99000  active    2024-02-19&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0005   cust-0005   pro         yearly     29000  active    2024-03-22&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0006   cust-0006   enterprise  yearly     99000  active    2024-04-11&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;...&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0015   cust-0015   basic       monthly      900  paused    2024-12-01&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0016   cust-0016   pro         monthly     2900  active    2025-01-08&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That is the table as it stood that morning, rebuilt from events: the fifteen imported rows, the paused one still paused, every value where it was. Missing is only &lt;code&gt;sub-0004&lt;/code&gt;, the row that was already canceled and that we chose not to import. Stop the process and start it again, and it reports &lt;code&gt;Resuming after event 14&lt;/code&gt; (IDs start at zero, so that is the fifteenth) and applies nothing a second time. Send a command while it runs, and the row changes a moment later.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Apart from that one row you deliberately left behind, you have changed how the data gets into the table without changing what is in it.&lt;/strong&gt; The reports, the billing job, the admin screen somebody wrote four years ago – all of them keep working, untouched and unaware.&lt;/p&gt;&#xA;&lt;p&gt;Three things bite when you build this yourself, and none of them shows up until you do.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Typed parameters cut both ways.&lt;/strong&gt; &lt;code&gt;Text&lt;/code&gt; and &lt;code&gt;Number&lt;/code&gt; exist so that an &lt;code&gt;NVARCHAR&lt;/code&gt; column gets an &lt;code&gt;NVARCHAR&lt;/code&gt; parameter instead of whatever &lt;code&gt;AddWithValue&lt;/code&gt; infers from the value. But declaring a length means a value longer than the column is truncated silently, where &lt;code&gt;AddWithValue&lt;/code&gt; would have sent it in full and let SQL Server reject it. &lt;strong&gt;Typed parameters are the better habit, not a free one.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;T-SQL has no upsert.&lt;/strong&gt; There is no &lt;code&gt;ON CONFLICT&lt;/code&gt;, and &lt;code&gt;MERGE&lt;/code&gt; carries enough well-documented caveats that an &lt;code&gt;UPDATE&lt;/code&gt; followed by a conditional &lt;code&gt;INSERT&lt;/code&gt; is the safer habit:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dbo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;subscriptions&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;   &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;customer_id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;customerId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tier&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cadence&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cadence&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;       &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;amount&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;status&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;status&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;started_on&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;startedOn&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;IF&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ROWCOUNT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;INSERT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;INTO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dbo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;subscriptions&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;customer_id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cadence&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;status&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;started_on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;canceled_on&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;VALUES&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;customerId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cadence&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;status&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;@&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;startedOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;And between those two statements there is a gap.&lt;/strong&gt; Another writer could slip into it. &lt;strong&gt;The projection is meant to be the only thing writing this table, which is what makes it a read model, but nothing in the code enforces it&lt;/strong&gt;, and starting the process twice is enough to break the assumption. If you need it to hold under concurrency, put the pair in a transaction and take the lock on the &lt;code&gt;UPDATE&lt;/code&gt; itself, with &lt;code&gt;WITH (UPDLOCK, HOLDLOCK)&lt;/code&gt;. A transaction alone does not help, because an &lt;code&gt;UPDATE&lt;/code&gt; that matches no row takes no lock that would stop a second inserter.&lt;/p&gt;&#xA;&lt;p&gt;And that is where the afternoon ends. Everything works exactly as before. This is the moment that colleague asks what you have been doing all afternoon, and the answer is: nothing you can see.&lt;/p&gt;&#xA;&lt;h2 id=&#34;and-then-nothing-happens-for-eighteen-months&#34;&gt;And Then Nothing Happens for Eighteen Months&lt;a class=&#34;anchor&#34; href=&#34;#and-then-nothing-happens-for-eighteen-months&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;It is worth being blunt about this stretch, because it is the reason most people never try.&lt;/p&gt;&#xA;&lt;p&gt;For a long time, the events do nothing for you. They accumulate. The projection runs. The table looks the way it always did, and every feature you build in that period you would have built the same way anyway. If somebody asked you to justify the afternoon, you could not – not with a number, not with a demo. &lt;strong&gt;The only thing you have bought is that the history is no longer being thrown away&lt;/strong&gt;, and nobody misses history until they need it.&lt;/p&gt;&#xA;&lt;p&gt;There is a real commitment in there, and it deserves saying out loud. Events are permanent by design, and &lt;strong&gt;&lt;a href=&#34;/resources/blog/your-progress-bar-doesnt-belong-in-the-event-store&#34;&gt;what you write is as much a one-way door as what you leave out&lt;/a&gt;&lt;/strong&gt;. Trying the exercise on a throwaway instance costs nothing; &lt;strong&gt;the day you point it at production data, you are deciding to keep that history for good.&lt;/strong&gt; That is the right call for subscriptions and the wrong one for a job queue. &lt;strong&gt;&lt;a href=&#34;/resources/blog/event-sourcing-is-not-for-everyone&#34;&gt;Event Sourcing is not for everyone&lt;/a&gt;&lt;/strong&gt;, and the question that sorts it quickest is whether anyone will still want to know about this a year from now.&lt;/p&gt;&#xA;&lt;p&gt;For a subscription, obviously yes. Which is why, eighteen months later, this happens.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-question-your-product-owner-brings&#34;&gt;The Question Your Product Owner Brings&lt;a class=&#34;anchor&#34; href=&#34;#the-question-your-product-owner-brings&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The scene is invented, but you have had some version of this conversation:&lt;/p&gt;&#xA;&lt;p&gt;&amp;quot;Do people give us any warning before they cancel? I keep feeling like the ones who leave get quieter first, but I have nothing to show for it. Could we see whether they downgrade before they go?&amp;quot;&lt;/p&gt;&#xA;&lt;p&gt;Under the old table this is a project. There is no column for it, and adding one now would start empty, so the answer would arrive next year at the earliest. With the events it is a second reader over the same history, for now a one-off report rather than a stored read model: no migration, no new column, nothing to wait for.&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;foreach&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;client&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ReadEventsAsync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;/subscriptions&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ReadEventsOptions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Recursive&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)))&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subscriptionId&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Subject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Subject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;LastIndexOf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;sc&#34;&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)..];&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;switch&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Downgraded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;GetData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SubscriptionDowngraded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;()!;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;lastDowngrades&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;subscriptionId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;LastDowngrade&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PreviousTier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Tier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;EffectiveOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PreviousAmount&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Upgraded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;c1&#34;&gt;// Going back up withdraws the signal, so it must not count as a warning.&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;lastDowngrades&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Remove&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;subscriptionId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;case&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;EventTypes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Canceled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;@event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;GetData&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SubscriptionCanceled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;()!;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;cancellationCount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;++;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;lastDowngrades&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TryGetValue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;subscriptionId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;out&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;downgrade&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;warnings&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Add&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Warning&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;subscriptionId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;downgrade&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;FromTier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;downgrade&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ToTier&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;downgrade&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;On&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CanceledOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                    &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CanceledOn&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;DayNumber&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;downgrade&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;On&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;DayNumber&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;downgrade&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;AmountLost&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That is the rule. Around it sits the usual scaffolding, a dictionary, a list, a counter, two records, a sort, a median, and the code that prints the table. Another sixty lines or so, none of it interesting. Here is what it printed:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;12 subscriptions were canceled.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;9 of them had stepped down to a cheaper tier first.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;id         from        to          downgraded  canceled     days   lost&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;-----------------------------------------------------------------------&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0010   basic       free        2025-06-01  2025-07-15     44    900&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0002   pro         basic       2025-03-01  2025-05-14     74   2000&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0006   pro         basic       2025-11-01  2026-01-20     80  20000&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0022   pro         basic       2026-04-01  2026-06-30     90  20000&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0014   enterprise  pro         2026-01-05  2026-04-10     95  70000&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0017   enterprise  pro         2026-02-14  2026-05-30    105  70000&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0019   pro         basic       2026-03-01  2026-06-15    106   2000&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0009   enterprise  pro         2025-09-01  2025-12-31    121   7000&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sub-0011   pro         basic       2025-10-01  2026-02-28    150   2000&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Median warning time: 95 days.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Shortest: 44 days. Longest: 150 days.&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The figures come from invented data and a real business would find its own, so look at the shape rather than the numbers. &lt;strong&gt;Three quarters of the cancellations announced themselves first, and the median announcement came three months ahead&lt;/strong&gt; – and that is with the first months of the window still distorted by the migration, as described above, so the real proportion is not lower than this.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Three months is not an analytics finding, it is a working window: long enough to call someone.&lt;/strong&gt; And turn the same rule around, run it as a live projection instead of over the history, and it stops being a report – it becomes a list of customers who have stepped down and not yet left. That list is a queue somebody can work through.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Here is the part worth keeping.&lt;/strong&gt; Nobody guessed the question. There is no &lt;code&gt;tier_changes&lt;/code&gt; table somebody had the foresight to add, no analytics event somebody remembered to fire. &lt;strong&gt;The only foresight required was deciding that &amp;quot;moved down&amp;quot; and &amp;quot;moved up&amp;quot; are different things&lt;/strong&gt; – which is not a prediction about churn, it is just refusing to flatten two events into one because the column was one column.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-the-audit-log-wouldnt-have-helped&#34;&gt;Why the Audit Log Wouldn&#39;t Have Helped&lt;a class=&#34;anchor&#34; href=&#34;#why-the-audit-log-wouldnt-have-helped&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;There is a version of this you may already have. Somewhere in most long-running systems sits a table called &lt;code&gt;audit_log&lt;/code&gt; or &lt;code&gt;activity&lt;/code&gt; or &lt;code&gt;history&lt;/code&gt;, with a timestamp, a type, and a JSON blob. In almost every long-running codebase we have worked in, one has grown at some point – nobody plans it in the first design; it arrives around year two, when somebody needs to know who changed what.&lt;/p&gt;&#xA;&lt;p&gt;The instinct is right, and the result is half of what you want. &lt;strong&gt;It is a log you can read, not one you can rebuild from.&lt;/strong&gt; It records that a change occurred, in a shape chosen for a support ticket, not for reconstruction, and usually only for the tables somebody remembered to instrument. The current state still lives in the main table, written separately, so the two can drift and nothing notices. We have made the same argument from the other side in &lt;strong&gt;&lt;a href=&#34;/resources/blog/you-dont-need-an-audit-log&#34;&gt;you don&#39;t need an audit log&lt;/a&gt;&lt;/strong&gt;: a second write kept correct by discipline rather than by construction.&lt;/p&gt;&#xA;&lt;p&gt;The difference fits in one sentence. &lt;strong&gt;The events are not a record &lt;em&gt;of&lt;/em&gt; the write – they &lt;em&gt;are&lt;/em&gt; the write.&lt;/strong&gt; There is no second thing to keep in sync, because the table is derived from the events rather than maintained beside them. That is why the product owner&#39;s question could be answered from what was already there, rather than from whatever somebody had thought to log.&lt;/p&gt;&#xA;&lt;h2 id=&#34;two-rows-that-look-the-same&#34;&gt;Two Rows That Look the Same&lt;a class=&#34;anchor&#34; href=&#34;#two-rows-that-look-the-same&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Remember the two customers on &lt;code&gt;pro&lt;/code&gt; from the beginning, the ones the table could not tell apart. In January 2026, ten days apart, this system produced exactly that pair.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;sub-0014&lt;/code&gt; came down from &lt;code&gt;enterprise&lt;/code&gt; on the fifth, and canceled ninety-five days later; it is the fifth line in that list. &lt;code&gt;sub-0023&lt;/code&gt;, one of the subscriptions that started after migration day, came up from &lt;code&gt;basic&lt;/code&gt; on the fifteenth and never appears in that list at all, because it never canceled. &lt;strong&gt;That absence is the entire difference&lt;/strong&gt; – and it was there to be seen long before the cancellation: for the eighty-five days the two overlapped, the standing list of customers who had stepped down and not yet left held one of them and not the other, while the table showed the same word for both. &lt;strong&gt;The difference was never missing from the business. It was missing from the table&lt;/strong&gt;, and only because a column holds one value at a time.&lt;/p&gt;&#xA;&lt;p&gt;You do not have to fix that everywhere, and you do not have to fix it today. Pick the one table where the history would have told you something. Spend an afternoon moving its write path onto events and rebuilding the table from them, and accept that for a while it will look like you achieved nothing – because visibly, nothing is what you achieved. What you actually did was stop throwing away the part nobody misses until they need it.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;/resources/blog/postgresql-is-not-an-event-store&#34;&gt;The post on PostgreSQL&lt;/a&gt;&lt;/strong&gt; ended by saying that trying EventSourcingDB is an experiment rather than a migration: something you can undo by deleting a container. This is what that experiment looks like on one table, and &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/installing-eventsourcingdb&#34;&gt;installing it&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href=&#34;/learn/tutorials/first-steps-with-eventsourcingdb/writing-events&#34;&gt;writing your first events&lt;/a&gt;&lt;/strong&gt; are where it starts.&lt;/p&gt;&#xA;&lt;p&gt;The step after that is not technical. Go and ask the people who work with that table every day which questions they gave up on – not the ones on the roadmap, the ones they stopped raising because the answer was always &amp;quot;we don&#39;t store that&amp;quot;. You will hear one within a minute, and it will tell you which table to pick better than any rule we could give you. &lt;strong&gt;We would like to hear what you find&lt;/strong&gt;: write to us at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt;. The questions people stopped asking are the most interesting thing about any system, and they are almost never written down anywhere.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/postgresql-is-not-an-event-store</id>
    <title>PostgreSQL Is Not an Event Store</title>
    <link href="https://www.eventfoundation.io/resources/blog/postgresql-is-not-an-event-store"></link>
    <published>2026-08-17T00:00:00+02:00</published>
    <updated>2026-08-17T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">The table takes about fifteen minutes to write. An ID, a stream name, a version number, an event type, a JSON payload, a timestamp. Add a unique constraint, add an index, and you have somewhere to put your events. It is the first thing almost every team reaches for, and for good reason: the database is already running, already backed up, already monitored, and everybody on the team can read it.</summary>
    <content type="html">&lt;p&gt;The table takes about fifteen minutes to write. An ID, a stream name, a version number, an event type, a JSON payload, a timestamp. Add a unique constraint, add an index, and you have somewhere to put your events. It is the first thing almost every team reaches for, and for good reason: the database is already running, already backed up, already monitored, and everybody on the team can read it.&lt;/p&gt;&#xA;&lt;p&gt;In &lt;strong&gt;&lt;a href=&#34;/resources/blog/kafka-is-not-an-event-store&#34;&gt;the previous post&lt;/a&gt;&lt;/strong&gt; we pinned down what an event store actually has to do and held Kafka against that list. This one turns it around and points it at the tool people reach for from the opposite direction – the general-purpose database they already trust with everything else. &lt;strong&gt;PostgreSQL passes more of that list than Kafka did, and that is exactly what makes it the harder case.&lt;/strong&gt; Kafka&#39;s central failure announces itself. PostgreSQL fails without a sound, a year in, when a read model is missing a row that nobody can account for.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;giving-the-table-its-due&#34;&gt;Giving the Table Its Due&lt;a class=&#34;anchor&#34; href=&#34;#giving-the-table-its-due&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The approach deserves a fair hearing first. A relational database hands you transactions, durability, point-in-time recovery, replication, and thirty years of operational practice around all of it – and the events table on top of it is genuinely simple. Appending an event is an &lt;code&gt;INSERT&lt;/code&gt;. Loading an aggregate is a &lt;code&gt;SELECT&lt;/code&gt; with a &lt;code&gt;WHERE&lt;/code&gt; and an &lt;code&gt;ORDER BY&lt;/code&gt;. Writing three events as one unit is a transaction, which the database has done well since before most of us started programming. &lt;strong&gt;None of this is naive.&lt;/strong&gt; It is the reasonable first move.&lt;/p&gt;&#xA;&lt;p&gt;We know, because we made it. Before EventSourcingDB there was wolkenkit, and &lt;strong&gt;&lt;a href=&#34;/resources/blog/ten-years-one-goal&#34;&gt;its event store ran on relational databases&lt;/a&gt;&lt;/strong&gt; – PostgreSQL, MariaDB, SQL Server. It worked. It never felt like the right shape, and it took us a long time to say why. &lt;strong&gt;The argument here is not that the table does not work. It is about what &amp;quot;works&amp;quot; is quietly covering for.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-same-six-item-list&#34;&gt;The Same Six-Item List&lt;a class=&#34;anchor&#34; href=&#34;#the-same-six-item-list&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Here is the yardstick again, unchanged from the first post. Changing the list to suit the candidate would defeat the purpose of having one.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;A durable source of truth.&lt;/strong&gt; Events are not data in transit on their way somewhere else; they are the truth itself, kept for as long as the system lives.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Append-only, immutable storage.&lt;/strong&gt; Not by convention or team discipline, but as a guarantee the store enforces, so that no past event can ever be rewritten or quietly dropped.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Atomic writes across multiple events.&lt;/strong&gt; A single decision that produces several events is persisted as one indivisible unit, so you never store half of it.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Optimistic concurrency per stream.&lt;/strong&gt; The ability to append events only if that stream has not changed since you last read it, which is how invariants survive concurrent writes.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Two different read patterns.&lt;/strong&gt; Reading the entire history in order, to build and rebuild read models, and reading a single entity&#39;s events cheaply, to reconstruct that one entity&#39;s current state.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Subscriptions as a first-class feature.&lt;/strong&gt; Observing new events as they arrive, with a seamless transition from replaying the past to following the present.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Six capabilities, and an event store has to deliver all of them together, not most of them with effort. Kafka came apart on the first item. PostgreSQL does not.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-postgresql-gets-right&#34;&gt;What PostgreSQL Gets Right&lt;a class=&#34;anchor&#34; href=&#34;#what-postgresql-gets-right&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Take that first criterion. &lt;strong&gt;A relational database is a durable source of truth by construction, and it takes work to make it stop being one.&lt;/strong&gt; There is no retention window quietly expiring the events you were counting on, no compaction collapsing history. What you wrote is there next decade.&lt;/p&gt;&#xA;&lt;p&gt;Atomic writes across multiple events fall out for free: one &lt;code&gt;INSERT&lt;/code&gt; with several rows inside one transaction, all of them landing or none, including when they belong to different streams. Reading a single entity&#39;s events cheaply is an index on stream name and version. &lt;strong&gt;Neither is a concession. This is a relational database doing what it was built to do.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Optimistic concurrency takes more work and is still achievable. A unique constraint on stream name and version means two writers who both believe they are appending version 7 cannot both succeed: one commits, the other gets a constraint violation, and you translate that into a domain-level conflict. Plenty of production systems run on exactly this.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The difference from a purpose-built store is not who has to remember – both make every writing path declare its expectation – but what you can declare.&lt;/strong&gt; A unique constraint only checks the number it is handed: a writer that computes the version as &lt;code&gt;MAX(version) + 1&lt;/code&gt; inside the &lt;code&gt;INSERT&lt;/code&gt;, rather than passing the one it read, appends without complaint even though the stream moved on in between. A &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/preconditions&#34;&gt;precondition&lt;/a&gt;&lt;/strong&gt; travels with the write itself and reaches conditions a constraint cannot express.&lt;/p&gt;&#xA;&lt;p&gt;That is two of the six outright, a third once you have written the constraint yourself and every writing path uses it as intended, and half of a fourth: the half of the read patterns that loads a single entity. Two criteria are left, and neither is subtle. What is subtle is the other half – the one PostgreSQL looks like it passes.&lt;/p&gt;&#xA;&lt;h2 id=&#34;append-only-is-a-promise-not-a-property&#34;&gt;Append-Only Is a Promise, Not a Property&lt;a class=&#34;anchor&#34; href=&#34;#append-only-is-a-promise-not-a-property&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The second criterion draws the line exactly where PostgreSQL falls: not by convention or team discipline, but as a guarantee the store enforces. &lt;strong&gt;In a relational database, &lt;code&gt;UPDATE&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; exist on every table, always.&lt;/strong&gt; You can make them harder to reach: revoke the privileges, connect as a role that only holds &lt;code&gt;INSERT&lt;/code&gt; and &lt;code&gt;SELECT&lt;/code&gt;, add a &lt;code&gt;BEFORE UPDATE&lt;/code&gt; trigger that raises. Each of these is a fence. None is a wall.&lt;/p&gt;&#xA;&lt;p&gt;Fences come down in unremarkable ways. A migration tool recreates the table and the trigger does not survive it. A new service copies its connection string from an older one and arrives with more privileges than intended. &lt;strong&gt;None of this requires bad intent. It only requires an ordinary Tuesday.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;We have made part of this argument before. An audit log beside your tables is &lt;strong&gt;&lt;a href=&#34;/resources/blog/you-dont-need-an-audit-log&#34;&gt;a second write, kept correct by discipline rather than by construction&lt;/a&gt;&lt;/strong&gt;. Here there is no second write – only the discipline, which is the part that fails. The same holds for &lt;strong&gt;&lt;a href=&#34;/resources/blog/proving-without-revealing-merkle-trees&#34;&gt;a hash chain&lt;/a&gt;&lt;/strong&gt; over rows an &lt;code&gt;UPDATE&lt;/code&gt; can reach: it goes invalid the moment somebody reaches them, and a chain nobody has published outside the database can simply be recomputed.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;A store with no edit operation and a table that nobody is supposed to edit are different kinds of object.&lt;/strong&gt; The first is a property of the system, the second a property of the people currently operating it – and people change jobs. EventSourcingDB has no edit operation to revoke, and each event carries the hash of its predecessor, computed by the database rather than by the writer, so a later alteration is something you detect rather than something you trust did not happen.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-gap-you-cannot-see&#34;&gt;The Gap You Cannot See&lt;a class=&#34;anchor&#34; href=&#34;#the-gap-you-cannot-see&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now the interesting one, because on paper it looks fine. Reading the entire history in order needs a global sequence, and PostgreSQL has an obvious candidate: a &lt;code&gt;BIGSERIAL&lt;/code&gt; or identity column handing out numbers that only go up. A projection remembers the highest number it processed and asks for everything above it. It is the standard recipe.&lt;/p&gt;&#xA;&lt;p&gt;Here is what that misses. &lt;strong&gt;Sequence numbers are handed out at &lt;code&gt;INSERT&lt;/code&gt; time, not at commit time, and the sequence never takes one back.&lt;/strong&gt; That is deliberate and right – it is what lets two writers insert at the same time without queueing behind each other for a number. But it means the order in which numbers are assigned and the order in which rows become visible are two different orders. Transaction A takes 43. Transaction B takes 44. B commits first. For a moment, the table contains a visible row 44 and an invisible row 43.&lt;/p&gt;&#xA;&lt;p&gt;Now let the projection poll during that moment. It asks for everything above 42, receives 44, processes it, and records 44 as its position. A moment later A commits and row 43 becomes visible – to a reader that will never ask for it again. &lt;strong&gt;The event is not lost, not corrupted, not delayed. It is simply skipped, permanently, by a consumer with no way of knowing it happened.&lt;/strong&gt; Nothing raises, nothing logs, and the read model is just wrong – and stays wrong until somebody notices a number that is off.&lt;/p&gt;&#xA;&lt;p&gt;One writer at a time and this never happens, which is why it stays hidden so long. Nobody marks the day the second one arrives – usually the day somebody raises the connection pool size or adds a worker.&lt;/p&gt;&#xA;&lt;p&gt;Every workaround costs something real. You cannot simply wait for the missing number to appear: rolled-back transactions burn their sequence values for good, so a gap might mean &amp;quot;not committed yet&amp;quot; or &amp;quot;never will be&amp;quot;. A few seconds&#39; lag before processing buys probability, not correctness. Comparing each row&#39;s &lt;code&gt;xmin&lt;/code&gt; against &lt;code&gt;pg_snapshot_xmin(pg_current_snapshot())&lt;/code&gt; is correct only once the cursor stops being a sequence number and becomes a transaction ID – and then every projection is coupled to transaction-visibility internals, and one long-running write transaction stalls them all. An advisory lock works if it is held from before the &lt;code&gt;INSERT&lt;/code&gt; until the commit, and gives up concurrent writes.&lt;/p&gt;&#xA;&lt;p&gt;By contrast, &lt;strong&gt;EventSourcingDB assigns every event a unique, monotonically increasing ID, globally across all subjects and &lt;a href=&#34;/docs/eventsourcingdb/consistency-guarantees&#34;&gt;without gaps&lt;/a&gt;, and an event becomes visible only once its transaction has committed.&lt;/strong&gt; A cursor means what a cursor appears to mean.&lt;/p&gt;&#xA;&lt;h2 id=&#34;nobody-subscribes-to-a-table&#34;&gt;Nobody Subscribes to a Table&lt;a class=&#34;anchor&#34; href=&#34;#nobody-subscribes-to-a-table&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The last criterion asks for subscriptions as a first-class feature. PostgreSQL&#39;s answer is &lt;code&gt;LISTEN&lt;/code&gt; and &lt;code&gt;NOTIFY&lt;/code&gt;, and it is cleverer than it gets credit for: a notification is delivered when the transaction commits, so you are never told about an event that then rolls back.&lt;/p&gt;&#xA;&lt;p&gt;But a notification is a nudge, not a delivery. &lt;strong&gt;It is not durable&lt;/strong&gt; – a listener disconnected when it fires never learns that anything happened, and nothing replays it. The payload has to stay under 8,000 bytes, so it can carry the news that an event exists but not, reliably, the event. Put a position in there instead, and it still helps nobody who was not connected to receive it.&lt;/p&gt;&#xA;&lt;p&gt;Which means you poll – precisely the cursor whose gap we just took apart. &lt;strong&gt;The two failures reinforce each other:&lt;/strong&gt; the ordering gap only bites once something is reading by cursor, and that reader is the part you have to build yourself.&lt;/p&gt;&#xA;&lt;p&gt;Notice too what &amp;quot;seamless&amp;quot; was asking for. &lt;code&gt;NOTIFY&lt;/code&gt; delivers live events but never replays, so you write a catch-up path, a live path, and a handover between them – and the handover is where the off-by-one lives, at exactly the moment events arrive faster than the catch-up finishes.&lt;/p&gt;&#xA;&lt;p&gt;The alternative is the one that actually works, and it deserves a price rather than a wave. &lt;strong&gt;A logical replication slot hands you changes in commit order&lt;/strong&gt;, and it is a PostgreSQL feature, not a bolt-on. But a slot exists per consumer and somebody creates and monitors each one, and an unread slot pins write-ahead log on disk until the volume fills. That is not a query; it is a service you operate.&lt;/p&gt;&#xA;&lt;p&gt;The other route is a transactional outbox – and we have argued that &lt;strong&gt;&lt;a href=&#34;/resources/blog/you-dont-need-an-outbox&#34;&gt;you don&#39;t need an outbox&lt;/a&gt;&lt;/strong&gt; when storing an event and publishing it are one atomic act. Reaching for one here is the Kafka symptom from the other direction.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-you-end-up-building&#34;&gt;What You End Up Building&lt;a class=&#34;anchor&#34; href=&#34;#what-you-end-up-building&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Step back and look at what is now on the bill. Revoked privileges and a trigger to approximate append-only, plus a review habit so the next migration does not undo them. A read path that respects commit order rather than sequence order – or, if you settle for a lag window, the monitoring for when it was too narrow. A poller, or a slot and its upkeep. And the tests that prove any of it, which means reproducing an out-of-order commit on purpose. &lt;strong&gt;Idempotent projections and a stored cursor you would have needed either way; the rest is the price of the table.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Then the part that never makes it into the estimate. &lt;strong&gt;Somebody has to own all of it&lt;/strong&gt;: write down why the trigger is there, explain it to whoever arrives next, take the call when a projection drifts. This is not a library you install; it is a component your team maintains forever, beside the product you were hired to build.&lt;/p&gt;&#xA;&lt;p&gt;Piece by piece, &lt;strong&gt;you build an event store inside your relational database – the very thing you set out to avoid needing.&lt;/strong&gt; The Kafka case at least forces the conversation: a retention window eventually deletes something that mattered, and somebody has to explain why. PostgreSQL never forces it. It holds your events faithfully for a decade and lets you find the missing pieces one incident at a time.&lt;/p&gt;&#xA;&lt;h2 id=&#34;where-the-tables-job-ends&#34;&gt;Where the Table&#39;s Job Ends&lt;a class=&#34;anchor&#34; href=&#34;#where-the-tables-job-ends&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The fifteen minutes at the top of this post were real. The columns are right, and nobody would correct that table on a whiteboard. What is wrong is the assumption that you were finished. &lt;strong&gt;The table was not an event store. It was the first fifteen minutes of one.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;There is a tension here with something we have written before. We have said that &lt;strong&gt;&lt;a href=&#34;/resources/blog/it-was-never-about-the-database&#34;&gt;it was never about the database&lt;/a&gt;&lt;/strong&gt; – that the most valuable outcome of Event Sourcing is the shared language a team builds while modeling. We still mean it: a team that models well on a plain table beats one that models badly on a purpose-built store. &lt;strong&gt;But infrastructure still has to hold&lt;/strong&gt;, and &amp;quot;start with what you already run&amp;quot; is sound advice until what you already run holds the only copy of the truth.&lt;/p&gt;&#xA;&lt;p&gt;And there is a place where a relational database is not a compromise at all. Looking up current state by a known key is its home ground: a table shaped for one query, indexed for that query, answering in single-digit milliseconds, rebuilt whenever the shape changes. An event-sourced system usually has several such read models – &lt;strong&gt;&lt;a href=&#34;/resources/blog/the-read-model-zoo&#34;&gt;and several other shapes besides&lt;/a&gt;&lt;/strong&gt;. The relational database keeps its place in the architecture. It just is not this place.&lt;/p&gt;&#xA;&lt;p&gt;Which leaves one sentence. &lt;strong&gt;An event store is not a general-purpose database with a table that happens to contain events.&lt;/strong&gt; It is a store whose guarantees &lt;em&gt;are&lt;/em&gt; the event model: append-only because it cannot be otherwise, globally ordered because the order is fixed at commit rather than guessed at afterward, subscribable because handing out events as they arrive is what a log of truth is for. Those are not features layered on top of a database. They are what is left when you take the general purpose away.&lt;/p&gt;&#xA;&lt;p&gt;This is the second entry in the series, and the two candidates fail from opposite ends. Kafka keeps events beautifully in motion and poorly at rest. PostgreSQL keeps them beautifully at rest and has no idea they are events. Next, the same list goes to the NoSQL databases, where a schema flexible enough to hold anything makes events look like a natural fit.&lt;/p&gt;&#xA;&lt;p&gt;If you would rather feel the difference than take our word for it, that is an experiment, not a migration. Every event carries a globally ordered, gap-free ID, and one connection delivers the full history followed by the live events, so a subscriber never guesses whether it skipped something. Rebuilding a read model is pointing it at ID &lt;code&gt;0&lt;/code&gt; and letting it run. It is one binary or one container, nothing to stand up beside it, so &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/installing-eventsourcingdb&#34;&gt;installing it&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href=&#34;/learn/tutorials/first-steps-with-eventsourcingdb/writing-events&#34;&gt;writing your first events&lt;/a&gt;&lt;/strong&gt; take about as long as the table did.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/your-progress-bar-doesnt-belong-in-the-event-store</id>
    <title>Your Progress Bar Doesn&#39;t Belong in the Event Store</title>
    <link href="https://www.eventfoundation.io/resources/blog/your-progress-bar-doesnt-belong-in-the-event-store"></link>
    <published>2026-08-13T00:00:00+02:00</published>
    <updated>2026-08-13T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">The progress bar works. It fills up, users can see how far along their contract review is, and nobody has complained. Then you open the event store and find 312 events for a single contract, 310 of which nobody would have written if that bar did not have to move.</summary>
    <content type="html">&lt;p&gt;The progress bar works. It fills up, users can see how far along their contract review is, and nobody has complained. Then you open the event store and find 312 events for a single contract, 310 of which nobody would have written if that bar did not have to move.&lt;/p&gt;&#xA;&lt;p&gt;Nothing is broken, and &lt;strong&gt;that is exactly what makes this hard to argue about&lt;/strong&gt;. The system does what it should, the event names speak the language of the domain, and every one of those events was written on purpose. The unease is real anyway, and if you can&#39;t say where it comes from, the discussion ends before it starts. So: &lt;strong&gt;most of those events have a shelf life, and the event store is the one place in your system that doesn&#39;t&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;the-chain-that-leads-there&#34;&gt;The Chain That Leads There&lt;a class=&#34;anchor&#34; href=&#34;#the-chain-that-leads-there&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Nobody decides one morning to fill the store with noise. The reasoning that gets you there is short, and every step in it sound.&lt;/p&gt;&#xA;&lt;p&gt;A long-running process should show how far it has come. That progress is read from a projection, because in an event-sourced system a projection is how you read anything. Projections are updated by events. Therefore you need events – one when a clause check begins, one when it ends – and the bar has what it needs.&lt;/p&gt;&#xA;&lt;p&gt;Four steps, no error in any of them. And once those events exist, they defend themselves rather convincingly: they have business-sounding names, and something visible in the product depends on them. Try removing &lt;code&gt;ClauseCheckStarted&lt;/code&gt; and you will be told, correctly, that a feature people asked for would stop working.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;This is why the pattern survives review: it doesn&#39;t look like a shortcut, it looks like Event Sourcing done properly.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-litmus-test-has-a-blind-spot&#34;&gt;The Litmus Test Has a Blind Spot&lt;a class=&#34;anchor&#34; href=&#34;#the-litmus-test-has-a-blind-spot&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;A while ago, in &lt;strong&gt;&lt;a href=&#34;/resources/blog/event-sourcing-is-not-for-everyone&#34;&gt;Event Sourcing is Not For Everyone&lt;/a&gt;&lt;/strong&gt;, we offered a test for telling domain events from raw data: &lt;em&gt;Would anyone want to be notified about this specific occurrence?&lt;/em&gt; If yes, you&#39;re probably looking at an event. If no, it&#39;s probably just data. That test does good work. It sorts GPS coordinates and temperature readings out of the store, and it does it in one question.&lt;/p&gt;&#xA;&lt;p&gt;Run the clause check through it. Would anyone want to be notified that clause 47 of 155 has just been examined? &lt;strong&gt;Yes.&lt;/strong&gt; Someone is sitting in front of a screen waiting for exactly that.&lt;/p&gt;&#xA;&lt;p&gt;On the face of it there is a consequence: a projection updates, a bar moves. This is no sensor reading without meaning of its own; it&#39;s a deliberate step in a deliberate process.&lt;/p&gt;&#xA;&lt;p&gt;The test waves it through, and the test is not wrong. &lt;strong&gt;It is incomplete, because it asks whether anyone cares and asks it in the present tense.&lt;/strong&gt; Progress is the clearest kind of fact that answers yes to that question and still doesn&#39;t belong in the store, because the interest it commands expires the moment the process ends.&lt;/p&gt;&#xA;&lt;p&gt;You could push back here, because the original example justified its yes with &amp;quot;Multiple parts of the system care about this occurrence&amp;quot;, and only the progress view cares about a clause check. But what gets quoted in design discussions is the question, not the sentence that followed it in one example. And &lt;strong&gt;the question is passed the moment the projection exists&lt;/strong&gt;, because from then on something in the system genuinely does care – you built it to.&lt;/p&gt;&#xA;&lt;h2 id=&#34;three-shelf-lives&#34;&gt;Three Shelf Lives&lt;a class=&#34;anchor&#34; href=&#34;#three-shelf-lives&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;None of this is about disk space. We ran that math in &lt;strong&gt;&lt;a href=&#34;/resources/blog/18-months-of-events-fit-on-four-floppy-disks&#34;&gt;18 Months of Events Fit on Four Floppy Disks&lt;/a&gt;&lt;/strong&gt;, and the conclusion stands: events are small, stores handle millions of them, and storage should almost never enter an architectural decision. &lt;strong&gt;The cost here isn&#39;t bytes, it&#39;s noise.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;So the useful question isn&#39;t whether a fact matters. &lt;strong&gt;It&#39;s how long it matters&lt;/strong&gt;, and there are three answers.&lt;/p&gt;&#xA;&lt;p&gt;Some facts matter &lt;strong&gt;right now&lt;/strong&gt; and not a second longer. The message that moves the bar one step is the clearest example: it is interesting on its way to the screen, and by the time it arrives the next one is already due. Nothing needs to store it at all.&lt;/p&gt;&#xA;&lt;p&gt;Some facts matter &lt;strong&gt;for a while&lt;/strong&gt;. They have to survive a crash so the run can pick up where it left off instead of starting over, and they have to survive a page reload so the bar doesn&#39;t reset to zero. But once the run is finished, their job is finished too.&lt;/p&gt;&#xA;&lt;p&gt;And some facts matter &lt;strong&gt;forever&lt;/strong&gt;. That a contract was reviewed on a particular day, against a particular rule, with a particular outcome, will not stop being true or useful. &lt;strong&gt;That is what the event store is for&lt;/strong&gt;, and why EventSourcingDB has no expiry dates and no API that removes an event. The manifesto in &lt;strong&gt;&lt;a href=&#34;/resources/blog/thinking-in-events&#34;&gt;Thinking in Events&lt;/a&gt;&lt;/strong&gt; says as much: we do not erase the past, we add to it.&lt;/p&gt;&#xA;&lt;p&gt;Now look at how most systems are laid out. There&#39;s memory, which dies with the process, and the event store, which keeps things forever. There&#39;s usually a read-model database too, but everything in it is derived and rebuildable from the events at any time – and how far a run has come is precisely what isn&#39;t.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;So the middle horizon ends up with no designated home&lt;/strong&gt;, and when something has to survive a crash, the store is the first thing that comes to mind. That isn&#39;t carelessness: where durability means the store, it&#39;s the obvious move.&lt;/p&gt;&#xA;&lt;h2 id=&#34;state-not-history&#34;&gt;State, Not History&lt;a class=&#34;anchor&#34; href=&#34;#state-not-history&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;What the progress view needs is not a stream of things that happened, but where the run stands – and that difference is the whole point.&lt;/p&gt;&#xA;&lt;p&gt;The distinction isn&#39;t large versus small. If you want to show which clauses are done and which one is being worked on right now, the state is a list, and for a long contract a long one. &lt;strong&gt;That list is still state.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;It has one current version, gets written over as the run proceeds, and only the latest is ever read. A history keeps every version that ever existed, in order, timestamped, and never lets go.&lt;/p&gt;&#xA;&lt;p&gt;You can derive the state from the history – that&#39;s what a projection does, and usually it&#39;s a good deal. Here it isn&#39;t, because &lt;strong&gt;you pay for the derivation with permanence&lt;/strong&gt;: to know where the run stands, you commit to remembering every step it took, forever.&lt;/p&gt;&#xA;&lt;p&gt;So keep the state as state. There are three ordinary places to put it:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Process state you overwrite.&lt;/strong&gt; A row per run, updated as it goes: which clauses are done, which one is in progress, how many are left. Survives a crash, supports resuming, and is deleted when the run ends.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;A store that forgets on its own.&lt;/strong&gt; A cache or key-value store with a time to live, where expiry is built in and nobody has to remember to clean up – exactly what the event store deliberately lacks.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Whatever your infrastructure already tracks.&lt;/strong&gt; If the run goes through a queue, a scheduler, or a workflow engine, the progress and the resume point are already there. Then the fix isn&#39;t a new component, it&#39;s not writing the same information twice.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;None of these is exotic, and that&#39;s rather the point. &lt;strong&gt;The middle horizon doesn&#39;t need clever technology. It needs to be noticed.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;The first of them invites an objection, and a team doing Event Sourcing will raise it at once: a row per run is a second place that writes durably, and getting away from that is half the reason anyone adopts Event Sourcing. But &lt;strong&gt;the process row is not a second source of truth&lt;/strong&gt;, because nothing about the review is decided from it. If it is lost or wrong, the run redoes some work and the bar starts over – the only durable claim about the review is the closing event.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-the-closing-event-carries&#34;&gt;What the Closing Event Carries&lt;a class=&#34;anchor&#34; href=&#34;#what-the-closing-event-carries&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Skipping the per-clause events doesn&#39;t mean losing what the review found. The findings exist either way; the only question is where they&#39;re written down – and the closing event has room.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;ContractReviewCompleted&lt;/code&gt; can carry which clauses were examined, which were flagged, and what the finding was for each. That&#39;s not a compromise but a better record than 310 separate events, because it can be read in one go by anyone asking what people actually ask later: what came out of the review of contract 4711? It makes for a wide event, which is fine – &lt;strong&gt;&lt;a href=&#34;/resources/blog/dont-put-that-pdf-in-your-event&#34;&gt;Don&#39;t Put That PDF in Your Event&lt;/a&gt;&lt;/strong&gt; is about attachments, not structured domain information.&lt;/p&gt;&#xA;&lt;p&gt;What genuinely goes away is the order in which the clauses were examined, and how long each took. Be deliberate about that, because it is the real loss – then notice how unlikely anyone is to ask. &lt;strong&gt;Nobody opens an event store to find out that clause 12 was checked before clause 47.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;There is a second reason the per-clause events are redundant. &amp;quot;The contract was reviewed&amp;quot; already says that its clauses were examined; that&#39;s what reviewing a contract means. &lt;strong&gt;Where the upper level takes in the lower one by definition, the lower one is contained in its result&lt;/strong&gt; – which holds here, and does not hold everywhere, so it is worth checking rather than assuming. Once the closing event is written, every step anyone will ask about later has been said twice, and only one of the two versions will ever be read.&lt;/p&gt;&#xA;&lt;h2 id=&#34;private-journal-public-announcement&#34;&gt;Private Journal, Public Announcement&lt;a class=&#34;anchor&#34; href=&#34;#private-journal-public-announcement&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;There&#39;s a second way to arrive at the same line, and it has nothing to do with time but with direction.&lt;/p&gt;&#xA;&lt;p&gt;In &lt;strong&gt;&lt;a href=&#34;/resources/blog/one-database-to-rule-them-all&#34;&gt;One Database to Rule Them All&lt;/a&gt;&lt;/strong&gt; we put it like this: your domain events are your private journal, your integration events are your public announcements, and the two should be kept apart. Domain events can be fine-grained, awkwardly specific, and shaped entirely around what your write model needs, precisely because nobody outside has to understand them. That&#39;s the freedom of keeping them inside.&lt;/p&gt;&#xA;&lt;p&gt;That post drew the line between services. The same line runs between your service and its own front end. &lt;strong&gt;The browser is outside too.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Progress information sits firmly on the outer side: it exists to be shown to someone, and &lt;strong&gt;no domain decision rests on it&lt;/strong&gt;. That explains the pattern differently than the chain at the top of this post, and it reaches further.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;/learn/tutorials/first-steps-with-eventsourcingdb/observing-events&#34;&gt;Observing events&lt;/a&gt;&lt;/strong&gt; is the obvious way to keep something in sync in an event-sourced system. So when the browser needs to stay in sync, the same reflex fires – and to observe something, something has to be written. &lt;strong&gt;They passed for facts, and nothing in the test stopped them. But what actually put them there is that the store was the transport lying around.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;The consequence is worth spelling out: &lt;strong&gt;your internal model is now the wire format of your user interface&lt;/strong&gt;. Rename an event, split it, change its payload, and something on a screen breaks. That is a coupling nobody chose and nobody wrote down.&lt;/p&gt;&#xA;&lt;p&gt;The alternative is to send the two kinds of thing differently. Progress is pushed to the client with its content, over the ordinary server-push channel you would use anyway and fed straight from the process state, so nothing has to be written for it to travel. A change to the domain is announced without its content – a signal that something is new, after which the client asks for what it needs through the read model built for it. &lt;strong&gt;For the domain: push to pull.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Observing keeps its place; it is still how your own components learn that something happened. What changes is what leaves the building: the observer inside turns a domain event into a signal, and &lt;strong&gt;the signal is what the browser gets to see&lt;/strong&gt;. The journal stays inside, and the outside is told only that there&#39;s something new to look at.&lt;/p&gt;&#xA;&lt;h2 id=&#34;but-what-if-i-need-it-later&#34;&gt;But What If I Need It Later?&lt;a class=&#34;anchor&#34; href=&#34;#but-what-if-i-need-it-later&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The strongest objection to all of this is a good one, and it deserves better than reassurance. In &lt;strong&gt;&lt;a href=&#34;/resources/blog/naming-events-beyond-crud&#34;&gt;Naming Events Beyond CRUD&lt;/a&gt;&lt;/strong&gt; we called it a one-way door: you can always combine fine-grained events into a coarser view, but you can never split a coarse event into finer ones after the fact. Write too little and the information is gone for good.&lt;/p&gt;&#xA;&lt;p&gt;That&#39;s true. What&#39;s less often said is that &lt;strong&gt;there is a second one-way door, facing the other way&lt;/strong&gt;: everything you write is permanent as well.&lt;/p&gt;&#xA;&lt;p&gt;Every event type has to be versioned as the domain moves under it – see &lt;strong&gt;&lt;a href=&#34;/resources/blog/versioning-events-without-breaking-everything&#34;&gt;Versioning Events Without Breaking Everything&lt;/a&gt;&lt;/strong&gt;. Every event is replayed each time a projection is rebuilt, and every event is something a person debugging at two in the morning has to read past. And getting personal data back out of a store designed never to forget means migrating the whole thing into a fresh instance, which is why our &lt;strong&gt;&lt;a href=&#34;/concepts/gdpr-compliance&#34;&gt;GDPR Compliance&lt;/a&gt;&lt;/strong&gt; guide opens by telling you to think before you write.&lt;/p&gt;&#xA;&lt;p&gt;So both directions cost something: there&#39;s no safe default and no reflex that gets you out of deciding. &lt;strong&gt;The answer isn&#39;t fewer events. It&#39;s the events someone will ask about.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;If the sequence genuinely matters in your domain – because an auditor will want the order, or because the duration per clause is the product – then model it, deliberately. If what matters is the outcome, model the outcome. &lt;strong&gt;Fine-grained always meant saying precisely what happened. It never meant saying it more often.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;two-events-and-the-bar-still-fills&#34;&gt;Two Events, and the Bar Still Fills&lt;a class=&#34;anchor&#34; href=&#34;#two-events-and-the-bar-still-fills&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Back to the 312. After the change there are two: one that opens the review and names its scope, one that closes it with the findings. Everything in between lives in the process state, is pushed straight to the browser while it&#39;s relevant, and is thrown away when the run ends. &lt;strong&gt;That it came out at two is the result, not the aim&lt;/strong&gt; – the aim was to write down only what someone will ask about later.&lt;/p&gt;&#xA;&lt;p&gt;And the progress bar still fills, exactly as before. &lt;strong&gt;Nothing about the user-facing feature was the problem&lt;/strong&gt; – on a long review it&#39;s the right thing to build, and showing which clause is being examined is a courtesy to someone who would otherwise stare at a spinner. Only its address was ever in question.&lt;/p&gt;&#xA;&lt;p&gt;The question worth carrying away is short enough for a design discussion. Not &lt;em&gt;&amp;quot;Would anyone want to be notified about this specific occurrence?&amp;quot;&lt;/em&gt; – everything someone is waiting for passes that one – but &lt;strong&gt;&amp;quot;will anyone still want to know about this a year from now?&amp;quot;&lt;/strong&gt; Facts that survive it belong in the store. The rest needs a home only for as long as it matters, for a while or not at all, and giving it that place keeps the store worth reading.&lt;/p&gt;&#xA;&lt;p&gt;If you look at your own events and can&#39;t say which of them anyone will still ask about a year from now, we&#39;d be glad to go through them with you – write to us at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/compensating-events-wont-save-you</id>
    <title>Compensating Events Won&#39;t Save You</title>
    <link href="https://www.eventfoundation.io/resources/blog/compensating-events-wont-save-you"></link>
    <published>2026-08-10T00:00:00+02:00</published>
    <updated>2026-08-10T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">A customer calls about their account statement. One line records a payment of 4,800 euros, and nobody at your company can explain where it came from. You look into the event store, and there it is: payment-received, timestamped, hash-chained, immutable. The event is wrong, and it will be wrong forever.</summary>
    <content type="html">&lt;p&gt;A customer calls about their account statement. One line records a payment of 4,800 euros, and nobody at your company can explain where it came from. You look into the event store, and there it is: &lt;code&gt;payment-received&lt;/code&gt;, timestamped, hash-chained, immutable. &lt;strong&gt;The event is wrong, and it will be wrong forever.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Ask around and you will hear the same answer everywhere: write a compensating event – a new event that reverses the effect of an earlier one, the way a refund reverses a payment. It is good advice, and for two of the three situations that produce a wrong event, it is exactly right. &lt;strong&gt;For the third, it makes things worse&lt;/strong&gt; – and the third is the one nobody warns you about.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;facts-dont-lie-records-do&#34;&gt;Facts Don&#39;t Lie, Records Do&lt;a class=&#34;anchor&#34; href=&#34;#facts-dont-lie-records-do&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;There is a sentence that gets repeated so often in Event Sourcing that it has stopped carrying meaning: you cannot change the past. We have said it that way ourselves – &lt;strong&gt;&lt;a href=&#34;/resources/blog/thinking-in-events&#34;&gt;Thinking in Events&lt;/a&gt;&lt;/strong&gt; puts it as &amp;quot;a correction is a new Event, never a changed one&amp;quot; – and as a principle it holds. It just does not hold for the event store alone: &lt;strong&gt;the world is every bit as immutable as your append-only log.&lt;/strong&gt; The customer either transferred that money on a Tuesday in March or they did not, and no amount of engineering will revisit that Tuesday.&lt;/p&gt;&#xA;&lt;p&gt;So if both are equally unchangeable, why does one of them cause trouble? Because immutability is not the interesting difference between them. &lt;strong&gt;A fact simply is. An event is a claim about that fact – and claims can be wrong.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;That distinction sounds academic until you try to fix something. &amp;quot;Can we correct the past?&amp;quot; is unanswerable, and teams burn weeks on it. &lt;strong&gt;&amp;quot;What exactly was wrong?&amp;quot; has three possible answers&lt;/strong&gt;, and each one leads somewhere different. &lt;strong&gt;Either the claim did not match the fact, or the claim matched it and the world moved on, or there was no fact for the claim to match at all.&lt;/strong&gt; Call them the &lt;em&gt;typo&lt;/em&gt;, the &lt;em&gt;reversal&lt;/em&gt;, and the &lt;em&gt;phantom&lt;/em&gt; – the names matter less than the habit of asking which of the three you have.&lt;/p&gt;&#xA;&lt;p&gt;One thing this post is not about: events whose &lt;em&gt;shape&lt;/em&gt; no longer fits. Renaming a field, splitting a payload, turning a string into an array – that is schema evolution, it has a well-worn answer in upcasting, and we covered it in &lt;strong&gt;&lt;a href=&#34;/resources/blog/versioning-events-without-breaking-everything&#34;&gt;Versioning Events Without Breaking Everything&lt;/a&gt;&lt;/strong&gt;. Here, the structure is fine. The content is the problem.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-typo-that-isnt-a-change&#34;&gt;The Typo That Isn&#39;t a Change&lt;a class=&#34;anchor&#34; href=&#34;#the-typo-that-isnt-a-change&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Start with the harmless one, the typo. A support agent registers a new customer and types &lt;code&gt;Meyer&lt;/code&gt;. The man&#39;s name is Meier. The store now holds &lt;code&gt;customer-registered&lt;/code&gt; with a name that was never his.&lt;/p&gt;&#xA;&lt;p&gt;Notice what did not go wrong. &lt;strong&gt;The fact in the world was correct the entire time&lt;/strong&gt; – he has always been Meier, before the registration and after it. Nothing in the business changed, nothing needs to be undone. Only the sentence your system wrote down was false, and the moment someone noticed, that noticing became a fact in its own right. &lt;strong&gt;A correction is not a repair of history; it is a new event about a mistake somebody made.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Which means the domain already has words for it. Somebody spotted a data entry error and fixed it. That is an ordinary business occurrence, and it deserves an ordinary business event: &lt;code&gt;customer-name-corrected&lt;/code&gt;, carrying the right name and, ideally, who noticed. Such an event is written today about something that went wrong earlier, so it carries two different times, and keeping them apart is its own small discipline – we worked through it in &lt;strong&gt;&lt;a href=&#34;/resources/blog/time-is-of-the-essence&#34;&gt;Time is of the Essence&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;What it must not be is &lt;code&gt;customer-name-changed&lt;/code&gt;. That event already means something else: the man got married, or changed his name by deed poll. &lt;strong&gt;Those two events look identical in their payload and mean opposite things&lt;/strong&gt; – one says the world moved, the other says we wrote it down wrong. Flatten them into one type, and you can never tell them apart again.&lt;/p&gt;&#xA;&lt;p&gt;This is not naming pedantry, and the cost shows up in the read models. A projection that lists customers whose legal name changed in the past year – for a reporting obligation, for duplicate detection, for a mailing – needs the marriages and not the typos. &lt;strong&gt;If both arrive as &lt;code&gt;customer-name-changed&lt;/code&gt;, that projection is quietly wrong, and nobody will notice for months.&lt;/strong&gt; We wrote about this pull toward generic verbs in &lt;strong&gt;&lt;a href=&#34;/resources/blog/naming-events-beyond-crud&#34;&gt;Naming Events Beyond CRUD&lt;/a&gt;&lt;/strong&gt;; the correction case is where it bites hardest, because the generic name is so tempting.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;For this case, the standard advice works perfectly.&lt;/strong&gt; Write the correcting event, let the projections apply it, move on.&lt;/p&gt;&#xA;&lt;h2 id=&#34;a-cancellation-is-not-a-correction&#34;&gt;A Cancellation Is Not a Correction&lt;a class=&#34;anchor&#34; href=&#34;#a-cancellation-is-not-a-correction&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The second case, the reversal, is the one teams misfile most often. An order is canceled. A subscription is terminated and later reactivated. A payment is refunded because the customer sent it back.&lt;/p&gt;&#xA;&lt;p&gt;Here the original event was true. &lt;strong&gt;The store said a payment arrived, and a payment did arrive.&lt;/strong&gt; Then the world produced another fact, and the second fact does not retract the first – it follows it. The money came in on Tuesday and went back out on Friday, and both of those things really happened, in that order.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;This is why the question &amp;quot;how do I correct this?&amp;quot; leads nowhere: there is nothing to correct.&lt;/strong&gt; You write &lt;code&gt;order-canceled&lt;/code&gt; or &lt;code&gt;subscription-reactivated&lt;/code&gt;, exactly as you would write any other event, and you are done. &lt;strong&gt;This is the case compensating events are made for&lt;/strong&gt;, and notice that they need no special status here: the compensation is simply the next domain event. We made the same point from the other direction in &lt;strong&gt;&lt;a href=&#34;/resources/blog/soft-delete-is-a-workaround&#34;&gt;Soft Delete Is a Workaround&lt;/a&gt;&lt;/strong&gt;: nothing is removed, so nothing needs restoring, and the reactivation sits in the history right after the cancellation, where it belongs.&lt;/p&gt;&#xA;&lt;p&gt;The failure mode here is not technical, it is conceptual. Teams call these cases corrections, and then they build machinery for them – a correction framework, a reversal flag on the event, a special path through the projections. &lt;strong&gt;All of it is scaffolding around plain domain modeling.&lt;/strong&gt; Worse, the scaffolding blurs the model: once cancellations travel through a correction mechanism, the domain no longer says clearly that canceling is a thing customers do.&lt;/p&gt;&#xA;&lt;p&gt;There is a one-question test that separates a reversal from a typo, and it does not involve any technology. &lt;strong&gt;Would somebody in the business use the word &amp;quot;mistake&amp;quot;?&lt;/strong&gt; A canceled order is not a mistake, it is a customer changing their mind. A misspelled name is a mistake. If the domain expert shrugs and says &amp;quot;yes, that happens all the time&amp;quot;, you are not correcting anything.&lt;/p&gt;&#xA;&lt;h2 id=&#34;when-nothing-happened-at-all&#34;&gt;When Nothing Happened At All&lt;a class=&#34;anchor&#34; href=&#34;#when-nothing-happened-at-all&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Then there is the third case, the phantom, and it is the reason this post exists.&lt;/p&gt;&#xA;&lt;p&gt;A retry loop fires without a precondition and writes 40,000 &lt;code&gt;payment-received&lt;/code&gt; events for payments that were already recorded. A migration script runs against the wrong configuration and creates customers who do not exist. A test fixture escapes into production. &lt;strong&gt;The store now asserts facts that have no counterpart in the world whatsoever.&lt;/strong&gt; Not described inaccurately – not described at all, because there is nothing there to describe.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;That is what makes this case different in kind rather than in degree.&lt;/strong&gt; In the first two cases, you could point at something real when you corrected the record: he is actually called Meier, she actually canceled. &lt;strong&gt;With a phantom, the world has nothing to say.&lt;/strong&gt; No agent mistyped anything. No customer changed their mind. Inside the business, the day passed without event.&lt;/p&gt;&#xA;&lt;p&gt;And that is exactly why the vocabulary runs out. &lt;strong&gt;Domain events describe what happened in the business, so a domain event for &amp;quot;this never happened&amp;quot; is a contradiction.&lt;/strong&gt; Yet the standard advice keeps insisting: write a compensating event. So somebody writes &lt;code&gt;payment-refunded&lt;/code&gt;, forty thousand times.&lt;/p&gt;&#xA;&lt;p&gt;Look at what that claims. It says money went back to the customer. &lt;strong&gt;No money went back, because no money came in.&lt;/strong&gt; You have not corrected a false statement, you have added a second one to cover it – and a second lie does not average out with the first into truth. The store now contains twice as much fiction as before, and both halves look perfectly legitimate to anything reading them.&lt;/p&gt;&#xA;&lt;p&gt;The bill arrives when someone asks questions. An auditor sees 40,000 payments and 40,000 refunds and wants to know what on earth happened here, and the actual answer – nothing, it was a bug – is nowhere in the data. In &lt;strong&gt;&lt;a href=&#34;/resources/blog/you-dont-need-an-audit-log&#34;&gt;You Don&#39;t Need an Audit Log&lt;/a&gt;&lt;/strong&gt; we argued that the events &lt;em&gt;are&lt;/em&gt; the audit trail, which is precisely why this hurts: &lt;strong&gt;you have just written invented business activity into the only record you have.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;The cheapest phantom is the one you never get. Most of them come from writes that should have been rejected and were not, and EventSourcingDB can reject them for you: &lt;code&gt;isSubjectPristine&lt;/code&gt; refuses a write when a subject already has events, and &lt;code&gt;isSubjectOnEventId&lt;/code&gt; refuses one when anything has been appended to that subject since you last read it. Both are evaluated inside the same transaction as the write, so a duplicated retry fails with a &lt;code&gt;409&lt;/code&gt; instead of doubling your history. If you want the details, they are in the documentation on &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/preconditions&#34;&gt;preconditions&lt;/a&gt;&lt;/strong&gt; – a few lines in a request body that keep the events you would otherwise have to disown from ever existing.&lt;/p&gt;&#xA;&lt;h2 id=&#34;whats-left-when-the-vocabulary-runs-out&#34;&gt;What&#39;s Left When the Vocabulary Runs Out&lt;a class=&#34;anchor&#34; href=&#34;#whats-left-when-the-vocabulary-runs-out&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Suppose the 40,000 events already exist. &lt;strong&gt;There is no elegant answer here, only a choice between two awkward ones&lt;/strong&gt;, and pretending otherwise is how teams end up with the refunds.&lt;/p&gt;&#xA;&lt;p&gt;The first option is an event that makes no claim about the world at all, only about the store: these event IDs came from a defect and are not facts. It claims nothing false, which is exactly its appeal, and it keeps the knowledge inside the store where the events are. &lt;strong&gt;The price is that every consumer has to understand it&lt;/strong&gt; – a projection that does not know the type keeps counting revenue that was never earned.&lt;/p&gt;&#xA;&lt;p&gt;The second option leaves the store untouched and fixes the readers: the affected IDs go into a filter, and the projections get rebuilt. Nothing fictional is ever written, which is its great virtue, and rebuilding a projection is routine work rather than an emergency – we walked through one in &lt;strong&gt;&lt;a href=&#34;/resources/blog/the-three-cent-problem&#34;&gt;The Three-Cent Problem&lt;/a&gt;&lt;/strong&gt;. &lt;strong&gt;The price is that the knowledge now lives in application code&lt;/strong&gt;, so every new consumer built next year has to be told about a bug from last year, and eventually one of them will not be.&lt;/p&gt;&#xA;&lt;p&gt;Which one fits depends on how many consumers there are and how long the defect stays relevant. A handful of projections you own, a bounded window of bad events, a rebuild you can run tonight – filtering is enough, and it keeps the store clean. Many consumers, some of them outside your team, or a defect whose window nobody can pin down – then the knowledge belongs in the store, because that is the only place everyone reads.&lt;/p&gt;&#xA;&lt;p&gt;What is not on the menu is rewriting the store. It is tempting, it looks surgical, and with hash-chained events it does not stay hidden: the chain breaks at the point you edited, and the integrity check fails from there on. &lt;strong&gt;You would be trading a data problem for the loss of the one property you bought an event store for&lt;/strong&gt; – the ability to demonstrate that the history is intact, which we described in &lt;strong&gt;&lt;a href=&#34;/resources/blog/proving-without-revealing-merkle-trees&#34;&gt;Proving Without Revealing&lt;/a&gt;&lt;/strong&gt;. There is also a subtler cost, and it is permanent. &lt;strong&gt;Rewrite once, and you can never again prove that you only did it once.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;sorting-the-case-costs-less-than-solving-it&#34;&gt;Sorting the Case Costs Less Than Solving It&lt;a class=&#34;anchor&#34; href=&#34;#sorting-the-case-costs-less-than-solving-it&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Each of the three cases has a workable answer, and none of them is hard. &lt;strong&gt;What is expensive is filing the situation under the wrong case&lt;/strong&gt;, because then you apply a correct solution to the wrong problem and the damage compounds quietly.&lt;/p&gt;&#xA;&lt;p&gt;Treat a reversal as a typo, and you build correction machinery for ordinary business behavior. That one is merely wasteful. Treat a typo as a phantom, and you reach for technical retractions when the domain had a perfectly good word available, losing the useful information that a human being caught an error. &lt;strong&gt;Treat a phantom as a reversal, and you write fiction into your audit trail&lt;/strong&gt; – that is the one that costs you the trust of the record itself.&lt;/p&gt;&#xA;&lt;p&gt;The sorting takes two questions. &lt;strong&gt;Was there a fact in the world that this event was trying to describe?&lt;/strong&gt; If yes, did the event describe it wrongly, or describe it correctly before the world moved on? If no – if nothing happened out there at all – &lt;strong&gt;you are looking at a phantom, and no amount of domain vocabulary will rescue you&lt;/strong&gt;, because there is no domain occurrence to name.&lt;/p&gt;&#xA;&lt;p&gt;Notice that both questions are about the business, not the database. &lt;strong&gt;You cannot answer them by reading the payload&lt;/strong&gt;, which is why this is a modeling conversation and not a debugging session. The event that says 4,800 euros arrived looks identical whether the amount was mistyped, refunded a week later, or conjured by a retry loop.&lt;/p&gt;&#xA;&lt;h2 id=&#34;back-to-the-statement&#34;&gt;Back to the Statement&lt;a class=&#34;anchor&#34; href=&#34;#back-to-the-statement&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;So which one was it? If an agent fat-fingered the amount, write the correction and name it for what it was. If the money did arrive and did go back, there was never anything to fix – the refund is just the next fact in the story. And if no payment ever existed, then the worst thing you can do is invent a refund to balance it out.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;In none of the three cases does the original event change.&lt;/strong&gt; It stays exactly where it is, wrong, forever. What changes is what the system knows about it – and the fact that this knowledge gets appended rather than overwritten is the whole reason anyone can reconstruct, years later, that it was a mistake at all. A system that quietly fixed the number would have left you with a correct statement and no way to explain it.&lt;/p&gt;&#xA;&lt;p&gt;Compensating events are not the villain here. They are the right tool for a real fact that reverses another real fact, and they will serve you well for as long as the world is what went wrong. &lt;strong&gt;They just cannot help you when the world was never involved&lt;/strong&gt; – and knowing which of those two you are looking at is the entire skill.&lt;/p&gt;&#xA;&lt;p&gt;If you are sitting on a phantom right now, with a store full of events you wish had never been written, we would like to hear about it. Write to us at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt; – working out which events are facts and which are artifacts is a conversation we have often, and it is usually a shorter conversation than teams expect.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/someone-built-a-visualizer-for-esdm-and-it-wasnt-us</id>
    <title>Someone Built a Visualizer for ESDM – And It Wasn&#39;t Us</title>
    <link href="https://www.eventfoundation.io/resources/blog/someone-built-a-visualizer-for-esdm-and-it-wasnt-us"></link>
    <published>2026-08-06T00:00:00+02:00</published>
    <updated>2026-08-06T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">In mid-July, we put ESDM under the MIT license and wrote down a sentence that we meant as a principle: because the schema is public, tooling built on ESDM never needs us in the loop. Two weeks later, that sentence turned out to be a prediction instead.</summary>
    <content type="html">&lt;p&gt;In mid-July, we put &lt;strong&gt;&lt;a href=&#34;/resources/blog/esdm-is-now-open-source&#34;&gt;ESDM under the MIT license&lt;/a&gt;&lt;/strong&gt; and wrote down a sentence that we meant as a principle: because the schema is public, tooling built on ESDM never needs us in the loop. &lt;strong&gt;Two weeks later, that sentence turned out to be a prediction instead.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;https://www.impierce.com/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Impierce Technologies&lt;/a&gt;&lt;/strong&gt;, a company in the Netherlands and good friends of ours, built the &lt;strong&gt;&lt;a href=&#34;https://github.com/impierce/esdm-visualizer&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;ESDM Visualizer&lt;/a&gt;&lt;/strong&gt;: an open source web application that turns a folder full of &lt;code&gt;.esdm.yaml&lt;/code&gt; files into a board you can walk through, click into, and share as a link. Nobody asked us, and nobody had to. Here is what it is, what it does, and how to have it running against your own model in about a minute.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;from-files-to-a-board&#34;&gt;From Files to a Board&lt;a class=&#34;anchor&#34; href=&#34;#from-files-to-a-board&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;ESDM, which we &lt;strong&gt;&lt;a href=&#34;/resources/blog/one-year-of-eventsourcingdb-and-the-language-that-belongs-beside-it&#34;&gt;introduced in May&lt;/a&gt;&lt;/strong&gt;, describes an event-sourced domain – Aggregates, Events, Commands, Queries, Read Models, Actors, Bounded Contexts – as plain YAML files that sit next to your code. That is the entire point of the format: &lt;strong&gt;a model you can diff, review in a pull request, and validate in CI, because it is text and not a picture.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;The catch is that text has one weakness a whiteboard doesn&#39;t have. Reading twenty-five files tells you what is in the model, but it doesn&#39;t show you the shape of it. You can&#39;t see at a glance which command feeds which read model, or which aggregate has grown three times as large as its neighbors. &lt;strong&gt;A format that is perfect for machines and reviews is not automatically good at the one thing a model exists for, which is talking about it with other people.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;That is the gap the visualizer closes. Point it at a directory, and it walks that directory recursively, picks up every file ending in &lt;code&gt;.esdm.yaml&lt;/code&gt;, and renders all of them together as a single graph. Actors, commands, events, aggregates, read models, and queries each get their own kind of node, with their own shape and color.&lt;/p&gt;&#xA;&lt;p&gt;The edges are the interesting part, because &lt;strong&gt;the visualizer doesn&#39;t invent a single one of them.&lt;/strong&gt; An arrow from an actor to a command appears because the command lists that actor. An arrow from a command to an event appears because the command declares that it publishes it. Events point at the read models whose projections consume them, and read models point at the queries that read from them. The board doesn&#39;t write the relation next to the line – it doesn&#39;t have to, because the kinds of the two nodes it connects already say which one it is.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Which also means the board is honest about your model in a way a drawing never is.&lt;/strong&gt; If a command claims to publish an event that doesn&#39;t exist in that bounded context, no arrow shows up. Nothing is drawn from memory, nothing is drawn from intention. What you see is what your files actually say.&lt;/p&gt;&#xA;&lt;figure&gt;&#xA;&lt;img src=&#34;/resources/blog/images/esdm-visualizer-ordering-board.png&#34; alt=&#34;The ESDM Visualizer showing the ordering context of the shop example in dark mode&#34; loading=&#34;lazy&#34; /&gt;&#xA;&lt;figcaption&gt;The ordering context of the shop example: two actors, two commands, the events they publish, and the read model and query that follow&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&lt;h2 id=&#34;one-minute-to-your-first-board&#34;&gt;One Minute to Your First Board&lt;a class=&#34;anchor&#34; href=&#34;#one-minute-to-your-first-board&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;There is no build step, no account, and no configuration file. There is one command:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker run -p 3000:3000 -v .:/data impierce/esdm-visualizer&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Run that from the directory that holds your model files, then open &lt;code&gt;http://localhost:3000&lt;/code&gt;. The image is around 44 MB, so the first pull is over before you have finished reading this paragraph. That is the whole setup: the current directory is mounted, every &lt;code&gt;.esdm.yaml&lt;/code&gt; below it is picked up, and nothing is written back. If you would rather hack on the visualizer itself, it is a SvelteKit application built with Bun, and the repository tells you the rest.&lt;/p&gt;&#xA;&lt;p&gt;The repository ships two example models, so you have something to look at before you have written a line of YAML yourself. Clone it, run the command from &lt;code&gt;examples/shop&lt;/code&gt;, and you get the model the screenshot above shows: a &lt;code&gt;shop&lt;/code&gt; domain with two bounded contexts, &lt;code&gt;catalog&lt;/code&gt; and &lt;code&gt;ordering&lt;/code&gt;, a customer and a staff member as actors, orders that get placed and shipped, stock that gets restocked and decreased, and read models with the queries that serve them. The other example is a small library. &lt;strong&gt;It is small enough to hold in your head and large enough to have the problems real models have&lt;/strong&gt;, which makes it a good place to click around for five minutes.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;ESDM models are plain files against a public schema, which means any tool can read them without asking anyone for permission, so going from a folder of YAML to a shared board really is one &lt;code&gt;docker run&lt;/code&gt; away.&lt;/strong&gt; If you don&#39;t have an ESDM model to point it at yet, the fastest way in is the documentation at &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;esdm.io&lt;/a&gt;&lt;/strong&gt;: write a first model, then let the visualizer show you what you just described.&lt;/p&gt;&#xA;&lt;h2 id=&#34;narrowing-the-view&#34;&gt;Narrowing the View&lt;a class=&#34;anchor&#34; href=&#34;#narrowing-the-view&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Any model worth having is too big for one screen, and a graph that shows everything at once shows nothing at all. The visualizer takes that seriously and narrows the board in three steps.&lt;/p&gt;&#xA;&lt;p&gt;The first is a dropdown for the bounded context. Pick one, and everything outside it disappears, which is the coarse cut that turns a whole domain into one team&#39;s territory. The second is a dropdown for the aggregate inside that context, and this is the cut that matters most. &lt;strong&gt;Choose an aggregate, and the board reduces to its slice of the world&lt;/strong&gt;: the commands that act on it, the events they publish, the read models fed by those events, the queries that read from them, and only those actors who actually touch any of it. Everything belonging to a neighboring aggregate is gone, even though it lives in the same context.&lt;/p&gt;&#xA;&lt;p&gt;The third step is the visibility panel, which lists the commands of the selected aggregate as a row of checkboxes, with &amp;quot;All&amp;quot; and &amp;quot;None&amp;quot; to sweep them in one go. Uncheck a command and it leaves the board – &lt;strong&gt;and the events it publishes leave with it&lt;/strong&gt;, unless another visible command publishes them too. On an aggregate with a dozen commands, that is the difference between a wall of boxes and the three commands you actually came to discuss, which is precisely the move you want in an &lt;strong&gt;&lt;a href=&#34;/concepts/event-storming&#34;&gt;Event Storming&lt;/a&gt;&lt;/strong&gt; session where the conversation keeps narrowing.&lt;/p&gt;&#xA;&lt;p&gt;Both dropdown selections live in the URL, which quietly makes every one of those views a link you can paste into a chat. Beyond that, there is a legend that names the node kinds, a dark mode, and an option to animate the edges, which is more useful than it sounds when you are explaining the direction of a flow to someone over a screen share.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-sits-behind-a-node&#34;&gt;What Sits Behind a Node&lt;a class=&#34;anchor&#34; href=&#34;#what-sits-behind-a-node&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Click a node, and a side panel opens with the actual content of that document. This is where the visualizer stops being a diagram tool.&lt;/p&gt;&#xA;&lt;p&gt;For a command or an event, you get every field with its type, its description, and whether it is required. For an aggregate, you get its state and, more importantly, its invariants – the rules that must hold, written out. For a command, you get its constraints in plain language and the list of actors allowed to issue it. Actors themselves tell you whether they are human or a system, what they are responsible for, and which external system backs them. Read models report how many projections feed them, and queries show both their parameters and the shape of their result.&lt;/p&gt;&#xA;&lt;p&gt;Take the &lt;code&gt;order&lt;/code&gt; aggregate from the shop example. The panel shows its state – line items, a shipping address, and a status that is either &lt;code&gt;placed&lt;/code&gt; or &lt;code&gt;shipped&lt;/code&gt; – and next to it the invariant the model calls &lt;code&gt;ships-at-most-once&lt;/code&gt;, spelled out as the sentence that an order transitions from placed to shipped at most once, and that shipping it again has no effect. Click the &lt;code&gt;place-order&lt;/code&gt; command sitting next to it, and you see the shape of the data it carries, that only a customer may issue it, and that it publishes &lt;code&gt;placed&lt;/code&gt;. &lt;strong&gt;Three clicks, and you know not just what the system does, but what it refuses to do.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;So the board isn&#39;t a picture of the model. It is the model, arranged so that you can look at it.&lt;/strong&gt; Zoom out and it is a map. Click once and it is the specification. There is no second artifact that needs to be kept in sync, because there is no second artifact – which is exactly the problem that &lt;strong&gt;&lt;a href=&#34;/resources/blog/continuous-modeling-or-what-happens-to-the-model-on-tuesday&#34;&gt;a model living in a drawing tool&lt;/a&gt;&lt;/strong&gt; never manages to solve.&lt;/p&gt;&#xA;&lt;h2 id=&#34;domain-stories-and-given-when-then&#34;&gt;Domain Stories and Given-When-Then&lt;a class=&#34;anchor&#34; href=&#34;#domain-stories-and-given-when-then&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;ESDM is more than structure, and the visualizer follows it there too.&lt;/p&gt;&#xA;&lt;p&gt;It reads Domain Storytelling documents, which describe a process as numbered sentences: this actor does this to that work object, and here is what happens next. The visualizer surfaces them with their actors, their work objects, and their annotations, along with the metadata ESDM records about each story – whether it describes the world as it is or as it should be, whether it is coarse- or fine-grained, and whether it captures the pure domain or the digitalized version of it. &lt;strong&gt;That last distinction is the one people forget, and it is the difference between modeling a business and modeling last year&#39;s software.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;It also reads Given-When-Then features. Each scenario starts from a set of events that already happened, applies a single trigger – a command, an incoming event, a query, or a timer – and states what must follow. And what follows is either a list of events, or a rejection that names the exact invariant that was violated. &lt;strong&gt;A scenario like that isn&#39;t documentation about the system, it is a statement about the system that can be checked.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;The shop example carries a feature named &lt;code&gt;order-lifecycle&lt;/code&gt; that shows what this feels like in practice. One scenario starts from nothing at all, has a customer issue &lt;code&gt;place-order&lt;/code&gt;, and expects a single &lt;code&gt;placed&lt;/code&gt; event with exactly the line items and the address that went in. The next one starts from that &lt;code&gt;placed&lt;/code&gt; event as its given, has a staff member issue &lt;code&gt;ship-order&lt;/code&gt;, and takes the story one step further. &lt;strong&gt;The aggregate&#39;s invariant said shipping happens at most once; the scenarios are where that claim stops being prose and turns into something with concrete data attached.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Seeing those scenarios next to the graph they belong to closes a loop we spent our &lt;strong&gt;&lt;a href=&#34;/resources/blog/candy-for-ai-an-interview-on-spec-driven-development&#34;&gt;last interview&lt;/a&gt;&lt;/strong&gt; talking about: the model describes the structure, the scenarios pin down the behavior, and both live in the same files, in the same repository, under the same review process as the code they describe.&lt;/p&gt;&#xA;&lt;h2 id=&#34;who-built-this&#34;&gt;Who Built This&lt;a class=&#34;anchor&#34; href=&#34;#who-built-this&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Impierce Technologies builds infrastructure for digital identity – EUDI wallets, verifiable credentials, and the plumbing that makes them work across organizations. It is a domain where the question of who did what, when, and on whose authority is not a reporting requirement bolted on at the end, but the actual subject matter. It is also, unsurprisingly, a domain they model carefully.&lt;/p&gt;&#xA;&lt;p&gt;They are no strangers to Event Sourcing either: they maintain &lt;strong&gt;&lt;a href=&#34;https://github.com/impierce/eventsourcingdb-es&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;eventsourcingdb-es&lt;/a&gt;&lt;/strong&gt;, an EventSourcingDB-backed implementation of the &lt;code&gt;cqrs-es&lt;/code&gt; event store for Rust, in the open and under Apache-2.0. The visualizer carries the same license, which means &lt;strong&gt;you can fork it, extend it, and run it inside your own organization without asking anybody, including us.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;We asked Daniel Mader, CTO of Impierce Technologies, what made them build it:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&amp;quot;We appreciate the deterministic nature and strict guarantees of ESDM for our modeling process. We built a thin UI visualization to bridge the gap during business sessions, purposefully keeping it read-only so that ESDM still remains the single source of truth for our YAML files.&amp;quot;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;the-loop-were-not-in&#34;&gt;The Loop We&#39;re Not In&lt;a class=&#34;anchor&#34; href=&#34;#the-loop-were-not-in&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;When we opened ESDM up, we wrote that a generator, a validator, an editor integration, or an AI-assisted modeler could read and write the format without us being involved. &lt;strong&gt;The ESDM Visualizer is the first tool built on ESDM by someone other than us, and it is the proof that the sentence was more than a nice thing to say.&lt;/strong&gt; We didn&#39;t commission it, we didn&#39;t review it, and we didn&#39;t approve it. We found out about it the way you just did.&lt;/p&gt;&#xA;&lt;p&gt;It is worth saying plainly that this is a young project, sitting at version 0.0.1 and moving quickly. The board currently draws six kinds of node, so the policies that wire two bounded contexts together are in the model but not yet on the screen. It will grow, it will change, and it will have rough edges along the way. &lt;strong&gt;That is what a healthy ecosystem looks like at the beginning&lt;/strong&gt; – not a finished product handed down from a vendor, but somebody solving their own problem well enough that it is worth sharing.&lt;/p&gt;&#xA;&lt;p&gt;Every tool that speaks the same language is one less island in a field that has &lt;strong&gt;&lt;a href=&#34;/resources/blog/too-many-islands-too-few-bridges&#34;&gt;far too many of them&lt;/a&gt;&lt;/strong&gt;. The visualizer is a bridge, and we didn&#39;t have to build it. That is by far the best part.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;So: what are you building on ESDM?&lt;/strong&gt; If you have written a generator, wired the models into your CI, taught an agent to read them, or built something we haven&#39;t thought of yet, we would genuinely love to know. Write to us at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt; – not because we need to be in the loop, but because we would like to watch.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/until-further-notice</id>
    <title>Until Further Notice</title>
    <link href="https://www.eventfoundation.io/resources/blog/until-further-notice"></link>
    <published>2026-08-03T00:00:00+02:00</published>
    <updated>2026-08-03T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">The printout lies on the table between us with one line marked in red. 31,400 euros, approved by N. Prentice, March 27, 14:52.</summary>
    <content type="html">&lt;p&gt;The printout lies on the table between us with one line marked in red. 31,400 euros, approved by N. Prentice, March 27, 14:52.&lt;/p&gt;&#xA;&lt;p&gt;The N stands for Naomi. It is my line, and every number on it is correct.&lt;/p&gt;&#xA;&lt;p&gt;Merrick asks whether I was authorized to do that. I say yes. He asks whether I can prove it.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;march-13-six-months-earlier&#34;&gt;March 13, Six Months Earlier&lt;a class=&#34;anchor&#34; href=&#34;#march-13-six-months-earlier&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Ballard is out. He doesn&#39;t say what it is and I don&#39;t ask. On the phone he sounds like a man who has been told something and has not finished thinking about it yet.&lt;/p&gt;&#xA;&lt;p&gt;In the afternoon HR calls. Could I cover for him. A few weeks, they say, possibly longer.&lt;/p&gt;&#xA;&lt;p&gt;I have been in purchasing here for eleven years. The volume doesn&#39;t frighten me; the part of the job I have never done is the part Ballard keeps for himself, which is sitting across from suppliers who have known him for two decades and telling them no. I say yes anyway, because the alternative is that nobody signs anything for weeks, and nobody can tell me how many.&lt;/p&gt;&#xA;&lt;p&gt;Two days later the memo is in my in-tray, countersigned by Ballard and by the managing director. &lt;em&gt;Deputy Head of Purchasing. Approval limit 50,000 euros. Effective March 13 until further notice.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;The last three words don&#39;t bother me. Nobody knows when he is coming back, so there is no date anyone could honestly write down. I put the memo in the drawer where I keep things I will not need again.&lt;/p&gt;&#xA;&lt;p&gt;On Monday I log in and my profile says 50,000 instead of 10,000. Everything works exactly as it is supposed to.&lt;/p&gt;&#xA;&lt;h2 id=&#34;march-27-1452&#34;&gt;March 27, 14:52&lt;a class=&#34;anchor&#34; href=&#34;#march-27-1452&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Thurlow&#39;s price commitment expires at the end of the month. From April, eighteen percent more on steel bar stock. It is in writing and it is not a negotiating position; I call and it is still not a negotiating position.&lt;/p&gt;&#xA;&lt;p&gt;We need the material in May regardless. So the question is only whether we buy it now at the old price or in five weeks at the new one.&lt;/p&gt;&#xA;&lt;p&gt;I run it twice. Pulling the second-quarter demand forward costs us storage space and about three weeks of tied-up capital, and it saves 4,900 euros. I call Pike in the warehouse and ask whether he can take four pallets earlier than planned. He can, grudgingly, which is how Pike does everything.&lt;/p&gt;&#xA;&lt;p&gt;The order comes to 31,400 euros.&lt;/p&gt;&#xA;&lt;p&gt;Before I approve it I look at my limit, because that is what you do. 50,000. There is no prompt, no second pair of eyes, no note in the margin. Why would there be. At 14:52 I approve PO-4471 and move on to the next line item.&lt;/p&gt;&#xA;&lt;p&gt;It is the kind of decision Ballard would have made in ten seconds and I have taken forty minutes over it. I am mildly pleased with myself for the rest of the afternoon, and then I forget about it entirely for six months.&lt;/p&gt;&#xA;&lt;h2 id=&#34;april&#34;&gt;April&lt;a class=&#34;anchor&#34; href=&#34;#april&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Ballard is back, earlier than expected and thinner. He goes straight into a supplier meeting on his second day, which everyone including me agrees is stupid.&lt;/p&gt;&#xA;&lt;p&gt;My profile says 10,000 again. I see the number and think nothing at all.&lt;/p&gt;&#xA;&lt;h2 id=&#34;may&#34;&gt;May&lt;a class=&#34;anchor&#34; href=&#34;#may&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Reorganization in purchasing: four areas become three, two people move, one team leader position disappears. My role is still called PUR-2. It has a new limit, 7,500 euros.&lt;/p&gt;&#xA;&lt;p&gt;For two days I am annoyed, because I now need Ballard&#39;s signature for parts that cost less than a decent office chair. Then I get used to it, the way you get used to a door that sticks.&lt;/p&gt;&#xA;&lt;p&gt;Nothing has been deleted. The field simply holds a different number now.&lt;/p&gt;&#xA;&lt;h2 id=&#34;september&#34;&gt;September&lt;a class=&#34;anchor&#34; href=&#34;#september&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Internal audit runs an automated check across the year: every order compared against the approval limit of the person who approved it. Six thousand line items. Eleven exceptions.&lt;/p&gt;&#xA;&lt;p&gt;Ten of them are transposition errors, cleared in an afternoon by putting two numbers next to each other.&lt;/p&gt;&#xA;&lt;p&gt;The eleventh needs a date.&lt;/p&gt;&#xA;&lt;p&gt;Merrick is polite about it. He puts the printout on the table with one line in red and asks whether I was authorized. I say yes. He asks whether I can prove it, and I say of course, and that is the last easy sentence of the month.&lt;/p&gt;&#xA;&lt;p&gt;It takes me five days to understand that I cannot.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-the-systems-remembered&#34;&gt;What the Systems Remembered&lt;a class=&#34;anchor&#34; href=&#34;#what-the-systems-remembered&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;The approval log.&lt;/strong&gt; It is complete, unbroken, tamper-evident and retained for seven years. It says: Prentice, approval, PO-4471, March 27, 14:52:16. It says nothing about what my limit was at 14:52. It records what I did. It does not record what I was permitted to do.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The role assignment.&lt;/strong&gt; Doyle from IT shows me the table on his screen. There is one row for me and a column called &lt;code&gt;valid_from&lt;/code&gt;, and it says May 4. I ask where the March row is. He says there is one row per person; it gets changed, not added to. He says it the way you would explain that water is wet, and he is not being unhelpful, he is being accurate.&lt;/p&gt;&#xA;&lt;p&gt;I ask about the roles table itself, the one that holds the limit for PUR-2. Same answer. The field holds 7,500. What it held in March is not stored anywhere, because nobody ever needed it to be.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The transaction log.&lt;/strong&gt; Doyle gets there before I do. It exists, it is exactly what I am looking for, and it is retained for eight days, because its purpose is recovery, not history.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The personnel file.&lt;/strong&gt; The memo is there, scanned as a PDF. &lt;em&gt;Effective March 13 until further notice.&lt;/em&gt; There is no end date, because in March nobody could name one. Which means that on paper I am still deputy head of purchasing today, in September, with a limit of 50,000 euros. This does not help me. It makes the file look sloppy, and a sloppy file is not the thing you want to put in front of internal audit.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-document-that-was-new-every-time&#34;&gt;The Document That Was New Every Time&lt;a class=&#34;anchor&#34; href=&#34;#the-document-that-was-new-every-time&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;The order document.&lt;/strong&gt; This is the one that costs me a night&#39;s sleep, because for about two hours I think I have it.&lt;/p&gt;&#xA;&lt;p&gt;Every purchase order can be printed as a PDF, and the footer of that PDF carries the name and the function of the person who approved it. If the document from March 27 says &lt;em&gt;Deputy Head of Purchasing&lt;/em&gt;, the question is settled.&lt;/p&gt;&#xA;&lt;p&gt;I open PO-4471 and print it and the footer says: N. Prentice, PUR-2.&lt;/p&gt;&#xA;&lt;p&gt;Doyle explains it without being asked, because he can see my face. The document is not stored. It is generated when you ask for it, from the data as it stands at that moment. The order from March is real and unchanged; the sheet of paper describing it is new every time, and it describes March using September.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The backups.&lt;/strong&gt; Daily, overwritten after ninety days, with quarterly snapshots kept indefinitely. So there are exactly two snapshots that bracket March 27. The one from December 31, in which I have a limit of 10,000 and no deputy role at all. And the one from March 30, in which I am deputy head of purchasing with 50,000.&lt;/p&gt;&#xA;&lt;p&gt;Somewhere between those two points my row changed. The March 30 snapshot is three days after the order.&lt;/p&gt;&#xA;&lt;p&gt;Merrick says what it proves, and he says it without any edge, which somehow makes it worse: it proves March 30.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-one-system-that-never-deleted-anything&#34;&gt;The One System That Never Deleted Anything&lt;a class=&#34;anchor&#34; href=&#34;#the-one-system-that-never-deleted-anything&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;The mail archive.&lt;/strong&gt; It is journaled, it has never overwritten anything, and it goes back eight years. It has Thurlow&#39;s letter with the price and the deadline. It has a thread from mid-March in which the managing director&#39;s assistant asks me to take over the Thurlow negotiation while Ballard is out.&lt;/p&gt;&#xA;&lt;p&gt;Nobody writes approval limits in emails.&lt;/p&gt;&#xA;&lt;p&gt;So the one system in this building that has never destroyed anything holds the reason, and the reason is not what I am being asked about.&lt;/p&gt;&#xA;&lt;p&gt;Ballard confirms all of it. He ordered the delegation himself, he remembers the conversation, he remembers signing. So there are now two people who remember the same thing correctly and in detail, and it is worth nothing, because it is testimony.&lt;/p&gt;&#xA;&lt;p&gt;Joanne in finance tells me to stop, that nobody thinks I did anything wrong, that these things go into reports all the time and are never read again. She means it kindly and she is probably right about the reading.&lt;/p&gt;&#xA;&lt;p&gt;The trouble is what the finding is attached to. It is not attached to a role assignment table with one row per person. It is attached to my name, in a document with my employee number on it, and the sentence next to it will say that the matter could not be resolved. I would like it to say something else.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-sheet-of-a4&#34;&gt;The Sheet of A4&lt;a class=&#34;anchor&#34; href=&#34;#the-sheet-of-a4&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;For the second meeting I bring a sheet of A4 with a timeline on it, written by hand, dates down the left margin. March 13, the phone call. March 15, the memo. March 16, the login with the new limit. March 24, Thurlow&#39;s letter. March 27, the order. April, Ballard&#39;s return. May, the reorganization. I built it from my calendar, the memo, the mail archive and the order itself, and it took me most of a Sunday.&lt;/p&gt;&#xA;&lt;p&gt;Merrick reads it properly, all the way down, which I appreciate. He says it is helpful.&lt;/p&gt;&#xA;&lt;p&gt;Then he says: it is your account of events.&lt;/p&gt;&#xA;&lt;p&gt;I tell him about the price commitment, about the eighteen percent, about the 4,900 euros. He listens and he does not write it down, and he is right not to, because the check compares two numbers and there is no field anywhere in it for why a person decided something.&lt;/p&gt;&#xA;&lt;p&gt;At the end he asks whether this has happened before.&lt;/p&gt;&#xA;&lt;p&gt;I tell him that to answer that, someone would have to do for six thousand orders what the two of us have just failed to do for one.&lt;/p&gt;&#xA;&lt;p&gt;The report says: &lt;em&gt;not conclusively determinable.&lt;/em&gt; Recommendation to revise the authorization concept, with a note on the documentation of temporary delegations. It is not an accusation. It is also not an exoneration. It will sit in the file, and the next auditor will read it before they meet me.&lt;/p&gt;&#xA;&lt;p&gt;Merrick is friendly when he leaves. That is the part that stays.&lt;/p&gt;&#xA;&lt;h2 id=&#34;october&#34;&gt;October&lt;a class=&#34;anchor&#34; href=&#34;#october&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Since then I have kept a folder. On the first working day of every month I take a screenshot of my own profile, with the limit and the date visible, and save it as a PNG with the date in the filename. It takes eleven seconds.&lt;/p&gt;&#xA;&lt;p&gt;Doyle would tell me it is redundant. He would be right, in the sense that every number in it is already in the system somewhere. It is also not evidence.&lt;/p&gt;&#xA;&lt;p&gt;It is just the only thing I could think of.&lt;/p&gt;&#xA;&lt;h2 id=&#34;nobody-cut-a-corner&#34;&gt;Nobody Cut a Corner&lt;a class=&#34;anchor&#34; href=&#34;#nobody-cut-a-corner&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Nobody in this story cut a corner.&lt;/p&gt;&#xA;&lt;p&gt;There is an approval log, and it is a good one. There is a validity date on the role assignment. There are backups with a sensible retention policy. There is a signed memo in the personnel file. Every number was correct at the moment it was written, and every change was made by the right person for the right reason.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The information did not disappear through a failure. It disappeared through normal operation.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;One question before you go: was Naomi Prentice authorized, at 14:52 on March 27, to approve 31,400 euros?&lt;/p&gt;&#xA;&lt;p&gt;You didn&#39;t have to think about it.&lt;/p&gt;&#xA;&lt;h2 id=&#34;you-were-the-event-store&#34;&gt;You Were the Event Store&lt;a class=&#34;anchor&#34; href=&#34;#you-were-the-event-store&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;You have known almost from the beginning. You know the date it started, you know the number, you know who signed. You know something that never entered the audit at all, which is why she placed the order. And if you scrolled back up to be certain before you answered – that was the entire exercise.&lt;/p&gt;&#xA;&lt;p&gt;Note that you had &lt;strong&gt;less&lt;/strong&gt; data than the company did. No ERP, no approval log, no snapshots, no personnel file. What you had was a sequence of sentences in the past tense, in the order in which things happened, and &lt;strong&gt;not one of them overwrote the one before it&lt;/strong&gt;. You did not hold a state. &lt;strong&gt;You held a sequence, and you computed the answer at the moment the question arrived&lt;/strong&gt;, rather than months earlier.&lt;/p&gt;&#xA;&lt;p&gt;That is the whole difference.&lt;/p&gt;&#xA;&lt;h2 id=&#34;update-is-a-small-deletion&#34;&gt;&lt;code&gt;UPDATE&lt;/code&gt; Is a Small Deletion&lt;a class=&#34;anchor&#34; href=&#34;#update-is-a-small-deletion&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;State is already the answer to a question somebody asked long ago: &lt;em&gt;what is this person allowed to do today?&lt;/em&gt; For that question, &lt;code&gt;UPDATE&lt;/code&gt; is the correct operation, and one row per person is the correct design. It is also, at the same time, a small deletion. &lt;strong&gt;It removes the answers to every question nobody has asked yet.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;That is the uncomfortable part. Dropping a row at least looks like destruction, which is why &lt;strong&gt;&lt;a href=&#34;/resources/blog/soft-delete-is-a-workaround&#34;&gt;soft delete feels like the responsible alternative&lt;/a&gt;&lt;/strong&gt; and mostly is not. Overwriting a field does not look like anything at all. It looks like Tuesday. The same instinct that turns a lifecycle into &lt;strong&gt;&lt;a href=&#34;/resources/blog/your-aggregate-is-not-a-table&#34;&gt;a single row you keep editing&lt;/a&gt;&lt;/strong&gt; is what left Doyle with nothing to show Merrick, and neither Doyle nor the person who designed that table did anything wrong.&lt;/p&gt;&#xA;&lt;p&gt;Nor would a separate audit log have saved her. Hers was excellent, and it still answered the wrong question: it recorded actions faithfully and said nothing about the state those actions were taken against. We took that argument apart in &lt;strong&gt;&lt;a href=&#34;/resources/blog/you-dont-need-an-audit-log&#34;&gt;You Don&#39;t Need an Audit Log&lt;/a&gt;&lt;/strong&gt;. Merrick&#39;s five days are what it looks like from the inside.&lt;/p&gt;&#xA;&lt;p&gt;The most complete record in that building is a human being. Naomi&#39;s memory is ordered, it is only ever appended to, and it knows intent. It has exactly one property that makes it useless: &lt;strong&gt;it does not count as proof&lt;/strong&gt;. Every field that takes investigation seriously has learned this the expensive way – &lt;strong&gt;&lt;a href=&#34;/resources/blog/what-aviation-teaches-us-about-auditing&#34;&gt;aviation&lt;/a&gt;&lt;/strong&gt; does not ask the crew what the aircraft was doing, it reads the recorder.&lt;/p&gt;&#xA;&lt;p&gt;The second most complete record is the mail archive, which nobody ever designed to be a system of record and which behaves like one anyway, for an entirely unglamorous reason. &lt;strong&gt;Nothing in it is ever changed in place.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;there-are-other-ways-to-lose-information&#34;&gt;There Are Other Ways to Lose Information&lt;a class=&#34;anchor&#34; href=&#34;#there-are-other-ways-to-lose-information&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;What was missing in this story was state at a point in the past. That is one way to lose information, and it is the one that ends up in audit reports. There are others, quieter ones, and this is the first post in a new series about them.&lt;/p&gt;&#xA;&lt;p&gt;The companies and the people in these stories are invented. The failure modes are not. Every one of them comes from a system doing exactly what it was built to do, which is what makes them so hard to see coming.&lt;/p&gt;&#xA;&lt;p&gt;If Naomi&#39;s five days sound familiar, the remedy is less dramatic than the problem. &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/installing-eventsourcingdb&#34;&gt;EventSourcingDB&lt;/a&gt;&lt;/strong&gt; stores what happened instead of what is true right now, and reading a stream up to a chosen point in the past is an ordinary read, not an excavation. That means September&#39;s question – what applied at 14:52 on March 27 – is answered from the same data as everything else, without anyone in March having to anticipate that it would ever be asked. Our guide on &lt;strong&gt;&lt;a href=&#34;/concepts/temporal-queries&#34;&gt;patterns for temporal queries&lt;/a&gt;&lt;/strong&gt; shows what that looks like in practice.&lt;/p&gt;&#xA;&lt;p&gt;And if you have a story of your own, a question your systems could not answer in hindsight no matter how carefully they were built, we&#39;d like to read it. Write to us at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt;. Some of them may well end up here, suitably disguised.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/candy-for-ai-an-interview-on-spec-driven-development</id>
    <title>Candy for AI: An Interview on Spec-Driven Development</title>
    <link href="https://www.eventfoundation.io/resources/blog/candy-for-ai-an-interview-on-spec-driven-development"></link>
    <published>2026-07-30T00:00:00+02:00</published>
    <updated>2026-07-30T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <author>
      <name>Martin Dilger</name>
    </author>
    <summary type="text">Golo: Martin, you&#39;re the founder of Nebulit and the author of Understanding Eventsourcing, a book that deliberately refuses to treat Event Modeling, Event Sourcing, and Vertical Slice Architecture as three separate topics, and instead weaves them into a single, coherent way of building software. You also co-host the Event Modeling and Event Sourcing podcast, so you spend a lot of your time thinking out loud about this space. Over the last year, though, your attention has visibly shifted toward something new: getting AI agents to turn an event model into running, production-grade code, with as little human support as possible. Before we open the hood, take us back to the moment this clicked for you. What made you point your attention at autonomous code generation, and what convinced you it was more than the usual AI hype?</summary>
    <content type="html">&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Martin, you&#39;re the founder of Nebulit and the author of &lt;strong&gt;&lt;a href=&#34;https://eventsourcingbook.com/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Understanding Eventsourcing&lt;/a&gt;&lt;/strong&gt;, a book that deliberately refuses to treat Event Modeling, Event Sourcing, and Vertical Slice Architecture as three separate topics, and instead weaves them into a single, coherent way of building software. You also co-host the Event Modeling and Event Sourcing podcast, so you spend a lot of your time thinking out loud about this space. Over the last year, though, your attention has visibly shifted toward something new: getting AI agents to turn an event model into running, production-grade code, with as little human support as possible. Before we open the hood, take us back to the moment this clicked for you. What made you point your attention at autonomous code generation, and what convinced you it was more than the usual AI hype?&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; Honestly, I saw pretty early that using AI to build slices was an almost perfect match – the size and shape just fit. But the real &amp;quot;aha&amp;quot; moment happened during a workshop we ran in October 2025, a public online session for Nebulit where I was teaching Event Modeling and Event Sourcing, and how slices let you build software in a predictable way, AI included. After everyone left, I did what I always do: went in to clean up the model, adding a few comments here and there. What I hadn&#39;t realized was that the agent was still running in the background. Every change I&#39;d made to the board had been translated into running code, essentially in real time. That was a real &amp;quot;eureka&amp;quot; moment for me. Most engineers still think about AI as: write the spec, hand it to an agent, review the output – basically what we always did, just with AI doing the typing. What I stumbled into was different. There was no handover at all. I was working with the agent in real time, and it kept up.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; We&#39;ve dabbled at the edges of this ourselves. We taught &lt;strong&gt;&lt;a href=&#34;/resources/blog/a-skill-for-easter-teaching-claude-to-speak-eventsourcingdb&#34;&gt;Claude to speak EventSourcingDB&lt;/a&gt;&lt;/strong&gt;, and we&#39;ve written about how Event Sourcing gives you a place to &lt;strong&gt;&lt;a href=&#34;/resources/blog/training-ai-without-the-data-you-dont-have&#34;&gt;train AI on data you don&#39;t have yet&lt;/a&gt;&lt;/strong&gt;. But there&#39;s a more fundamental claim hiding in your work: that it isn&#39;t enough to let an agent write &lt;em&gt;any&lt;/em&gt; code, and that the architecture underneath matters enormously for how well an agent performs. Why Event Sourcing, and why slices? What is it about this particular shape of system that makes the difference between an agent that helps and an agent that quietly makes a mess?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; Slices are decoupled – that&#39;s really the whole story. They&#39;re what microservices always should have been: small, isolated business capabilities, small enough to fit into a single head without any preparation. That size and that decoupling are what make the difference. An agent working on a slice needs to look at that slice, and nothing else. There&#39;s no shared understanding scattered across the codebase it has to reconstruct first, no hidden coupling it can accidentally trip over. Compare that to pointing an agent at a broad service or a sprawling aggregate: now it needs a mental model of everything that touches that aggregate, every implicit rule, every other consumer. That&#39;s exactly where agents start guessing, and guessing is where the mess starts.&lt;/p&gt;&#xA;&lt;p&gt;And this isn&#39;t just a theoretical nicety, there&#39;s a real cost benefit behind it. The context you need to understand one slice can be laughably small. Which means you typically don&#39;t need frontier models for this work at all – small models, even local ones, are enough. That&#39;s what makes local AI a genuine game-changer in this approach: once the context is that small, you&#39;re no longer forced into paying frontier-model prices just to get a slice built correctly.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-a-slice-is-candy-for-ai&#34;&gt;Why a Slice Is Candy for AI&lt;a class=&#34;anchor&#34; href=&#34;#why-a-slice-is-candy-for-ai&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; You&#39;ve put it memorably: a vertical slice is &amp;quot;candy for AI.&amp;quot; That phrase has stuck with me, because it inverts the usual worry that AI can&#39;t handle real systems. Your point seems to be that the problem was never the AI, it was the size and shape of what we asked it to chew on. Can you unpack that? Why is the minimal scope of a single slice exactly the right bite for an agent, where a broad service or a sprawling aggregate is not?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; Exactly that – the problem was never the model, it was the bite size. A slice is small enough that an agent doesn&#39;t need any prior context to reason about it correctly. No spelunking through the rest of the codebase, no reverse-engineering of &amp;quot;how did we do it three services over.&amp;quot; The isolation is the point: change one slice, and nothing outside it needs to move. A broad service or a big aggregate doesn&#39;t give you that guarantee. The agent either has to hold the whole thing in its head – which is exactly what it&#39;s bad at – or it makes a local change that quietly breaks an assumption three files away that it never saw. Slices remove that risk almost by construction.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Part of what makes a slice tractable, I suspect, is that the vocabulary around it is precise. A command is an intent, an event is a fact, and we&#39;ve argued before that &lt;strong&gt;&lt;a href=&#34;/resources/blog/commands-arent-just-events-in-reverse&#34;&gt;commands aren&#39;t just events in reverse&lt;/a&gt;&lt;/strong&gt;. We&#39;ve also made the case that &lt;strong&gt;&lt;a href=&#34;/resources/blog/your-aggregate-is-not-a-table&#34;&gt;your aggregate is not a table&lt;/a&gt;&lt;/strong&gt;. When an agent works against that kind of explicit, well-named structure rather than against a tangle of CRUD operations, what changes in practice? Does the rigor of the model actually constrain the AI in a helpful way?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; It does, and the mechanism is very concrete for us: every slice comes with its business rules expressed as Given-When-Then scenarios. Those aren&#39;t just documentation – they translate directly into executable specifications. So the vocabulary isn&#39;t just precise for the sake of precision; it defines the perfect guardrails. The agent isn&#39;t free-styling against a vague &amp;quot;update this record&amp;quot; instruction, it&#39;s building against a scenario that says exactly what facts go in and what fact must come out. That&#39;s a constraint an agent can&#39;t argue its way around, and it&#39;s also one it can verify itself against.&lt;/p&gt;&#xA;&lt;h2 id=&#34;inside-the-loop&#34;&gt;Inside the Loop&lt;a class=&#34;anchor&#34; href=&#34;#inside-the-loop&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Let&#39;s get concrete, because I think people imagine &amp;quot;AI writes the code&amp;quot; as a single magic prompt, and that&#39;s not what you&#39;re doing at all. You&#39;ve built a loop that keeps iterating until every slice in a model is implemented. Walk us through it step by step. What actually happens, from the moment a slice is marked as planned to the moment it&#39;s done and verified?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; The first step is always modeling – extracting the domain knowledge and putting it on the board. Once a slice is modeled, you move it to status &amp;quot;planned.&amp;quot; If an agent is connected to that board, that status change is literally what wakes it up and triggers the process. The agent then builds the slice using the skills we&#39;ve provided for that stack. We maintain an ever-growing library of these skills, one set per supported stack, and quite often the people maintaining a given skill are the framework owners themselves. So the agent isn&#39;t inventing an approach from scratch – it&#39;s following a known, curated recipe for &amp;quot;how you build a slice in this stack,&amp;quot; constrained by the Given-When-Then scenarios that came out of modeling.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Loops like this tend to teach you things you didn&#39;t expect, both delightful and painful. What surprised you most while building and running it? And just as importantly: where does it break? How does the loop know the difference between &amp;quot;this slice is genuinely finished&amp;quot; and &amp;quot;I&#39;m stuck and going in circles,&amp;quot; and what happens when it gets that judgment wrong?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; The delightful surprise is that running this as a loop makes a real difference in practice – agents are effectively running 24/7, just waiting for the next slice to move to &amp;quot;planned.&amp;quot; The agent works within its guardrails: the Given-When-Then scenarios, a hard rule against touching anything outside its own slice, and the skills for the stack it&#39;s building in. The result is that the code it produces is dead boring – which, for once, is exactly the compliment you want to pay generated code. As for knowing when it&#39;s done: in practice, a slice is usually implemented within one loop iteration, with tests sometimes following in a second. But by the second iteration at the latest, it&#39;s typically finished. The tests are the actual completion signal – not the agent&#39;s own sense of confidence.&lt;/p&gt;&#xA;&lt;h2 id=&#34;specifications-as-the-source-of-truth&#34;&gt;Specifications as the Source of Truth&lt;a class=&#34;anchor&#34; href=&#34;#specifications-as-the-source-of-truth&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Underneath all of this sits a quieter but, I think, more radical idea: that the event model shouldn&#39;t just be a picture on a wall, but a machine-readable contract, a specification an agent and a human can both read without ambiguity. We&#39;ve been chasing the same idea from our side with &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;ESDM&lt;/a&gt;&lt;/strong&gt;, short for Event-Sourced Domain Modeling, which describes an event-sourced domain – aggregates, events, commands, process managers, read models, right down to Given-When-Then specifications – as plain YAML, with the tooling to manage it. You arrived at a JSON-Schema spec; we landed on YAML. That two teams reached independently for a machine-readable contract feels like more than coincidence. What do you gain when the model becomes a contract rather than a drawing, and what did you have to get right for that contract to actually be trustworthy?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; Having the model on a wall was always a good starting point for discovery – but it&#39;s horrible for everything that follows. Discovery alone simply isn&#39;t enough; you need more, and that &amp;quot;more&amp;quot; is a machine-readable contract. Concretely, the JSON-Schema spec is what lets me put guardrails on the agent&#39;s own output: I can verify that no additional tests were invented beyond what the model defines, and that every scenario the model does define has actually been implemented. That&#39;s not something you can check against a picture. The contract has to be exhaustive and unambiguous enough that &amp;quot;done&amp;quot; becomes a computable fact, not a judgment call.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; One thing I love about this is that the specification isn&#39;t a separate artifact you maintain out of guilt; it can become the test. We&#39;ve written about &lt;strong&gt;&lt;a href=&#34;/resources/blog/testing-without-mocks&#34;&gt;testing without mocks&lt;/a&gt;&lt;/strong&gt;, where a test reads almost like a sentence in the domain language: given these events, when this command, then these events. When a Given-When-Then specification is the thing the agent builds against &lt;em&gt;and&lt;/em&gt; the thing that proves it built the right thing, where does human judgment stay in the loop? What&#39;s the part you&#39;re not willing to hand over?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; The part I keep is deciding what the scenarios are in the first place – the model itself is where the thinking happens, and that stays with the human and the modeling process, not the agent alone. Once that&#39;s fixed, the agent&#39;s job is verification against a boundary it didn&#39;t set: it can&#39;t sneak in extra tests, and it has to satisfy every one that&#39;s defined. That&#39;s the line for me – the agent becomes a tool, not a thinker.&lt;/p&gt;&#xA;&lt;h2 id=&#34;modernizing-what-you-cant-see&#34;&gt;Modernizing What You Can&#39;t See&lt;a class=&#34;anchor&#34; href=&#34;#modernizing-what-you-cant-see&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; So far we&#39;ve talked about greenfield, where you start from a clean model. But most of the world&#39;s software is anything but clean, and you&#39;ve made a point I keep quoting: you cannot modernize what you don&#39;t understand. You&#39;ve experimented with pointing AI at large, legacy codebases to recover an event model from them, essentially reverse-engineering the intent that got buried over the years. How does that actually work, and just as importantly, how far can you trust the model that comes back out?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; You need to extract as much knowledge as possible, and you do it in layers. First, you look at the API endpoints and the persistence: how does data get into the system, how does it leave, and how is it actually stored. That gives you the skeleton. Then you add more sources of truth on top of that. What&#39;s worked exceptionally well for us is pointing an agent at an existing UI and having it explore the business concepts through the interface itself – the UI often reveals intent that the code alone has buried under years of implementation detail. It&#39;s not a single pass; it&#39;s discovery layered on discovery, each source correcting and enriching the last.&lt;/p&gt;&#xA;&lt;h2 id=&#34;keeping-control-at-ai-speed&#34;&gt;Keeping Control at AI Speed&lt;a class=&#34;anchor&#34; href=&#34;#keeping-control-at-ai-speed&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Here&#39;s the worry I&#39;d put to you bluntly. If agents can produce code this fast, the obvious risk is that we simply generate technical debt faster than ever, with architectural decisions made implicitly and at a pace no human is reviewing. How do you keep genuine architectural control at AI speed? What stops this from becoming a very efficient way to dig a very deep hole?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; The slice approach already forces a certain structure before a single line of code gets written. AI is used to generate and fill in the templates that structure defines – the actual thinking happened before, during modeling. So the architectural decisions aren&#39;t being made implicitly at generation time at all; they were made explicitly, upstream, when the slice was modeled and its scenarios were written. By the time the agent is involved, there&#39;s no architecture left to improvise.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; I want to push on one of your more contrarian positions, because it goes against everything most of us were taught. Within a slice, you deliberately reject DRY and shared abstraction; you&#39;d rather copy and paste than couple two slices through a clever abstraction. To a lot of engineers that sounds like heresy. Make the case. Why is the reflex toward reuse actively harmful here, and how does an agent change that calculus, if at all?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; Because the moment you introduce a shared abstraction between two slices, you&#39;ve reintroduced exactly the coupling the whole approach exists to remove. The structure the slice model forces is the control mechanism – AI just fills in the templates that structure gives it, and the thinking already happened upstream. Reuse across slices means an agent working on slice A now has to understand slice B&#39;s abstraction too, which is precisely the &amp;quot;look at nothing else&amp;quot; guarantee I don&#39;t want to give up. Copy-paste keeps each slice boring and self-contained, and boring is what you want an agent building against.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; And the flip side: what should never be delegated to an agent? Where, in your view, is the line that a human has to stay on the right side of, no matter how good the tooling gets?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; Verification. You need static checks, and you cannot have an agent review an agent – that&#39;s not a control, that&#39;s theater. I use bash scripts or MCP servers for this; honestly, anything static and deterministic works. The line is: an agent can produce, but something non-agentic has to be the judge of whether what it produced is correct.&lt;/p&gt;&#xA;&lt;h2 id=&#34;where-this-is-heading&#34;&gt;Where This Is Heading&lt;a class=&#34;anchor&#34; href=&#34;#where-this-is-heading&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; We&#39;ve written that &lt;strong&gt;&lt;a href=&#34;/resources/blog/great-minds-should-not-think-alike-they-should-think-together&#34;&gt;great minds should not think alike, they should think together&lt;/a&gt;&lt;/strong&gt;, and a lot of this conversation lives exactly at the seam between a model, a database, and an agent. Looking out two years or so: where do you think autonomous coding actually lands? And from where you sit, what does an event store need to bring to the table so that agents can build well on top of it, rather than fighting it?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; Something I&#39;m actively working toward is what I call &amp;quot;model as you speak&amp;quot;: a conversation between two humans, parsed by AI in real time, building the event model and the system as they talk – running by the time the conversation ends. That&#39;s where I think this heads. On the event store side, we&#39;re already seeing a preview of what&#39;s needed: one client is running agents directly on top of anonymized event streams, and whenever something goes wrong, agents can act autonomously to figure out what happened just by reading the stream. It works exceptionally well. So event stores need to support that kind of direct, structured access for agents going forward – likely through something like MCP – rather than assuming a human is always the one querying.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Finally, the practical question. Suppose someone reads this and wants to try this for themselves tomorrow, not the whole vision, just a first honest experiment. Where would you tell them to start, and what&#39;s the smallest thing they could do that would actually teach them something?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Martin:&lt;/strong&gt; Take your own system, use an agent, and model one single use case. You can literally load the skills, point the agent to the code base and have it modeling within less than 15 minutes. Just try it and see how it feels. This doesn&#39;t need to be a big commitment – fifteen minutes is enough to learn something real.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Martin, thank you so much for taking the time to walk us through this. I&#39;m genuinely curious to see how far this goes, and I suspect we&#39;ll be revisiting it sooner than we think.&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;Martin Dilger is the founder and CEO of Nebulit GmbH and the author of Understanding Eventsourcing. For more information or to get in touch, visit the &lt;strong&gt;&lt;a href=&#34;https://eventsourcingbook.com/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Understanding Eventsourcing&lt;/a&gt;&lt;/strong&gt; website.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/you-dont-need-an-audit-log</id>
    <title>You Don&#39;t Need an Audit Log</title>
    <link href="https://www.eventfoundation.io/resources/blog/you-dont-need-an-audit-log"></link>
    <published>2026-07-27T00:00:00+02:00</published>
    <updated>2026-07-27T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">Sooner or later, every serious system meets an auditor. Compliance, regulation, or plain good governance shows up and asks for the same thing: a complete, ordered, tamper-proof record of what happened and when. And almost every architecture review answers with the same reflex. Add an audit log. It has the right name, it produces something you can point at, and it feels responsible. In most systems, it is also the wrong answer.</summary>
    <content type="html">&lt;p&gt;Sooner or later, every serious system meets an auditor. Compliance, regulation, or plain good governance shows up and asks for the same thing: a complete, ordered, tamper-proof record of what happened and when. And almost every architecture review answers with the same reflex. &lt;strong&gt;Add an audit log.&lt;/strong&gt; It has the right name, it produces something you can point at, and it feels responsible. In most systems, it is also the wrong answer.&lt;/p&gt;&#xA;&lt;p&gt;This is not a case against auditing. Auditing matters, often more than the features built around it. It is a case against the &lt;em&gt;separate audit log&lt;/em&gt; as the way to deliver it, because a good auditor, one who actually does the job, will take about one meeting to see through it. The reason they can is the same reason the audit log was compromised from the start, and it reaches all the way down to how you decided to store your data in the first place.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;what-compliance-actually-asks-for&#34;&gt;What Compliance Actually Asks For&lt;a class=&#34;anchor&#34; href=&#34;#what-compliance-actually-asks-for&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Strip away the jargon and a compliance requirement usually comes down to three things. Auditors want to &lt;strong&gt;reconstruct what happened&lt;/strong&gt;, in the order it happened. They want to &lt;strong&gt;reconstruct the state of the system&lt;/strong&gt; at any past moment, not just the state you have today. And above all, they want to &lt;strong&gt;prove that none of it was altered after the fact&lt;/strong&gt;. In finance, healthcare, and public services, this is not a nice-to-have; it is a legal obligation with real consequences when it fails.&lt;/p&gt;&#xA;&lt;p&gt;Picture the concrete version of that. A regulator asks what a particular account looked like on a given day two years ago, and how it got from there to where it is now. Answering means replaying every change in order and convincing a skeptical outsider that the replay is faithful and complete. Auditing, at its core, is about trusting a record you did not personally watch being made, a theme we explored through a different lens in &lt;strong&gt;&lt;a href=&#34;/resources/blog/what-aviation-teaches-us-about-auditing&#34;&gt;What Aviation Teaches Us About Auditing&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Faced with that, the instinct is almost mechanical. You already have a database full of current state, so you bolt a log onto the side of it. Every time the system changes something, it also appends a row somewhere: who did it, what changed, when. Ordering gets a timestamp column. Immutability gets a promise that nobody will ever run an &lt;code&gt;UPDATE&lt;/code&gt; on the log table.&lt;/p&gt;&#xA;&lt;p&gt;It feels responsible, and it produces an artifact you can hand over. When the auditor arrives, you have something to show. That something is exactly where the trouble starts.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-question-a-good-auditor-asks&#34;&gt;The Question a Good Auditor Asks&lt;a class=&#34;anchor&#34; href=&#34;#the-question-a-good-auditor-asks&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;A weak auditor asks whether you have an audit log. A good one asks a harder question: &lt;strong&gt;how do you know the audit log matches what actually happened to the data?&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;That question is fatal, and it is worth sitting with why. Your business data lives in one place, the tables that hold current state, and your audit log lives in another. They are &lt;strong&gt;two separate artifacts&lt;/strong&gt;, produced by two separate writes, and nothing in the structure of the system forces them to agree. The log claims to describe the data. Whether it actually does is a matter of faith.&lt;/p&gt;&#xA;&lt;p&gt;You can show the auditor the log. What you cannot do is &lt;strong&gt;prove that it is complete and faithful&lt;/strong&gt;, that every change to the data produced exactly one truthful entry and that nothing slipped through unrecorded. The burden of proof sits with you, and a separate log does not carry it. You are effectively asking the auditor to trust that two things which &lt;em&gt;can&lt;/em&gt; disagree happen to agree, everywhere, always, going back years.&lt;/p&gt;&#xA;&lt;h2 id=&#34;how-the-log-drifts-from-reality&#34;&gt;How the Log Drifts from Reality&lt;a class=&#34;anchor&#34; href=&#34;#how-the-log-drifts-from-reality&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Once you accept that the log and the data can diverge, the interesting question is how. There are three ways, and every long-lived system eventually meets at least one.&lt;/p&gt;&#xA;&lt;p&gt;The first is &lt;strong&gt;malice&lt;/strong&gt;. The whole point of an audit trail is to catch people doing things they should not. But whoever can write the business data can usually write, or skip, the matching log entry. The person you are auditing against is often the person holding the pen. A record you can quietly edit protects no one.&lt;/p&gt;&#xA;&lt;p&gt;The second is &lt;strong&gt;accident&lt;/strong&gt;. Somewhere in the code there is a path that changes state but forgets to write the log. Perhaps an exception fires between the state write and the log write, so only one of them lands. Perhaps a batch job updates rows directly. The log does not announce that it missed something; it simply develops a hole, and the hole looks exactly like nothing having happened.&lt;/p&gt;&#xA;&lt;p&gt;The third, and the most insidious, is &lt;strong&gt;drift over time&lt;/strong&gt;. On the day it was written, the code logged the right thing. Then a refactor moved the logic, and the log call did not come along. Then a new feature added a second way to change the same data, through a path that predates the logging convention entirely. Each change is reasonable on its own. Together they turn yesterday&#39;s faithful log into today&#39;s incomplete one, and nobody notices until the audit shines a light on the gap.&lt;/p&gt;&#xA;&lt;p&gt;What ties all three together is a single fact: &lt;strong&gt;the log is a second write, kept correct by discipline rather than by construction.&lt;/strong&gt; Anything that depends on discipline, on every developer and every code path, forever, eventually fails. It is the same trap as the dual-write problem behind the outbox pattern, which we took apart in &lt;strong&gt;&lt;a href=&#34;/resources/blog/you-dont-need-an-outbox&#34;&gt;You Don&#39;t Need an Outbox&lt;/a&gt;&lt;/strong&gt;: two things that must stay in step, held together by nothing but good intentions.&lt;/p&gt;&#xA;&lt;h2 id=&#34;two-stores-can-always-disagree&#34;&gt;Two Stores Can Always Disagree&lt;a class=&#34;anchor&#34; href=&#34;#two-stores-can-always-disagree&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Step back from the mechanics and the deeper problem comes into focus. The separate audit log rests on an assumption so ingrained that it is easy to miss: that &lt;strong&gt;the data lives in one place and the record of how it got there lives in another.&lt;/strong&gt; Two stores, two sources of truth for one history.&lt;/p&gt;&#xA;&lt;p&gt;The moment you have two sources of truth, you have a question with no good answer. When they disagree, which one is right? The data says the balance is one number; the log implies it should be another. From inside the system there is no way to settle it. You have built a contradiction and hoped never to be asked about it.&lt;/p&gt;&#xA;&lt;p&gt;This is the same structural mistake wearing a different costume. It is the outbox pattern&#39;s dual write, state here and events there, and it is the framing we questioned in &lt;strong&gt;&lt;a href=&#34;/resources/blog/it-was-never-about-the-database&#34;&gt;It Was Never About the Database&lt;/a&gt;&lt;/strong&gt;: the way you set up the problem quietly decides which problems you will spend years fighting. The fix is never a better-synchronized second log. &lt;strong&gt;The fix is to stop having two.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;when-the-log-is-the-data&#34;&gt;When the Log Is the Data&lt;a class=&#34;anchor&#34; href=&#34;#when-the-log-is-the-data&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Event Sourcing turns the whole arrangement inside out. Instead of storing current state and logging the changes on the side, you store the changes, and you derive current state from them. An account balance is not a number you overwrite; it is the result of every deposit and withdrawal that ever happened. The list of those events &lt;strong&gt;is the data and the history at the same time.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;There is no separate audit log in an event-sourced system, because there is nothing to keep in sync. The events are not a copy of the truth written for the auditor&#39;s benefit. &lt;strong&gt;They are the truth&lt;/strong&gt;, the very records the application reads to do its everyday work. You cannot forget to write the audit entry, because the audit entry &lt;em&gt;is&lt;/em&gt; the write. You cannot let the log drift from the data, because they were never two things to begin with. Reconstructing the state of an account two years ago stops being a special audit feature and becomes an ordinary replay, the same operation the system already runs to answer any question about the present.&lt;/p&gt;&#xA;&lt;p&gt;This is where the resemblance to an append-only message log gets seductive, and where it pays to be precise. An ordered, append-only stream looks a great deal like what Event Sourcing needs, which is exactly why teams reach for tools that are not event stores and get burned, a trap we mapped out in &lt;strong&gt;&lt;a href=&#34;/resources/blog/kafka-is-not-an-event-store&#34;&gt;Kafka Is Not an Event Store&lt;/a&gt;&lt;/strong&gt;. The point is not that events happen to flow through a log. The point is that the log &lt;strong&gt;is the system of record&lt;/strong&gt;, the thing every read and every decision is built on, rather than a side channel narrating something that lives elsewhere.&lt;/p&gt;&#xA;&lt;h2 id=&#34;proving-nothing-was-touched&#34;&gt;Proving Nothing Was Touched&lt;a class=&#34;anchor&#34; href=&#34;#proving-nothing-was-touched&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Collapsing two stores into one closes the gap, but it leaves the auditor&#39;s other demand standing: proving that the single history was not altered after the fact. Making the events the source of truth does not help if someone can quietly rewrite an old one.&lt;/p&gt;&#xA;&lt;p&gt;EventSourcingDB answers this by &lt;strong&gt;hash-chaining every event&lt;/strong&gt;. Each event carries a cryptographic hash of its predecessor, linking the entire history into a chain that behaves much like a blockchain. Change a single event anywhere in the past and its hash changes, which breaks the hash stored in the next event, which breaks the one after that, all the way to the present. &lt;strong&gt;Tampering is not something you have to trust has not happened; it is something you can recompute and detect.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;That turns an audit into a mechanical check rather than an act of faith. A &lt;code&gt;POST&lt;/code&gt; to the &lt;code&gt;/api/v1/audit-history&lt;/code&gt; endpoint walks the chain and confirms its integrity:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -i &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -X POST &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  -H &lt;span class=&#34;s2&#34;&gt;&amp;#34;Authorization: Bearer &amp;lt;API_TOKEN&amp;gt;&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  http://localhost:3000/api/v1/audit-history&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once a store has been audited, you send back the &lt;code&gt;latestEventId&lt;/code&gt; and &lt;code&gt;latestEventHash&lt;/code&gt; from the previous run, and the next check covers only the events added since. Those &lt;strong&gt;incremental audits&lt;/strong&gt; keep the cost flat no matter how large the history grows, and a separate full store verification stands behind them when you need to check the storage itself end to end. This exists for a simple reason: auditors should not have to take anyone&#39;s word for it. &lt;strong&gt;A verifiable history means an external party can confirm for themselves that nothing was changed, so you can walk into a compliance review carrying proof instead of promises.&lt;/strong&gt; If you would like to try it against your own data, the &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/auditing-the-event-store&#34;&gt;guide to auditing the event store&lt;/a&gt;&lt;/strong&gt; walks through the entire flow.&lt;/p&gt;&#xA;&lt;p&gt;Now watch what happens to the auditor&#39;s fatal question. &amp;quot;How do you know the log matches the data?&amp;quot; no longer has an answer, because it no longer has a subject. There is no separate log to match. There is one history, and here is the cryptographic proof that it is intact. The question that sank the bolted-on approach becomes the easiest slide in the deck.&lt;/p&gt;&#xA;&lt;h2 id=&#34;when-you-still-want-a-separate-log&#34;&gt;When You Still Want a Separate Log&lt;a class=&#34;anchor&#34; href=&#34;#when-you-still-want-a-separate-log&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;None of this means you should never write a log again, and it is worth being clear about the exceptions.&lt;/p&gt;&#xA;&lt;p&gt;If you are locked into a CRUD system you cannot change, a legacy core, a vendor database, a platform decision made long before you arrived, then a separate audit log is a reasonable way to &lt;strong&gt;reduce risk&lt;/strong&gt;. It will not give you proof, and everything above still applies, but a flawed record beats no record. Just be honest with yourself about what it is: a mitigation, not a guarantee.&lt;/p&gt;&#xA;&lt;p&gt;Plenty of logging has nothing to do with any of this. Access logs, debug traces, request logs, that is &lt;strong&gt;telemetry&lt;/strong&gt;, and it is genuinely useful. The difference is what the log is &lt;em&gt;for&lt;/em&gt;. Telemetry helps you operate the system. A compliance audit trail is meant to be the authoritative record of business facts, and that is the job a second copy of the truth cannot safely do.&lt;/p&gt;&#xA;&lt;p&gt;So the rule is not &amp;quot;never keep a log.&amp;quot; It is narrower and sharper: &lt;strong&gt;do not build your compliance story on a second copy of the truth when you can make the truth itself the record.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;one-history-provable&#34;&gt;One History, Provable&lt;a class=&#34;anchor&#34; href=&#34;#one-history-provable&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Come back to the auditor and their one hard question. Against a bolted-on audit log, &amp;quot;how do you know it matches the data?&amp;quot; is the question that ends the meeting badly. Against an event-sourced history with a verifiable chain, it is the question that ends it well, because there is nothing to reconcile and everything to prove. You did not add an audit log. You &lt;strong&gt;removed the gap that made one necessary.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;That is the shift worth holding onto. The record of what happened should not sit &lt;em&gt;beside&lt;/em&gt; your data, forever hoping to stay in step with it. &lt;strong&gt;The record should be the data.&lt;/strong&gt; Once the events are the source of truth and the chain makes them tamper-evident, the audit trail stops being a component you build and maintain, and becomes something the system simply is.&lt;/p&gt;&#xA;&lt;p&gt;If you are staring down a compliance requirement and wondering whether a second log is really the best you can do, we would genuinely like to hear about it. Tell us what you are up against at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt;, whether it is a consulting conversation, a workshop, or a second opinion; that is exactly the kind of problem we like to think through with people.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/introducing-our-new-pricing-model</id>
    <title>Introducing Our New Pricing Model</title>
    <link href="https://www.eventfoundation.io/resources/blog/introducing-our-new-pricing-model"></link>
    <published>2026-07-23T00:00:00+02:00</published>
    <updated>2026-07-23T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">Today, we&#39;re introducing a new pricing model for EventSourcingDB. Until now, it came with a single commercial license and little else: one price, the same for everyone, whether you were a solo developer with a side project or a company running it in production. That was simple, but simple and fair are not always the same thing.</summary>
    <content type="html">&lt;p&gt;Today, we&#39;re introducing a new pricing model for EventSourcingDB. Until now, it came with a single commercial license and little else: one price, the same for everyone, whether you were a solo developer with a side project or a company running it in production. That was simple, but simple and fair are not always the same thing.&lt;/p&gt;&#xA;&lt;p&gt;So we&#39;ve opened the model up. It still fits on a single page and still shows every number in public, but it now makes room for the whole range of people who build on EventSourcingDB: a genuinely free tier, a reduced rate for individual developers, quarterly terms alongside yearly ones, and special conditions for startups, non-profits, and larger scale. Here is what it looks like, why we shaped it this way, and a quick way to tell which license is yours.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;the-trouble-with-a-single-price&#34;&gt;The Trouble With a Single Price&lt;a class=&#34;anchor&#34; href=&#34;#the-trouble-with-a-single-price&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Until recently, EventSourcingDB came with exactly that one commercial license: 2,500 EUR per instance, per year. Once you needed to store more than a small number of events, that was the price – and it stayed the same whether you were one person with a side project or a team running EventSourcingDB across a fleet of services.&lt;/p&gt;&#xA;&lt;p&gt;There&#39;s something to like about that. It&#39;s easy to understand, there&#39;s nothing to negotiate, and you always know exactly what you&#39;ll pay. But &lt;strong&gt;a single price is a blunt instrument.&lt;/strong&gt; The same figure that feels reasonable to a well-funded engineering team can feel steep to an individual developer paying out of their own pocket – and a full year is a lot to commit to when you just want to try an idea for a few weeks.&lt;/p&gt;&#xA;&lt;p&gt;So we kept the parts that worked and built out everything around them.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-the-model-looks-like-now&#34;&gt;What the Model Looks Like Now&lt;a class=&#34;anchor&#34; href=&#34;#what-the-model-looks-like-now&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The new model is small enough to hold in your head, and you can place yourself in it in about a minute.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The free tier is genuinely free.&lt;/strong&gt; As long as an instance stores fewer than 25,000 events, you can run EventSourcingDB without a license key at all – in development and in production alike. That&#39;s enough room for evaluation, for personal projects, for testing, and for plenty of smaller commercial systems. No account, no key, no expiry to keep an eye on.&lt;/p&gt;&#xA;&lt;p&gt;And it isn&#39;t a trial. There&#39;s no thirty-day clock and no stripped-down feature set – it&#39;s the same EventSourcingDB, with the same capabilities, simply run below the event threshold. If an instance never crosses 25,000 events, it never costs a cent, and nothing about it degrades to remind you that you&#39;re on the free tier. In practice, that also means the instances you run locally for development and testing usually stay free on their own; a license is for the instances that actually cross the threshold, which is typically the ones in production.&lt;/p&gt;&#xA;&lt;p&gt;Above that threshold, you need a license, and this is where the new options come in. &lt;strong&gt;The commercial license stays at 2,500 EUR per instance per year, and now also comes as a quarterly license at 750 EUR&lt;/strong&gt; – three months at a time, for when a full year is more commitment than you want to make up front. Both terms renew automatically, and both are, as before, tied to a single instance: if you run several instances, each one needs its own license.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;For individual developers, there is now a dedicated license at 360 EUR per year, or 120 EUR per quarter.&lt;/strong&gt; It unlocks exactly the same functionality as the commercial license – technically, the two are identical – but at a fraction of the cost. It exists for one reason: a person building on EventSourcingDB in their own time, out of their own pocket, shouldn&#39;t pay the same as a company.&lt;/p&gt;&#xA;&lt;p&gt;All prices are net and quoted per instance. The current figures, along with the details on VAT, always live in the &lt;strong&gt;&lt;a href=&#34;/products/eventsourcingdb/licensing&#34;&gt;licensing documentation&lt;/a&gt;&lt;/strong&gt; – that page is the source of truth, and this post is the story behind it.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-we-priced-it-this-way&#34;&gt;Why We Priced It This Way&lt;a class=&#34;anchor&#34; href=&#34;#why-we-priced-it-this-way&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Every decision in this model comes back to one idea: &lt;strong&gt;you should be able to understand what EventSourcingDB costs without talking to us first.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;That&#39;s why the prices are public and fixed. There&#39;s no &amp;quot;contact sales&amp;quot; wall in front of the common cases, no per-seat arithmetic, no quote that quietly depends on how much we think you can afford. You read the number, and the number is the number.&lt;/p&gt;&#xA;&lt;p&gt;The prices are not just public, they&#39;re predictable. We charge per instance, and only per instance – never per seat, per core, or per event written. &lt;strong&gt;What you pay follows the shape of your system, not its success:&lt;/strong&gt; it doesn&#39;t climb because you hired three more engineers or had a strong month for traffic. Once an instance is licensed, its event count stops mattering – the price is the same whether it holds thirty thousand events or three hundred million. And how many instances you run, including &lt;strong&gt;&lt;a href=&#34;/resources/blog/one-database-to-rule-them-all&#34;&gt;whether each service gets its own or they share one&lt;/a&gt;&lt;/strong&gt;, is an architectural decision you&#39;d weigh regardless of what we charge.&lt;/p&gt;&#xA;&lt;p&gt;It&#39;s also why the individual developer license exists at all. &lt;strong&gt;Charging a solo developer the same as a company was never fair&lt;/strong&gt; – and &amp;quot;just stay on the free tier forever&amp;quot; stops being an answer the moment a real project crosses 25,000 events. A separate, lower rate is the straightforward fix, and it unlocks the full product rather than a cut-down version of it.&lt;/p&gt;&#xA;&lt;p&gt;And it&#39;s why the quarterly term sits next to the yearly one. Not every project wants to commit twelve months ahead. &lt;strong&gt;Three months at a time lowers the bar to getting started&lt;/strong&gt; without locking you into a year you&#39;re not yet sure about. If the project grows, the yearly term is right there; if it doesn&#39;t, you were never overcommitted.&lt;/p&gt;&#xA;&lt;p&gt;It&#39;s worth saying plainly why EventSourcingDB costs anything at all. An event store is the backbone of whatever runs on top of it – it has to be there, correct and maintained, for as long as those systems live, which is often measured in years. &lt;strong&gt;We would rather fund that with a clear, sustainable price than with the familiar free-until-it-isn&#39;t playbook,&lt;/strong&gt; where the tool a business was built on gets acquired, hollowed out, or quietly shut down the moment its growth story stalls. We&#39;ve been working toward this since we &lt;strong&gt;&lt;a href=&#34;/resources/blog/ten-years-one-goal&#34;&gt;founded the native web in 2012&lt;/a&gt;&lt;/strong&gt;, and the pricing exists to keep EventSourcingDB around just as long.&lt;/p&gt;&#xA;&lt;p&gt;There&#39;s one more decision that matters more than any of the prices, and you only see it when a license comes to an end.&lt;/p&gt;&#xA;&lt;h2 id=&#34;when-a-license-ends-your-data-stays-yours&#34;&gt;When a License Ends, Your Data Stays Yours&lt;a class=&#34;anchor&#34; href=&#34;#when-a-license-ends-your-data-stays-yours&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Pricing is ultimately a question of trust, and the moment that trust gets tested is when a license lapses. Some systems treat that moment as leverage: miss a payment, and your data disappears behind a paywall.&lt;/p&gt;&#xA;&lt;p&gt;EventSourcingDB doesn&#39;t. &lt;strong&gt;When a license expires, the database drops into read-only mode, and all of your data stays fully accessible.&lt;/strong&gt; You can query it, replay it, and read every event you ever wrote. What you can&#39;t do is write new events beyond the free tier&#39;s threshold until you renew. Nothing is silently switched off, and nothing is held hostage – the system stays as transparent and predictable as it was the day before.&lt;/p&gt;&#xA;&lt;p&gt;The same spirit runs through how licenses are checked in the first place. A license is simply a key file that you point EventSourcingDB at; there is no online activation and no phoning home. &lt;strong&gt;EventSourcingDB never contacts an external server to verify your license,&lt;/strong&gt; which is exactly what you need in an air-gapped network or a high-security zone. Your database doesn&#39;t depend on our servers being reachable – or, for that matter, on us being around at all.&lt;/p&gt;&#xA;&lt;h2 id=&#34;startups-non-profits-and-serious-scale&#34;&gt;Startups, Non-Profits, and Serious Scale&lt;a class=&#34;anchor&#34; href=&#34;#startups-non-profits-and-serious-scale&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Three situations don&#39;t fit neatly into a fixed price, and for those we&#39;d rather have a short conversation than make you force-fit yourself into the wrong box.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;If you&#39;re running EventSourcingDB at real scale,&lt;/strong&gt; across many instances, volume licensing brings the per-instance price down accordingly. &lt;strong&gt;If you&#39;re a startup&lt;/strong&gt; still finding your footing, or &lt;strong&gt;a non-profit or a similar organization&lt;/strong&gt; where every euro is already spoken for, reduced terms are available on request. None of these hide behind a form and a waiting period – they&#39;re just a matter of asking.&lt;/p&gt;&#xA;&lt;p&gt;If any of that sounds like you, write to us at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt; and tell us a little about what you&#39;re building. We&#39;ll work out something that fits.&lt;/p&gt;&#xA;&lt;h2 id=&#34;which-license-fits-you&#34;&gt;Which License Fits You?&lt;a class=&#34;anchor&#34; href=&#34;#which-license-fits-you&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;If you take away one practical thing from this post, let it be this short walk through the options. Start at the top and stop at the first line that describes you:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Storing fewer than 25,000 events?&lt;/strong&gt; Use the free tier. No license, no key, nothing to buy.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Building as a private individual, with no company of your own?&lt;/strong&gt; The individual developer license is yours, at 360 EUR a year or 120 EUR a quarter.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Working as, or on behalf of, a company or team?&lt;/strong&gt; That&#39;s the commercial license, at 2,500 EUR a year or 750 EUR a quarter.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Running many instances, or at large scale?&lt;/strong&gt; Ask us about volume licensing.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;A startup or a non-profit?&lt;/strong&gt; Ask us about reduced terms.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The line most worth reading twice is the boundary between the individual and the commercial license. &lt;strong&gt;The individual developer license is meant for natural persons, with the invoice made out to you personally.&lt;/strong&gt; The moment a company is involved – including a single-person GmbH, a UG, or a registered sole proprietorship – the commercial license applies, even if you work entirely on your own and think of yourself as an individual developer.&lt;/p&gt;&#xA;&lt;h2 id=&#34;a-price-that-meets-you-where-you-are&#34;&gt;A Price That Meets You Where You Are&lt;a class=&#34;anchor&#34; href=&#34;#a-price-that-meets-you-where-you-are&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;We opened by admitting that a single price, however simple, was never quite fair. &lt;strong&gt;The point of this new model is to keep the simplicity and fix the fairness&lt;/strong&gt; – to make what you pay match the way you actually build. The prices are on the page, in public, with no contact form and no sales call in the way. The free tier is real. The individual rate exists because charging a solo developer like a corporation was never right. And if your situation is genuinely unusual, the door is open – you just have to knock.&lt;/p&gt;&#xA;&lt;p&gt;If you want the exact figures, the VAT details, and the fine print all in one place, the &lt;strong&gt;&lt;a href=&#34;/products/eventsourcingdb/licensing&#34;&gt;licensing documentation&lt;/a&gt;&lt;/strong&gt; has every last bit of it. And if you&#39;re not storing 25,000 events yet, there&#39;s nothing to decide at all: &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/installing-eventsourcingdb&#34;&gt;install EventSourcingDB&lt;/a&gt;&lt;/strong&gt; and start building – the pricing question can wait until you actually need to answer it.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/kafka-is-not-an-event-store</id>
    <title>Kafka Is Not an Event Store</title>
    <link href="https://www.eventfoundation.io/resources/blog/kafka-is-not-an-event-store"></link>
    <published>2026-07-20T00:00:00+02:00</published>
    <updated>2026-07-20T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">Every time a team sits down to build an event-sourced system, someone asks the same question: we already run Kafka, and Kafka is an append-only, ordered log, so isn&#39;t that exactly what Event Sourcing needs? The resemblance is real, and it is tempting. It is also superficial, and mistaking one for the other tends to get expensive months later, once the system is in production and the workarounds have quietly piled up.</summary>
    <content type="html">&lt;p&gt;Every time a team sits down to build an event-sourced system, someone asks the same question: we already run Kafka, and Kafka is an append-only, ordered log, so isn&#39;t that exactly what Event Sourcing needs? The resemblance is real, and it is tempting. It is also superficial, and mistaking one for the other tends to get expensive months later, once the system is in production and the workarounds have quietly piled up.&lt;/p&gt;&#xA;&lt;p&gt;So rather than arguing that Kafka is bad, because it is not, we want to do something more useful. We will first pin down what an event store actually has to do, then hold Kafka against that list and see where it stands. This is the first post in a small series that asks the same question of one candidate at a time. And for Kafka, the answer is the one that surprises people the least once they can see it laid out: it is an excellent tool, just not for this particular job.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;the-log-that-looks-like-an-event-store&#34;&gt;The Log That Looks Like an Event Store&lt;a class=&#34;anchor&#34; href=&#34;#the-log-that-looks-like-an-event-store&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Start with Kafka&#39;s own vocabulary, because that is the model a Kafka user thinks in. Events are published to &lt;strong&gt;topics&lt;/strong&gt;, each topic is split into a number of &lt;strong&gt;partitions&lt;/strong&gt;, and every event sits at an &lt;strong&gt;offset&lt;/strong&gt; within its partition. Consumers read forward from an offset, events stay around for as long as the configured retention allows, and you can replay a partition by rewinding. Append-only, ordered, replayable: on the surface, this reads like a description of Event Sourcing.&lt;/p&gt;&#xA;&lt;p&gt;That is exactly why the analogy is so seductive. Event Sourcing also treats an append-only, ordered sequence of events as the ground truth, and it also reconstructs state by reading those events back. Draw both on a whiteboard and you get the same picture: &lt;strong&gt;a log you append to and read from&lt;/strong&gt;. If the shapes match, why buy a second thing?&lt;/p&gt;&#xA;&lt;p&gt;Because &amp;quot;looks like&amp;quot; is not &amp;quot;is&amp;quot;. The whiteboard hides every guarantee that actually matters, and the guarantees are the whole point. &lt;strong&gt;The question is not whether Kafka can hold events&lt;/strong&gt; – anything can hold events – &lt;strong&gt;but whether it can hold them the way an event-sourced system depends on.&lt;/strong&gt; To answer that, we need a yardstick.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-an-event-store-has-to-do&#34;&gt;What an Event Store Has to Do&lt;a class=&#34;anchor&#34; href=&#34;#what-an-event-store-has-to-do&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Here is what a store has to provide to serve as the backbone of an event-sourced system. This is the list we will hold every candidate against, in this post and in the ones that follow.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;A durable source of truth.&lt;/strong&gt; Events are not data in transit on their way somewhere else; they are the truth itself, kept for as long as the system lives.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Append-only, immutable storage.&lt;/strong&gt; Not by convention or team discipline, but as a guarantee the store enforces, so that no past event can ever be rewritten or quietly dropped.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Atomic writes across multiple events.&lt;/strong&gt; A single decision that produces several events is persisted as one indivisible unit, so you never store half of it.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Optimistic concurrency per stream.&lt;/strong&gt; The ability to append events only if that stream has not changed since you last read it, which is how invariants survive concurrent writes.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Two different read patterns.&lt;/strong&gt; Reading the entire history in order, to build and rebuild read models, and reading a single entity&#39;s events cheaply, to reconstruct that one entity&#39;s current state.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Subscriptions as a first-class feature.&lt;/strong&gt; Observing new events as they arrive, with a seamless transition from replaying the past to following the present.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;That is the yardstick. &lt;strong&gt;Six capabilities, and an event store has to deliver all of them together, not most of them with effort.&lt;/strong&gt; Now let us hold Kafka against it, criterion by criterion.&lt;/p&gt;&#xA;&lt;h2 id=&#34;transport-not-truth&#34;&gt;Transport, Not Truth&lt;a class=&#34;anchor&#34; href=&#34;#transport-not-truth&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The deepest mismatch shows up at the very first item on the list. Kafka is built to move events from producers to consumers, and it is superb at that. But &lt;strong&gt;retention is a configuration knob, not a promise of permanence.&lt;/strong&gt; Set it to seven days, or let log compaction collapse a keyed topic down to the latest value per key, and older events are simply gone. You can, of course, configure retention to keep everything indefinitely – but permanence you have to remember to switch on is not the same as permanence by design. A store whose contents can expire by policy is not where you keep your only copy of the truth.&lt;/p&gt;&#xA;&lt;p&gt;Picture the consequence in concrete terms. A consumer that goes offline for a week comes back to rebuild its read model, only to find that the events it needed have already aged out of retention – the history it was supposed to replay is not there anymore. &lt;strong&gt;An event store never puts you in that position, because keeping the full history is its reason to exist, not a setting you can accidentally misconfigure.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;There is a subtler point hiding underneath, and it is one of the most essential rules of event-sourced systems: &lt;strong&gt;you store the event as the truth first, and publishing it is a separate, downstream concern.&lt;/strong&gt; In Kafka, those two steps are the same step. Publishing &lt;em&gt;is&lt;/em&gt; the write. There is no moment where the event has been made true and you then decide, separately, who gets to hear about it. The truth and the transport collapse into a single act.&lt;/p&gt;&#xA;&lt;p&gt;That collapse is the root of most of what follows. &lt;strong&gt;Kafka is a pipe, and a pipe is a wonderful thing to have – but it is not the place you keep the record that everything else depends on.&lt;/strong&gt; The source of truth has to live somewhere with stronger promises, and once it does, Kafka&#39;s role changes from &amp;quot;the store&amp;quot; to &amp;quot;the thing downstream of the store.&amp;quot;&lt;/p&gt;&#xA;&lt;h2 id=&#34;where-are-the-guardrails&#34;&gt;Where Are the Guardrails?&lt;a class=&#34;anchor&#34; href=&#34;#where-are-the-guardrails&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The next requirement Kafka drops is enforcing invariants when you write. The core move in Event Sourcing is small and constant: &lt;strong&gt;append this event only if the aggregate is still at the version I last saw.&lt;/strong&gt; That single check is what stops two concurrent commands from both succeeding and leaving an aggregate in a state the business rules say can never exist – two people booking the last seat, an account overdrawn past its limit, an order shipped twice.&lt;/p&gt;&#xA;&lt;p&gt;Kafka has no optimistic concurrency control at the granularity you need. The closest workaround is a &lt;strong&gt;coarse, pessimistic lock at the partition level&lt;/strong&gt; – and because a single partition holds many entities, it blocks far more than the one you actually care about, while trading away the throughput that made Kafka attractive in the first place. Optimistic concurrency solves the same problem precisely, at the level of one stream, and it scales because conflicts are the exception rather than the rule.&lt;/p&gt;&#xA;&lt;p&gt;This is one of the places where a purpose-built store earns its keep. In EventSourcingDB you attach &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/preconditions&#34;&gt;preconditions&lt;/a&gt;&lt;/strong&gt; to a write, and they are evaluated inside the very same transaction that appends your events. That means you can enforce a business or consistency rule declaratively, without external locks and without extra roundtrips, and a violated expectation simply aborts the write. If you would like to see how that feels in practice, the preconditions guide walks through it end to end.&lt;/p&gt;&#xA;&lt;h2 id=&#34;topics-are-not-streams&#34;&gt;Topics Are Not Streams&lt;a class=&#34;anchor&#34; href=&#34;#topics-are-not-streams&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Now to the mismatch that trips people up most, because it sits right in the vocabulary. &lt;strong&gt;Kafka&#39;s unit is the topic&lt;/strong&gt;, a coarse container split into a limited and deliberately small number of partitions. &lt;strong&gt;Event Sourcing&#39;s unit is the stream&lt;/strong&gt; – one per aggregate, one per entity – and a real system has as many streams as it has entities, which can easily be millions. EventSourcingDB calls these fine-grained streams &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/subjects&#34;&gt;subjects&lt;/a&gt;&lt;/strong&gt;, and reasoning in terms of them is what makes per-entity work natural.&lt;/p&gt;&#xA;&lt;p&gt;Watch what happens when you try to map one onto the other. Reconstructing a single aggregate means loading exactly one stream. In Kafka, &amp;quot;give me every event for order 4711&amp;quot; leaves you two bad options: put one topic or partition per entity, which does not scale to millions of them, or share a topic across entities and scan it, filtering out everything that is not order 4711, which is linear in the size of the whole topic. &lt;strong&gt;Both are wrong answers to the single most common read in an event-sourced system.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;And it gets worse when you need the other read pattern at the same time. Projections need the full history in a single, well-defined order. Across partitions, Kafka gives you order &lt;em&gt;within&lt;/em&gt; a partition but no global order for free; recovering one means adding your own sequencing logic on top. So the two reads pull against each other: partition by entity and you lose easy global order, keep a shared log and you lose cheap per-entity access. &lt;strong&gt;A topic is a coarse pipe; a stream is a fine-grained, addressable unit of truth. They are simply not the same shape.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-you-end-up-building&#34;&gt;What You End Up Building&lt;a class=&#34;anchor&#34; href=&#34;#what-you-end-up-building&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Step back and watch what a team actually does when it insists on Kafka as the store anyway, because the pattern is remarkably consistent. To get concurrency, they add an external database to hold and check version numbers. To read a single entity efficiently, they add an index or a secondary store keyed by entity id. To make sure events are never lost to retention, they add another durable store behind Kafka. And to keep that store and Kafka in agreement, they reach for the outbox pattern.&lt;/p&gt;&#xA;&lt;p&gt;Piece by piece, &lt;strong&gt;they rebuild an event store around Kafka&lt;/strong&gt; – the very thing they set out to avoid needing. The outbox in particular is a tell. We have argued before that &lt;strong&gt;&lt;a href=&#34;/resources/blog/you-dont-need-an-outbox&#34;&gt;you don&#39;t need an outbox&lt;/a&gt;&lt;/strong&gt; when storing an event and publishing it are the same atomic act; the outbox is precisely the workaround you reach for &lt;em&gt;because&lt;/em&gt; Kafka splits those two apart. Its presence is a symptom, not a solution.&lt;/p&gt;&#xA;&lt;p&gt;The result is an architecture with more moving parts, more places to get consistency subtly wrong, and more operational surface than the purpose-built store the team was trying to avoid buying. &lt;strong&gt;The complexity you were trying to skip comes back, wearing a different hat&lt;/strong&gt; – and now it is spread across several systems that all have to agree.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-right-log-for-the-right-job&#34;&gt;The Right Log for the Right Job&lt;a class=&#34;anchor&#34; href=&#34;#the-right-log-for-the-right-job&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Come back to the question we started with. Kafka is an append-only, ordered log, and Event Sourcing wants an append-only, ordered log, so the confusion is understandable. But the two logs are built for opposite ends of the system. &lt;strong&gt;Kafka&#39;s log is optimized to move events between services quickly and at scale. An event store&#39;s log is optimized to keep events as the truth, protect the invariants as they are written, and hand them back by entity and in order.&lt;/strong&gt; Those are different jobs, and optimizing hard for one means giving ground on the other.&lt;/p&gt;&#xA;&lt;p&gt;That is the whole point, stated plainly: &lt;strong&gt;a great messaging platform and the best event store are not the same tool&lt;/strong&gt;, and asking one to be the other is where the trouble starts. Kafka shines exactly where an event store should not be – as the transport between systems, downstream of the store, carrying events that have already been made true. Used there, it is not a compromise; it is the right tool doing what it is best at.&lt;/p&gt;&#xA;&lt;p&gt;It is worth being fair about the one item on our list Kafka handles gracefully: following a stream of new events as they arrive is its native strength. But even there, it follows partitions, not the fine-grained per-entity streams an event-sourced system reconstructs from. &lt;strong&gt;That is the tell in miniature&lt;/strong&gt; – Kafka fits so naturally on the transport side and so awkwardly on the storage side because every one of its strengths is aimed at moving events, not at keeping them as the truth.&lt;/p&gt;&#xA;&lt;p&gt;This is the first entry in a series that holds different candidates against the same six-item list. Next, we will turn it around and point it at the tools people reach for from the opposite direction: the general-purpose databases they already trust for everything else. If you are weighing these trade-offs for your own architecture and want to go deeper on Event Sourcing, CQRS, and Domain-Driven Design, we gather that thinking in one place at &lt;strong&gt;&lt;a href=&#34;https://www.cqrs.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;cqrs.com&lt;/a&gt;&lt;/strong&gt; – a good next stop once you have decided that the log on your whiteboard deserves a store built for it.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/well-take-the-benefits-hold-the-rules</id>
    <title>We&#39;ll Take the Benefits, Hold the Rules</title>
    <link href="https://www.eventfoundation.io/resources/blog/well-take-the-benefits-hold-the-rules"></link>
    <published>2026-07-16T00:00:00+02:00</published>
    <updated>2026-07-16T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">There&#39;s a moment that shows up in Event Sourcing projects again and again. The design is going well, the team is sold on the idea, and then a single hard requirement surfaces: GDPR&#39;s right to erasure. All at once, storing events immutably looks less like a feature and more like a liability. So the team grants itself one small exception – when someone asks to be deleted, they&#39;ll reach into the stored event and overwrite the offending field, turning an email address into &#34;redacted&#34;. One small edit, applied rarely, to a system that is otherwise append-only.</summary>
    <content type="html">&lt;p&gt;There&#39;s a moment that shows up in Event Sourcing projects again and again. The design is going well, the team is sold on the idea, and then a single hard requirement surfaces: GDPR&#39;s right to erasure. All at once, storing events immutably looks less like a feature and more like a liability. So the team grants itself one small exception – when someone asks to be deleted, they&#39;ll reach into the stored event and overwrite the offending field, turning an email address into &lt;code&gt;&amp;quot;redacted&amp;quot;&lt;/code&gt;. One small edit, applied rarely, to a system that is otherwise append-only.&lt;/p&gt;&#xA;&lt;p&gt;It sounds harmless. It even sounds responsible. But that one exception quietly cancels a large part of the reason they chose Event Sourcing in the first place, and they likely won&#39;t notice until the day they need one of the guarantees that no longer holds. Because the benefits of Event Sourcing aren&#39;t a menu you order from. They all hang on a single thread, and that field overwrite cuts it.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;weve-seen-this-before&#34;&gt;We&#39;ve Seen This Before&lt;a class=&#34;anchor&#34; href=&#34;#weve-seen-this-before&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;We&#39;ve watched this pattern play out for years, and not only with Event Sourcing.&lt;/p&gt;&#xA;&lt;p&gt;Rewind ten or fifteen years to when teams everywhere were adopting Scrum. They wanted the benefits: faster feedback, more predictable delivery, happier and more autonomous teams. But some of the practices were uncomfortable, so they trimmed them. Retrospectives got dropped because there was never time. The Product Owner doubled as Scrum Master because hiring for both roles seemed wasteful. Sprints were allowed to &amp;quot;breathe&amp;quot; whenever a deadline slipped. Each cut looked perfectly reasonable in isolation, and plenty of them genuinely were.&lt;/p&gt;&#xA;&lt;p&gt;Because here&#39;s the thing: adapting a method to your context isn&#39;t the mistake. There&#39;s nothing sacred about any practice, and shaping a discipline to fit how you actually work is usually the right instinct, not the wrong one. The question that&#39;s easy to skip past in the moment is a quieter one. &lt;strong&gt;When you change the thing, what changes with it?&lt;/strong&gt; What do you gain from the adaptation, and what quietly leaves along with the part you removed?&lt;/p&gt;&#xA;&lt;p&gt;That question is the heart of this post. &lt;strong&gt;The trap isn&#39;t adapting a discipline – it&#39;s expecting the unchanged payoff after you&#39;ve changed the thing that produced it.&lt;/strong&gt; You can keep the rules and keep the benefits, or drop the rules and accept the loss, but you can&#39;t drop the rules and keep the benefits. That bargain was never on the table.&lt;/p&gt;&#xA;&lt;p&gt;Event Sourcing invites exactly that question, and the field-overwrite exception is a textbook case. So let&#39;s look at what&#39;s actually being traded away.&lt;/p&gt;&#xA;&lt;h2 id=&#34;one-rule-holds-it-all-up&#34;&gt;One Rule Holds It All Up&lt;a class=&#34;anchor&#34; href=&#34;#one-rule-holds-it-all-up&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Strip Event Sourcing down to its core and you find a surprisingly small idea. You don&#39;t store the current state of your data. You store the sequence of facts that led to it, and you never change a fact once it&#39;s recorded. That&#39;s the whole thing. &lt;strong&gt;Events are immutable. Append-only. What happened, happened.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Everything people love about Event Sourcing grows out of that one rule. The trustworthy audit trail, the ability to rebuild any past state, cheap and disposable caches, time travel for debugging – none of these are separate features that the database bolts on the side. &lt;strong&gt;They are consequences of immutability.&lt;/strong&gt; Take immutability away and they don&#39;t degrade gracefully; they stop being true.&lt;/p&gt;&#xA;&lt;p&gt;This is the part that&#39;s easy to miss when you grant yourself &amp;quot;just one small exception.&amp;quot; You picture removing a single item from a feature list. What you&#39;re actually removing is the foundation that several of those features silently stand on. The features don&#39;t know about your good intentions or how rarely you&#39;ll use the exception. They only know whether the past can change.&lt;/p&gt;&#xA;&lt;p&gt;Let&#39;s trace what actually leaves the building when you overwrite that one field.&lt;/p&gt;&#xA;&lt;h2 id=&#34;auditability-was-the-whole-point&#34;&gt;Auditability Was the Whole Point&lt;a class=&#34;anchor&#34; href=&#34;#auditability-was-the-whole-point&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Many teams reach for Event Sourcing precisely because they need a history they can trust. Regulated industries, financial systems, anything where &amp;quot;what happened, and when?&amp;quot; must be answerable and defensible months or years later – these are the classic cases, and they&#39;re often the reason the idea gets approved at all.&lt;/p&gt;&#xA;&lt;p&gt;EventSourcingDB strengthens that history with hash chaining: each event carries a cryptographic hash that incorporates the hash of its predecessor, forming a tamper-evident chain that behaves much like a blockchain. We&#39;ve written about why that matters in &lt;strong&gt;&lt;a href=&#34;/resources/blog/what-aviation-teaches-us-about-auditing&#34;&gt;What Aviation Teaches Us About Auditing&lt;/a&gt;&lt;/strong&gt;, about how you can prove a specific event existed without revealing its contents in &lt;strong&gt;&lt;a href=&#34;/resources/blog/proving-without-revealing-merkle-trees&#34;&gt;Proving Without Revealing: Merkle Trees&lt;/a&gt;&lt;/strong&gt;, and about how to put it to work operationally in the guide on &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/auditing-the-event-store&#34;&gt;auditing the event store&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Here&#39;s the problem. The moment you allow yourself to overwrite a payload field, &lt;strong&gt;&amp;quot;the log says X&amp;quot; stops being proof of anything.&lt;/strong&gt; A tamper-evident history whose own keepers tamper with it on purpose is just a database with extra ceremony. Worse, the hash chain will tell on you: change a stored event and the chain breaks, which is precisely the signal an auditor is trained to distrust. You don&#39;t get a clean redaction; you get a visible scar that says &amp;quot;something here was altered after the fact.&amp;quot;&lt;/p&gt;&#xA;&lt;p&gt;So the field overwrite doesn&#39;t just redact one email. It downgrades the entire audit trail from &amp;quot;provable&amp;quot; to &amp;quot;probably accurate, unless someone edited it.&amp;quot; For the use case that very likely justified the whole approach, that&#39;s not a minor concession.&lt;/p&gt;&#xA;&lt;h2 id=&#34;caching-snapshots-and-replays-assume-the-past-holds-still&#34;&gt;Caching, Snapshots, and Replays Assume the Past Holds Still&lt;a class=&#34;anchor&#34; href=&#34;#caching-snapshots-and-replays-assume-the-past-holds-still&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The second casualty is less obvious but far more pervasive. Much of what makes an event-sourced system fast and flexible rests on a quiet assumption: &lt;strong&gt;replaying the same events always produces the same state.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;That determinism is what lets you build read models, or projections, on demand and actually trust them. You can throw a projection away and rebuild it, run several differently shaped views off the same stream, and add a new one next year from history that&#39;s still intact – we explored just how many shapes those views can take in &lt;strong&gt;&lt;a href=&#34;/resources/blog/the-read-model-zoo&#34;&gt;The Read Model Zoo&lt;/a&gt;&lt;/strong&gt;. The same assumption is what makes snapshots safe: a snapshot is only a cached fold over a prefix of the stream, valid precisely because that prefix never changes. EventSourcingDB even realizes snapshots through events rather than a separate store, which works only because the events beneath them never move.&lt;/p&gt;&#xA;&lt;p&gt;Now mutate one event in the middle of the stream. Every projection you built before the change was derived from data that no longer exists. Every snapshot taken after that point folds in a value that has since been rewritten. Every cache keyed on the old history is now silently wrong – and nothing tells you. The events still load, the system still runs, the numbers are just quietly off.&lt;/p&gt;&#xA;&lt;p&gt;You can no longer answer &amp;quot;if I rebuild from scratch, do I get the same result I had yesterday?&amp;quot; with a confident yes. You&#39;ve traded a system whose derived data is correct by construction for one where every cache and every snapshot is a question mark you have to reason about by hand.&lt;/p&gt;&#xA;&lt;h2 id=&#34;time-travel-needs-a-past-that-stayed-put&#34;&gt;Time Travel Needs a Past That Stayed Put&lt;a class=&#34;anchor&#34; href=&#34;#time-travel-needs-a-past-that-stayed-put&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The third loss is the one teams mourn most once they&#39;ve tasted it. Because an event-sourced system keeps every fact, you can reconstruct the exact state of the world at any moment in the past. &amp;quot;What did this account look like the instant before the disputed transaction?&amp;quot; becomes a query, not an archaeology project.&lt;/p&gt;&#xA;&lt;p&gt;This is extraordinary for debugging. When something goes wrong in production, you don&#39;t theorize about the state that led to it – you rebuild that state and look at it directly. The bug stops being a mystery and becomes an observation. It&#39;s also what makes &amp;quot;what if&amp;quot; analysis possible: replay history through a new rule and see how the outcome would have differed.&lt;/p&gt;&#xA;&lt;p&gt;But time travel only works if the past stayed put. The moment you&#39;ve overwritten fields after the fact, the past you reconstruct is the &lt;em&gt;edited&lt;/em&gt; past, not the one your system actually lived through. You&#39;ll rebuild a state that never existed, debug against a history that was retouched, and draw conclusions about an incident from data that was changed after the incident. &lt;strong&gt;The single most powerful debugging tool you had now lies to you, politely and without warning.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;you-probably-dont-have-to-choose&#34;&gt;You Probably Don&#39;t Have to Choose&lt;a class=&#34;anchor&#34; href=&#34;#you-probably-dont-have-to-choose&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Here&#39;s the part we most want teams to hear: the deletion problem is real, and it deserves a real solution. GDPR&#39;s right to erasure isn&#39;t optional, and immutable storage genuinely makes it harder. We&#39;re not waving that away. What we&#39;re questioning is the assumption that overwriting events is the only way out – because it&#39;s actually the most expensive one.&lt;/p&gt;&#xA;&lt;p&gt;The cheapest fix is to not write the personal data into the event in the first place. Store a reference – a user ID, an opaque token – and keep the personal data in a conventional store where you can update or delete it freely. Erasing a person then means deleting one row over there; the event stream, which only ever held a reference, stays intact and truthful. We made the broader version of this argument in &lt;strong&gt;&lt;a href=&#34;/resources/blog/its-none-of-your-business&#34;&gt;It&#39;s None of Your Business&lt;/a&gt;&lt;/strong&gt; and the concrete version in &lt;strong&gt;&lt;a href=&#34;/resources/blog/dont-put-that-pdf-in-your-event&#34;&gt;Don&#39;t Put That PDF in Your Event&lt;/a&gt;&lt;/strong&gt;: events are for facts, not for payloads you&#39;ll later wish you could take back.&lt;/p&gt;&#xA;&lt;p&gt;When the data genuinely must live inside the event, there&#39;s still a technique that preserves immutability: crypto-shredding. You encrypt the sensitive fields with a per-subject key, store only the ciphertext in the event, and keep the key elsewhere. To erase that person, you destroy their key. The event stays byte-for-byte unchanged, so the hash chain holds, replays stay deterministic, and the audit trail stays provable – but the protected data becomes permanently unreadable. You satisfy the erasure request without rewriting a single fact.&lt;/p&gt;&#xA;&lt;p&gt;Both approaches keep the rule intact, which is exactly why they keep the benefits intact. We&#39;ve written up the trade-offs, including when migration-based redaction is the right tool, in our &lt;strong&gt;&lt;a href=&#34;/concepts/gdpr-compliance&#34;&gt;GDPR compliance guide&lt;/a&gt;&lt;/strong&gt; – worth a read before you commit to any deletion strategy. &lt;strong&gt;The point is simply this: mutating events to comply is usually a false economy. You spend your audit trail, your reproducibility, and your time travel to solve a problem that has cheaper solutions costing you none of them.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;adapt-with-your-eyes-open&#34;&gt;Adapt With Your Eyes Open&lt;a class=&#34;anchor&#34; href=&#34;#adapt-with-your-eyes-open&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Come back to that one small exception. The day it&#39;s granted, overwriting a single field to satisfy a deletion request feels like a pragmatic compromise, a minor concession to reality. What we hope is clearer now is the size of the bill stapled to it. That one edit doesn&#39;t cost you a field. It costs you the provable audit trail, the reproducible caches and snapshots, and the ability to trust a reconstructed past – three of the headline reasons to choose Event Sourcing at all, given up to solve a problem that didn&#39;t require surrendering any of them.&lt;/p&gt;&#xA;&lt;p&gt;None of this means you can&#39;t adapt Event Sourcing. You can, and sometimes you should. Just as with Scrum, the freedom to change the practice was never the issue. &lt;strong&gt;The issue is expecting the benefits of a discipline you&#39;ve opted out of.&lt;/strong&gt; Keep the rule and you keep what the rule buys you. Break it and you should break it deliberately, knowing precisely which guarantees walk out the door with it – and ideally reaching for a technique that lets you keep both.&lt;/p&gt;&#xA;&lt;p&gt;So before you grant yourself that one small exception, ask which of these benefits you were counting on. If the honest answer is &amp;quot;none of them,&amp;quot; then adapt away with a clear conscience. If the answer is &amp;quot;several,&amp;quot; the exception is far more expensive than it looks.&lt;/p&gt;&#xA;&lt;p&gt;Modeling personal data in an event-sourced system is exactly the kind of decision that&#39;s cheap to get right at design time and painful to retrofit later. If you&#39;re weighing these trade-offs for your own architecture, we&#39;d genuinely enjoy thinking them through with you – just write to us at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/esdm-is-now-open-source</id>
    <title>ESDM Is Now Open Source</title>
    <link href="https://www.eventfoundation.io/resources/blog/esdm-is-now-open-source"></link>
    <published>2026-07-13T00:00:00+02:00</published>
    <updated>2026-07-13T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">As of today, ESDM is available under the MIT license. The full source of the esdm toolchain, the schema that defines the language, and the documentation now live in a public repository on GitHub. You can read it, use it in any project, and build on it – at no cost and with no strings attached.</summary>
    <content type="html">&lt;p&gt;As of today, &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;ESDM&lt;/a&gt;&lt;/strong&gt; is available under the &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;MIT license&lt;/a&gt;&lt;/strong&gt;. The full source of the &lt;code&gt;esdm&lt;/code&gt; toolchain, the schema that defines the language, and the documentation now live in a public repository on GitHub. &lt;strong&gt;You can read it, use it in any project, and build on it – at no cost and with no strings attached.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;We &lt;strong&gt;&lt;a href=&#34;/resources/blog/one-year-of-eventsourcingdb-and-the-language-that-belongs-beside-it&#34;&gt;introduced ESDM about two months ago&lt;/a&gt;&lt;/strong&gt;, and we deliberately kept the source in our own hands at first, long enough to sharpen the language through a few rounds of real use before handing it to the world. That stretch is behind us. The core has settled, and today is the day we open it up.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;what-you-get&#34;&gt;What You Get&lt;a class=&#34;anchor&#34; href=&#34;#what-you-get&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;ESDM lets you describe event-sourced domains – Aggregates, Events, Commands, Bounded Contexts, Process Managers, Read Models, and the rest – as plain &lt;code&gt;.esdm.yaml&lt;/code&gt; files that live next to your code. The toolchain ships a linter that catches structural and modeling mistakes, a &lt;code&gt;view&lt;/code&gt; command that renders a model as a readable summary, and a &lt;code&gt;glossary&lt;/code&gt; command that turns it into a shared vocabulary your whole team can use. All of it is now in the open.&lt;/p&gt;&#xA;&lt;p&gt;The MIT license is about as permissive as they come. &lt;strong&gt;Use ESDM in personal and commercial projects alike, wire it into your CI, embed it wherever you need it – no license key, no account, no per-seat cost, nothing to ask us for.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;If you have not tried ESDM yet, the fastest way in is the documentation at &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;esdm.io&lt;/a&gt;&lt;/strong&gt;: install it, write a first model, and see what it feels like when your domain model is a file your code can actually read. If you already use it, the news is simply that the source is now yours as much as it is ours.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-we-hope-youll-do-with-it&#34;&gt;What We Hope You&#39;ll Do With It&lt;a class=&#34;anchor&#34; href=&#34;#what-we-hope-youll-do-with-it&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Use it. Describe your own domains with it, capture the model of a system you already run, write tutorials and examples on top of it, teach with it, and build tooling around it. And because the schema itself is public now, that tooling never needs us in the loop: a generator, a validator, an editor integration, or an AI-assisted modeler can read and write the format on its own.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Every model and every tool that speaks the same language is one less island in a field that has far too many of them&lt;/strong&gt; – a gap we came back thinking hard about after this year&#39;s &lt;strong&gt;&lt;a href=&#34;/resources/blog/too-many-islands-too-few-bridges&#34;&gt;Event Modeling Conference&lt;/a&gt;&lt;/strong&gt;. The more of us who describe our domains in the same terms, the more we can genuinely share. And none of it hinges on us: whatever you build on ESDM is yours, independent of our roadmap, our business, or our permission.&lt;/p&gt;&#xA;&lt;h2 id=&#34;tell-us-what-works&#34;&gt;Tell Us What Works&lt;a class=&#34;anchor&#34; href=&#34;#tell-us-what-works&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;ESDM is still young, and it will improve fastest with more people putting it to real use. If a rule feels wrong, a diagnostic reads badly, or you wish it did something it doesn&#39;t yet, we would like to hear it. &lt;strong&gt;&lt;a href=&#34;https://github.com/thenativeweb/esdm&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Open an issue on GitHub&lt;/a&gt;&lt;/strong&gt;, or simply write to us at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt; – for feedback, for questions, or just to tell us what you built.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/claude-start-my-database</id>
    <title>Claude, Start My Database</title>
    <link href="https://www.eventfoundation.io/resources/blog/claude-start-my-database"></link>
    <published>2026-07-09T00:00:00+02:00</published>
    <updated>2026-07-09T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">When we released the Claude Code Plugin for EventSourcingDB at Easter, we proudly listed everything it did not need: no SDK, no MCP configuration, not even a Docker container. Claude had learned to speak EventSourcingDB fluently. But there was a quiet assumption baked into every conversation – that somewhere, someone had already started a database for Claude to talk to.</summary>
    <content type="html">&lt;p&gt;When we released the &lt;strong&gt;&lt;a href=&#34;/resources/blog/a-skill-for-easter-teaching-claude-to-speak-eventsourcingdb&#34;&gt;Claude Code Plugin for EventSourcingDB&lt;/a&gt;&lt;/strong&gt; at Easter, we proudly listed everything it did not need: no SDK, no MCP configuration, not even a Docker container. Claude had learned to speak EventSourcingDB fluently. But there was a quiet assumption baked into every conversation – that somewhere, someone had already started a database for Claude to talk to.&lt;/p&gt;&#xA;&lt;p&gt;Version 1.1.0 of the plugin removes that assumption. &lt;strong&gt;Claude can now start a local EventSourcingDB on its own&lt;/strong&gt;, and it has picked up a few habits that make longer sessions noticeably smoother: consistent event sources, uniform event type prefixes, and a deeper understanding of the CloudEvents format. In this post, we walk through what is new – along a session that begins with an empty terminal and ends with a queryable event store.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;one-sentence-to-a-running-instance&#34;&gt;One Sentence to a Running Instance&lt;a class=&#34;anchor&#34; href=&#34;#one-sentence-to-a-running-instance&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The headline feature of 1.1.0 is the new &lt;strong&gt;&lt;code&gt;/esdb:start-server&lt;/code&gt;&lt;/strong&gt; Skill. You type: &amp;quot;Spin up a throwaway EventSourcingDB for me.&amp;quot; That single sentence replaces a Docker command you would otherwise have to remember, or at least look up:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker run -d --rm --name eventsourcingdb-dev -p 3000:3000 &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  thenativeweb/eventsourcingdb:latest run &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  --api-token&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;secret &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  --data-directory-temporary &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  --http-enabled &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  --https-enabled&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;false&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  --with-ui&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We wrote about this setup in &lt;strong&gt;&lt;a href=&#34;/resources/blog/local-development-setup&#34;&gt;Local Development Setup: EventSourcingDB in 5 Minutes&lt;/a&gt;&lt;/strong&gt; – it is not complicated, but it is exactly the kind of incantation you forget between projects. Now Claude remembers it for you. The instance it starts runs in &lt;strong&gt;development mode&lt;/strong&gt;: data lives in a temporary directory, the server speaks plain HTTP instead of HTTPS, the &lt;strong&gt;built-in management UI&lt;/strong&gt; is enabled, and the API token is simply &lt;code&gt;secret&lt;/code&gt;. A few seconds later, Claude reports back with the API base URL, the address of the UI, the token, and a reminder that everything is ephemeral.&lt;/p&gt;&#xA;&lt;p&gt;The Skill does more than fire off a command. Before starting anything, it &lt;strong&gt;checks that Docker is actually running&lt;/strong&gt;, looks for an existing development instance, and verifies that the port is free. If an instance is already up, Claude tells you so and hands you the connection details instead of starting a second one. If another process occupies port 3000, Claude asks you which port to use instead. And if you want a specific image version or a different token, you just say so – the defaults are defaults, not dogma.&lt;/p&gt;&#xA;&lt;p&gt;It is worth pausing on two of those defaults, because they shape what the instance is good for. The &lt;strong&gt;temporary data directory&lt;/strong&gt; means the database starts empty every single time – and that is a feature, not a limitation. Demos begin from a clean slate, test runs cannot contaminate each other, and you never have to wonder what leftover state from last Tuesday is skewing your experiment. And the &lt;strong&gt;management UI&lt;/strong&gt;, served on the same port as the API, gives you a second pair of eyes on the session: while you talk to Claude in the terminal, you can watch subjects and events appear in the browser, or try an EventQL query interactively. If you have not seen the UI yet, &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/management-ui&#34;&gt;Using the Management UI&lt;/a&gt;&lt;/strong&gt; gives you the tour.&lt;/p&gt;&#xA;&lt;p&gt;One thing the Skill is deliberately strict about: &lt;strong&gt;this setup is for development only.&lt;/strong&gt; Temporary storage and plain HTTP are wonderful for experiments and exactly wrong for production. When you are ready to deploy for real, &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/running-eventsourcingdb&#34;&gt;Running EventSourcingDB&lt;/a&gt;&lt;/strong&gt; covers the proper way.&lt;/p&gt;&#xA;&lt;h2 id=&#34;a-session-from-start-to-finish&#34;&gt;A Session from Start to Finish&lt;a class=&#34;anchor&#34; href=&#34;#a-session-from-start-to-finish&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;To see how the pieces fit together, let us play through a complete session. You open Claude Code in an empty directory, with nothing running.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Starting the database.&lt;/strong&gt; You type: &amp;quot;Start a local EventSourcingDB for me to experiment with.&amp;quot; Claude runs the preflight checks, starts the container, pings the instance to confirm it is healthy, and suggests exporting the connection settings so all other Skills pick them up automatically:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;ESDB_URL&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;http://localhost:3000&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;ESDB_API_TOKEN&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;secret&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Writing events.&lt;/strong&gt; You continue: &amp;quot;Acquire a book called &#39;2001 – A Space Odyssey&#39; by Arthur C. Clarke, and register a reader named Alice.&amp;quot; Claude asks you once for your event source and type prefix – more on that in a moment – then writes the events: a &lt;code&gt;book-acquired&lt;/code&gt; event for &lt;code&gt;/books/42&lt;/code&gt; and a &lt;code&gt;reader-registered&lt;/code&gt; event for &lt;code&gt;/readers/1&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Reading the history.&lt;/strong&gt; &amp;quot;What happened to /books/42 so far?&amp;quot; Claude reads the event stream for the subject and presents it: one acquisition event, with its timestamp, ID, and payload.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Asking questions.&lt;/strong&gt; &amp;quot;How many books does the library have?&amp;quot; Claude translates the question into an EventQL query, runs it, and answers. You never left natural language, and you never wrote a single line of JSON by hand.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Guarding the data.&lt;/strong&gt; Since this is a fresh instance, it is also the perfect moment to try schemas without consequences: &amp;quot;Register a schema for &lt;code&gt;book-acquired&lt;/code&gt; events that requires a title and an author, both as strings.&amp;quot; Claude registers the JSON Schema, and from then on the database rejects any &lt;code&gt;book-acquired&lt;/code&gt; event that does not match. Because the whole instance is disposable, you can iterate on the schema design freely – if you get it wrong, you restart and try again, instead of living with an immutable schema you regret.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Watching it live.&lt;/strong&gt; For the full effect, open a second terminal and ask Claude there: &amp;quot;Observe everything under /books.&amp;quot; Now write another event from the first terminal and watch it arrive in the second one in real time. The observe connection streams events as they happen and closes itself after a timeout, so you cannot accidentally leave a dangling connection behind. It is a small thing, but seeing an event you just described in plain English show up live in another window makes the architecture tangible in a way no diagram can.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Cleaning up.&lt;/strong&gt; Finally: &amp;quot;Stop the database again.&amp;quot; Claude reminds you that the container and all stored data will be gone – the instance was started with temporary storage, after all – and stops it. Your machine is exactly as clean as it was before the session.&lt;/p&gt;&#xA;&lt;p&gt;If you want to follow along yourself, installation takes less than a minute: add the marketplace with &lt;code&gt;/plugin marketplace add thenativeweb/claude-plugins&lt;/code&gt;, install the plugin with &lt;code&gt;/plugin install esdb@thenativeweb&lt;/code&gt;, and you are set. The &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/claude-code-plugin/getting-started&#34;&gt;Getting Started guide&lt;/a&gt;&lt;/strong&gt; walks you through the details, and the &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/claude-code-plugin/available-skills&#34;&gt;Available Skills page&lt;/a&gt;&lt;/strong&gt; lists everything the plugin can do.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-end-of-made-up-sources&#34;&gt;The End of Made-Up Sources&lt;a class=&#34;anchor&#34; href=&#34;#the-end-of-made-up-sources&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The second change in 1.1.0 is less flashy than a self-starting database, but at least as valuable in practice. It addresses a subtle problem that anyone who has used an LLM for structured work will recognize: &lt;strong&gt;left to its own devices, a language model invents plausible values.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Every event in EventSourcingDB carries a &lt;code&gt;source&lt;/code&gt; that identifies the system which produced it, and a &lt;code&gt;type&lt;/code&gt; in reverse domain notation, such as &lt;code&gt;io.eventsourcingdb.library.book-acquired&lt;/code&gt;. Both are free-form enough that Claude used to make reasonable but inconsistent choices. One write might use &lt;code&gt;https://library.eventsourcingdb.io&lt;/code&gt; as the source, the next one &lt;code&gt;tag:eventsourcingdb.io,2026:library&lt;/code&gt;, and a third one something else entirely. Each individual event was perfectly valid. Taken together, they were a mess – and since &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/sources&#34;&gt;sources&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/event-types&#34;&gt;event types&lt;/a&gt;&lt;/strong&gt; are precisely the fields you filter and query by, that mess had a cost.&lt;/p&gt;&#xA;&lt;p&gt;Version 1.1.0 introduces two session defaults: &lt;strong&gt;&lt;code&gt;ESDB_SOURCE&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;ESDB_EVENT_TYPE_PREFIX&lt;/code&gt;&lt;/strong&gt;. If they are set, every Skill that constructs events uses them. If they are not set, Claude asks you once – the first time a value is needed – and then &lt;strong&gt;sticks to your answer for the rest of the session&lt;/strong&gt; instead of improvising. To make the choice permanent across sessions, you export the variables alongside the connection settings:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;ESDB_SOURCE&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;https://library.eventsourcingdb.io&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;export&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;ESDB_EVENT_TYPE_PREFIX&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;io.eventsourcingdb.library&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The result is boring in the best possible way. Every event written in a session shares one source and one type prefix, exactly as if a disciplined developer had written them. Consistency is not glamorous, but it is what makes an event store queryable six months later.&lt;/p&gt;&#xA;&lt;p&gt;There is a broader lesson in this that goes beyond the plugin. &lt;strong&gt;Working with an LLM gets dramatically better when you move decisions out of the conversation and into configuration.&lt;/strong&gt; Every value the model does not have to choose is a value it cannot choose inconsistently. The session defaults apply that principle to the two fields where drift hurts the most – and they do it without ceremony, because an environment variable is the most boring configuration mechanism there is. If you have ever reviewed data an AI assistant produced over a long session and found three spellings of the same identifier, you know exactly which problem this solves.&lt;/p&gt;&#xA;&lt;h2 id=&#34;claude-knows-cloudevents-by-heart&#34;&gt;Claude Knows CloudEvents by Heart&lt;a class=&#34;anchor&#34; href=&#34;#claude-knows-cloudevents-by-heart&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The third improvement is invisible until you look at the quality of what Claude produces. The plugin now ships a shared &lt;strong&gt;CloudEvents reference&lt;/strong&gt; that all event-related Skills draw on. It teaches Claude which fields are yours to provide when writing – &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;subject&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, and &lt;code&gt;data&lt;/code&gt; – and which ones the server manages, such as &lt;code&gt;specversion&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;, and &lt;code&gt;time&lt;/code&gt;. It also covers the validation rules EventSourcingDB enforces, like subjects being slash-separated paths and &lt;code&gt;data&lt;/code&gt; being a JSON object.&lt;/p&gt;&#xA;&lt;p&gt;In practice, this means &lt;strong&gt;fewer rejected writes&lt;/strong&gt;. Claude no longer has to discover by trial and error that a subject may not contain spaces or that an event type needs its reverse domain structure. It gets the shape right on the first attempt, because the rules travel with the plugin.&lt;/p&gt;&#xA;&lt;p&gt;It also means better answers when reading. Events returned by EventSourcingDB carry &lt;strong&gt;extension attributes&lt;/strong&gt; beyond the CloudEvents standard fields: a &lt;code&gt;hash&lt;/code&gt;, a &lt;code&gt;predecessorhash&lt;/code&gt;, and a &lt;code&gt;signature&lt;/code&gt;. The hashes chain all events of the store together cryptographically, which is the foundation for tamper detection and auditing; the signature is set when the server is configured with a signing key. Claude now knows what these fields mean and can explain them in context instead of glossing over them. If you want the full picture of how events are structured, the &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/cloud-events&#34;&gt;CloudEvents documentation&lt;/a&gt;&lt;/strong&gt; has recently been extended with exactly these details.&lt;/p&gt;&#xA;&lt;p&gt;Alongside the CloudEvents reference, we consolidated the conventions that every Skill follows – how to handle streaming NDJSON responses, how to filter out heartbeats, how to spot an error that arrives mid-stream after the connection already reported success. Previously, each Skill carried its own copy of these instructions; now they live in one shared place. For you as a user, the visible effect is &lt;strong&gt;uniformity&lt;/strong&gt;: every Skill treats configuration, streaming, and errors the same way, and an error in a long-running observe connection is surfaced instead of silently swallowed. For us as maintainers, it means a fix or improvement lands in every Skill at once – which is, not coincidentally, the same argument we make for keeping domain knowledge in one place in your systems.&lt;/p&gt;&#xA;&lt;h2 id=&#34;from-speaking-to-serving&#34;&gt;From Speaking to Serving&lt;a class=&#34;anchor&#34; href=&#34;#from-speaking-to-serving&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;At Easter, Claude learned to speak EventSourcingDB. With version 1.1.0, it also sets the table: it brings its own database when you need one, keeps your events consistent without being told twice, and understands the event format deeply enough to get things right on the first try. The plugin has grown from a translator into something closer to a lab assistant – one that prepares the workbench, hands you the right tools, and cleans up afterwards.&lt;/p&gt;&#xA;&lt;p&gt;The nicest part is how little you need to know to benefit. &lt;strong&gt;The entire feature set of this release is reachable through ordinary sentences.&lt;/strong&gt; &amp;quot;Start a database for me.&amp;quot; &amp;quot;Write these events.&amp;quot; &amp;quot;How many books do we have?&amp;quot; &amp;quot;Stop it again.&amp;quot; Everything between those sentences – Docker flags, port checks, event formatting, EventQL – is handled for you.&lt;/p&gt;&#xA;&lt;p&gt;If you already use the plugin, version 1.1.0 is available from the &lt;strong&gt;thenativeweb marketplace&lt;/strong&gt; now. If you have not tried it yet, the &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/claude-code-plugin/getting-started&#34;&gt;Getting Started guide&lt;/a&gt;&lt;/strong&gt; takes you from zero to your first conversation in about a minute. And we are genuinely curious where this should go next: which Skill is missing, what would make your terminal sessions smoother, what should Claude learn about EventSourcingDB that it does not know yet? Tell us at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt; – the best ideas for 1.2.0 will come from the people using 1.1.0.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/opencqrs-2-0</id>
    <title>OpenCQRS 2.0: Tests That Read Like the Domain</title>
    <link href="https://www.eventfoundation.io/resources/blog/opencqrs-2-0"></link>
    <published>2026-07-06T00:00:00+02:00</published>
    <updated>2026-07-06T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">In most software, testing is something you bolt on after the fact: you write the code, then you write tests to convince yourself the code does what you hoped. Event Sourcing quietly inverts that relationship. Because behavior is expressed as events, a test can read like a sentence about the domain: given these past events, when this command arrives, then these new events should follow. The test stops being scaffolding around the code and becomes the specification of what the system is supposed to do.</summary>
    <content type="html">&lt;p&gt;In most software, testing is something you bolt on after the fact: you write the code, then you write tests to convince yourself the code does what you hoped. Event Sourcing quietly inverts that relationship. &lt;strong&gt;Because behavior is expressed as events, a test can read like a sentence about the domain: given these past events, when this command arrives, then these new events should follow.&lt;/strong&gt; The test stops being scaffolding around the code and becomes the specification of what the system is supposed to do.&lt;/p&gt;&#xA;&lt;p&gt;On June 26th, our friends at &lt;strong&gt;&lt;a href=&#34;https://www.digitalfrontiers.de/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Digital Frontiers&lt;/a&gt;&lt;/strong&gt; shipped &lt;strong&gt;&lt;a href=&#34;https://www.opencqrs.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;OpenCQRS 2.0&lt;/a&gt;&lt;/strong&gt;, and the change we keep coming back to leans all the way into that idea. The release modernizes plenty under the hood, but the part that matters most day to day is the redesigned testing support – a fluent DSL that makes those given/when/then specifications read less like test code and more like a conversation with the domain. Let&#39;s start with why that conversation is worth having at all.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;why-event-sourcing-tests-are-different&#34;&gt;Why Event Sourcing Tests Are Different&lt;a class=&#34;anchor&#34; href=&#34;#why-event-sourcing-tests-are-different&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Think about how you test a typical state-based service. You set up a database, or you mock the repository that talks to it. You arrange rows, call a method, then read the rows back and assert on them. Half the test is infrastructure, and the mocks encode your assumptions about collaborators rather than the behavior you actually care about. When such a test breaks, it is often the scaffolding that broke, not the logic.&lt;/p&gt;&#xA;&lt;p&gt;Event Sourcing removes most of that ceremony. A command handler in this world is close to a pure function: &lt;strong&gt;you feed it the events that already happened and the command that just arrived, and it decides which new events should be appended.&lt;/strong&gt; There is no hidden state, no clock you cannot control, and no network in the middle. The same inputs always produce the same decision.&lt;/p&gt;&#xA;&lt;p&gt;That property is a gift for testing. &lt;strong&gt;A test can mirror the handler exactly: arrange the past as a list of events, act by sending a command, and assert on the events that come out.&lt;/strong&gt; There is nothing to mock, because there are no collaborators standing between you and the decision. We have made this case before in &lt;strong&gt;&lt;a href=&#34;/resources/blog/testing-without-mocks&#34;&gt;Testing Without Mocks&lt;/a&gt;&lt;/strong&gt;, and it holds especially well here, where the events are both the input and the output.&lt;/p&gt;&#xA;&lt;p&gt;There is a second benefit that is easy to miss. &lt;strong&gt;A test written this way is documentation that cannot rot, because it runs.&lt;/strong&gt; A new teammate reading &amp;quot;given a confirmed order, when a shipment is requested, then an &lt;code&gt;OrderShipped&lt;/code&gt; event follows&amp;quot; learns the rule and gets proof that the rule still holds, in the same breath. The specification and the evidence that the code obeys it are the same artifact, which is something a state-based test full of mocks can rarely claim.&lt;/p&gt;&#xA;&lt;p&gt;It also explains why a CQRS framework lives or dies by its testing story. If the whole point is that behavior is expressed as events, then the way you write tests is the way you talk about behavior. Frank Scheffler made exactly this argument when we sat down with him; if you want the background on how OpenCQRS approaches CQRS without forcing the aggregate pattern, our &lt;strong&gt;&lt;a href=&#34;/resources/blog/rethinking-cqrs-an-interview-on-opencqrs&#34;&gt;interview on OpenCQRS&lt;/a&gt;&lt;/strong&gt; is a good place to start.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-old-way-worked-but-the-phases-were-implicit&#34;&gt;The Old Way Worked, but the Phases Were Implicit&lt;a class=&#34;anchor&#34; href=&#34;#the-old-way-worked-but-the-phases-were-implicit&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;OpenCQRS 1.0 already understood this. Its test fixture let you write a given/when/then test as a single chain of method calls, and for the simplest cases it read just fine:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;fixture&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;givenNothing&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;when&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;expectSuccessfulExecution&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;expectSingleEvent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This did the job. The friction was subtler than any missing feature: &lt;strong&gt;every assertion began with the same &lt;code&gt;expect&lt;/code&gt; prefix, and that repetition was the visible symptom of a structural issue underneath.&lt;/strong&gt; Setup, execution, and assertion all hung off one flat surface, so the given/when/then phases the test was meant to express were never actually separated in the code. They were a convention you kept in mind, not a structure the API enforced.&lt;/p&gt;&#xA;&lt;p&gt;That is the problem the redesign went after. A test that needs to say &amp;quot;given a book that is already lent, when someone tries to borrow it, then the command fails with a specific exception&amp;quot; should be assembled from three distinct steps that mirror that sentence – arrange, act, assert – rather than a single chain in which the boundaries between them are left for the reader to infer.&lt;/p&gt;&#xA;&lt;h2 id=&#34;a-dsl-that-reads-like-the-domain&#34;&gt;A DSL That Reads Like the Domain&lt;a class=&#34;anchor&#34; href=&#34;#a-dsl-that-reads-like-the-domain&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;OpenCQRS 2.0 replaces the old fixture API with a fluent Given/When/Then DSL&lt;/strong&gt;, and the difference is immediately visible. The same simple test now reads as three clearly separated phases:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;fixture&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;given&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;nothing&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;when&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;succeeds&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;allEvents&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;single&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;comparing&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;expectedEvent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Read it top to bottom and it narrates itself: given nothing, when this command, then it succeeds and produces exactly one event equal to the one you expected. &lt;strong&gt;The phases are no longer a flat chain&lt;/strong&gt; – &lt;code&gt;given()&lt;/code&gt; opens the arrangement, &lt;code&gt;when()&lt;/code&gt; performs the act, and the assertions that follow describe the outcome.&lt;/p&gt;&#xA;&lt;p&gt;Getting a fixture is just as low-ceremony. You annotate a test class with &lt;code&gt;@CommandHandlingTest&lt;/code&gt; and let Spring inject a &lt;code&gt;CommandHandlingTestFixture&lt;/code&gt; for the command you want to exercise. &lt;strong&gt;The slice wires up only what command handling needs&lt;/strong&gt;, so the test starts fast and stays focused on behavior, and the DSL takes over from there – no mock, no database, no broker in sight.&lt;/p&gt;&#xA;&lt;p&gt;The arrangement is where the new DSL earns its keep. Instead of a single &lt;code&gt;givenNothing()&lt;/code&gt;, you can describe a real history: choose the subject you are writing to, append events one after another, pin the time, and even advance the clock between them. That makes the awkward scenario from earlier read almost word for word like the domain sentence it describes:&lt;/p&gt;&#xA;&lt;pre class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;fixture&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;given&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;usingSubject&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;/books/4711&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;event&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;e&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;e&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;payload&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;BookLentEvent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;4711&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;reader-1&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;when&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;BorrowBookCommand&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;4711&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;reader-2&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;fails&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;throwing&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;BookAlreadyLentException&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;class&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;The outcome side is just as expressive as the arrangement side.&lt;/strong&gt; Beyond &lt;code&gt;succeeds()&lt;/code&gt; you can assert that a command &lt;code&gt;fails()&lt;/code&gt;, and then say how: &lt;code&gt;throwing(...)&lt;/code&gt; for a specific exception, or &lt;code&gt;violatingExactly(...)&lt;/code&gt; when a consistency condition such as &lt;code&gt;PRISTINE&lt;/code&gt; was the reason it was rejected. On the success path, you can drill into the produced events with &lt;code&gt;allEvents().single(...)&lt;/code&gt;, or assert on the rebuilt state with &lt;code&gt;havingState(...)&lt;/code&gt; and &lt;code&gt;stateSatisfying(...)&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Time deserves a special mention. Plenty of domain rules are temporal – an offer expires, a grace period elapses, a rate limit resets – and testing them usually means fighting the system clock. The new arrangement lets you set an explicit starting instant with &lt;code&gt;time(...)&lt;/code&gt; and advance it by a fixed &lt;code&gt;timeDelta(...)&lt;/code&gt; between events, so a scenario like &amp;quot;given a reservation made an hour ago&amp;quot; can be stated directly rather than faked. &lt;strong&gt;Deterministic time turns flaky temporal tests into ordinary ones.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;None of this changes what a test is for. &lt;strong&gt;It changes how close the test gets to the language you would use to describe the behavior out loud&lt;/strong&gt; – which, in a system whose entire premise is expressing behavior as events, is exactly the point.&lt;/p&gt;&#xA;&lt;h2 id=&#34;tests-are-only-as-good-as-your-consistency-rules&#34;&gt;Tests Are Only as Good as Your Consistency Rules&lt;a class=&#34;anchor&#34; href=&#34;#tests-are-only-as-good-as-your-consistency-rules&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;A test is a promise, and a promise is only worth something if reality keeps it. &lt;strong&gt;A given/when/then test that asserts a command must fail when a book is already lent is only meaningful if the running system actually refuses that write.&lt;/strong&gt; This is where the rest of OpenCQRS 2.0 quietly supports the testing story.&lt;/p&gt;&#xA;&lt;p&gt;OpenCQRS leans on EventSourcingDB&#39;s preconditions to enforce consistency at the moment of writing, and 2.0 makes that vocabulary richer. There are two new preconditions: &lt;code&gt;SubjectIsPopulated&lt;/code&gt;, which requires that a subject already has events, and &lt;code&gt;EventQlQueryIsTrue&lt;/code&gt;, which lets a write depend on the result of an EventQL query. Together they let you express conditions that used to live scattered across application code as guarantees the database itself checks.&lt;/p&gt;&#xA;&lt;p&gt;Consider a rule like &amp;quot;a member may hold at most three loans at once.&amp;quot; Before, you would source the member&#39;s events, count the active loans in application code, and hope no concurrent command slipped a fourth one in between your check and your write. With &lt;code&gt;EventQlQueryIsTrue&lt;/code&gt;, the limit becomes part of the write itself: the new events are appended only if the query still holds at commit time. &lt;strong&gt;The race condition you used to reason about nervously simply cannot happen&lt;/strong&gt; – and the test that asserts the limit is exercising the very same guarantee.&lt;/p&gt;&#xA;&lt;p&gt;The framework now maps a command&#39;s intent onto those preconditions more precisely. A handler that declares its subject must already exist is backed by &lt;code&gt;SubjectIsPopulated&lt;/code&gt; at the final write; one that requires a pristine subject is backed by &lt;code&gt;SubjectIsPristine&lt;/code&gt;; and the sourcing mode decides exactly which subjects get optimistic-concurrency checks. &lt;strong&gt;Command handlers can also attach their own preconditions when they publish&lt;/strong&gt;, including for command-relative subjects, so a single command can assert a fact about a different part of the stream hierarchy before it commits.&lt;/p&gt;&#xA;&lt;p&gt;The payoff for testing is subtle but important. &lt;strong&gt;The rules you assert in a given/when/then test are the same rules EventSourcingDB enforces at the write&lt;/strong&gt; – not a parallel reimplementation that can drift out of sync. We have argued that &lt;strong&gt;&lt;a href=&#34;/resources/blog/consistency-is-a-business-decision&#34;&gt;consistency is a business decision&lt;/a&gt;&lt;/strong&gt; rather than a technical afterthought, and preconditions are how that decision becomes executable. If you want the mechanics, the &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/preconditions&#34;&gt;preconditions documentation&lt;/a&gt;&lt;/strong&gt; walks through each one.&lt;/p&gt;&#xA;&lt;h2 id=&#34;when-the-test-becomes-the-spec&#34;&gt;When the Test Becomes the Spec&lt;a class=&#34;anchor&#34; href=&#34;#when-the-test-becomes-the-spec&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;We opened with the claim that in Event Sourcing, the test is the specification rather than an afterthought. &lt;strong&gt;OpenCQRS 2.0 is what it looks like when a framework takes that claim literally&lt;/strong&gt; – a testing DSL that reads like a description of behavior, backed by consistency rules the database enforces so those descriptions stay true. The platform work underneath keeps it current; the testing work on top is what makes it a pleasure to use.&lt;/p&gt;&#xA;&lt;p&gt;It is also another sign of the ecosystem we always hoped EventSourcingDB would grow. When we &lt;strong&gt;&lt;a href=&#34;/resources/blog/celebrating-opencqrs-1-0&#34;&gt;celebrated OpenCQRS 1.0&lt;/a&gt;&lt;/strong&gt;, the milestone was that the framework existed at all. With 2.0, the story is maturity – the same vision, refined by the teams running it in production.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-else-shipped&#34;&gt;What Else Shipped&lt;a class=&#34;anchor&#34; href=&#34;#what-else-shipped&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Two more additions are worth calling out. The client gained &lt;code&gt;readSubjects&lt;/code&gt;, which lists the subjects beneath a base subject – a small API with outsized value for hierarchical streams, where discovering what exists under &lt;code&gt;/books&lt;/code&gt; or &lt;code&gt;/orders&lt;/code&gt; is suddenly a single call. Think of a reporting job that needs to walk every order placed today, or an admin screen that lists the active loans for a member without you keeping a separate index: instead of guessing identifiers or maintaining a side lookup, you ask the store what is there. It is the kind of thing that makes diagnostics and administrative tooling far easier to build, and it leans directly on one of EventSourcingDB&#39;s quieter strengths: streams that nest.&lt;/p&gt;&#xA;&lt;p&gt;The other theme is modernization. &lt;strong&gt;OpenCQRS 2.0 moves onto a current foundation&lt;/strong&gt; – Spring Boot 4.1, Jackson 3, Gradle 9, and EventSourcingDB 1.2, with the event-processor autoconfiguration adopting Spring&#39;s new bean registration API and the codebase picking up JSpecify nullness annotations. If you want to know what arrived in the database release it now builds on, our &lt;strong&gt;&lt;a href=&#34;/resources/blog/announcing-eventsourcingdb-1-2-0&#34;&gt;EventSourcingDB 1.2.0 announcement&lt;/a&gt;&lt;/strong&gt; has the details.&lt;/p&gt;&#xA;&lt;p&gt;That last item is quieter than the version bumps, but it earns a moment. JSpecify is an emerging, JVM-wide standard for expressing which references may be &lt;code&gt;null&lt;/code&gt; and which may not, in a form that build tools, IDEs, and the Kotlin compiler all understand. &lt;strong&gt;The payoff shows up right at the boundary between your code and the framework:&lt;/strong&gt; because OpenCQRS annotates its public API, your null-checker knows exactly where a &lt;code&gt;null&lt;/code&gt; is allowed and where it is not, and can flag at compile time the mistakes that would otherwise surface as a &lt;code&gt;NullPointerException&lt;/code&gt; at runtime – and for Kotlin callers, that same boundary stops leaking unchecked platform types.&lt;/p&gt;&#xA;&lt;p&gt;A major version means a few sharp edges on the way up. The biggest one is, fittingly, the tests: the old fixture API is gone, so existing tests move to the new DSL. The Jackson 3 upgrade also shifts serializer packages from &lt;code&gt;com.fasterxml.jackson&lt;/code&gt; to &lt;code&gt;tools.jackson&lt;/code&gt;, which matters if you wrote custom marshallers. There is no separate migration guide to point you at, but it is worth saying plainly: &lt;strong&gt;the rewrite that costs you the most effort is the one that leaves your tests reading better than they did before.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;If you build on the JVM and want your tests to read like the domain they describe, this is a good moment to look. &lt;strong&gt;&lt;a href=&#34;https://www.opencqrs.com&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;OpenCQRS 2.0&lt;/a&gt;&lt;/strong&gt; is available now, and if you are starting from scratch, the fastest path is to bring up EventSourcingDB first: &lt;strong&gt;&lt;a href=&#34;/docs/eventsourcingdb/installing-eventsourcingdb&#34;&gt;installing EventSourcingDB&lt;/a&gt;&lt;/strong&gt; takes about a minute, and from there the given/when/then loop is yours to try. We&#39;d love to hear how it reads in your own domain – tell us at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/use-model-with-domain-an-interview-on-domain-storytelling</id>
    <title>Use Model With Domain: An Interview on Domain Storytelling</title>
    <link href="https://www.eventfoundation.io/resources/blog/use-model-with-domain-an-interview-on-domain-storytelling"></link>
    <published>2026-07-02T00:00:00+02:00</published>
    <updated>2026-07-02T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <author>
      <name>Henning Schwentner</name>
    </author>
    <author>
      <name>Dr. Stefan Hofer</name>
    </author>
    <summary type="text">Golo: Henning, Stefan, you are both working at WPS – Workplace Solutions and you are the creators of Domain Storytelling, a collaborative modeling technique that has gained a lot of attention in the Domain-Driven Design community. Before we dive into the method itself, can you tell us a bit about your backgrounds and how Domain Storytelling came to life?</summary>
    <content type="html">&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Henning, Stefan, you are both working at &lt;strong&gt;&lt;a href=&#34;https://www.wps.de/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;WPS – Workplace Solutions&lt;/a&gt;&lt;/strong&gt; and you are the creators of Domain Storytelling, a collaborative modeling technique that has gained a lot of attention in the Domain-Driven Design community. Before we dive into the method itself, can you tell us a bit about your backgrounds and how Domain Storytelling came to life?&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;p&gt;&lt;strong&gt;Stefan:&lt;/strong&gt; The predecessor of Domain Storytelling had been developed at the University of Hamburg and WPS, a university spin-off. When I joined WPS in 2005, my colleagues had been using the method in requirements workshops with clients. It went without saying that the diagrams were the backbone of our requirements and domain models. Since this was my first job as a professional software developer, it took me some time to realize that this was something special. It seemed like no one else on the planet worked like we did – until DDD started to gain adoption around 2015.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Henning:&lt;/strong&gt; And that&#39;s when Domain Storytelling came to life. We simplified the academic method, gave it a catchy name and started talking about it at meetups and smaller conferences. 2018 was our break-out year with talks and workshops at all DDD conferences and the release of our open-source modeling tool &lt;strong&gt;&lt;a href=&#34;https://egon.io/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Egon.io&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-origins-of-domain-storytelling&#34;&gt;The Origins of Domain Storytelling&lt;a class=&#34;anchor&#34; href=&#34;#the-origins-of-domain-storytelling&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Domain Storytelling has a very distinctive visual language – pictographic icons, arrows, and numbered sentences that read almost like a storyboard. It reminds me of the classic LucasArts point-and-click adventures like &lt;em&gt;Monkey Island&lt;/em&gt; or &lt;em&gt;Indiana Jones and the Fate of Atlantis&lt;/em&gt;, where every interaction was built around actors, objects, and activities. Was there any such inspiration behind the visual style, or how did you arrive at this particular notation?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Stefan:&lt;/strong&gt; The pictographic language was already part of Domain Storytelling&#39;s academic predecessor. It was inspired by &lt;strong&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Rich_picture&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Rich Pictures&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Henning:&lt;/strong&gt; We strengthened the &amp;quot;storytelling&amp;quot; aspect: To us, the notation is not just icons and arrows – it consists of &lt;em&gt;sentences&lt;/em&gt; with a &lt;em&gt;subject&lt;/em&gt;, &lt;em&gt;verb&lt;/em&gt;, and &lt;em&gt;objects&lt;/em&gt;. The basic structure is &lt;em&gt;who&lt;/em&gt; (an &lt;em&gt;actor&lt;/em&gt;) does &lt;em&gt;what&lt;/em&gt; (&lt;em&gt;activity&lt;/em&gt;) with &lt;em&gt;what&lt;/em&gt; (&lt;em&gt;work object&lt;/em&gt;) with &lt;em&gt;whom&lt;/em&gt; (another &lt;em&gt;actor&lt;/em&gt;). A &lt;em&gt;story&lt;/em&gt; consists of several sentences that are ordered by &lt;em&gt;sequence numbers&lt;/em&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; For those who haven&#39;t used Domain Storytelling yet – can you walk us through the basic mechanics? What does a typical Domain Storytelling workshop look like?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Henning:&lt;/strong&gt; The workshop brings people together: people with questions (like developers and business analysts) and people with domain knowledge – our &lt;em&gt;storytellers&lt;/em&gt;. They learn from each other in a structured conversation, led by a &lt;em&gt;moderator&lt;/em&gt;. The moderator visualizes the conversation as a diagram – live, while the conversation takes place.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Stefan:&lt;/strong&gt; The storytellers tell one scenario at a time. A scenario is a concrete, meaningful example of a business process. Usually very few domain stories are enough to grasp a business process.&lt;/p&gt;&#xA;&lt;h2 id=&#34;in-practice&#34;&gt;In Practice&lt;a class=&#34;anchor&#34; href=&#34;#in-practice&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Many people in the DDD community are familiar with Event Storming as a collaborative modeling technique. How does Domain Storytelling differ from Event Storming, and when would you recommend one over the other? Or do they complement each other?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Stefan:&lt;/strong&gt; People are surprised when I tell them that I use Event Storming almost as often as Domain Storytelling. In some cases, it does not even matter which modeling technique is used as long as it facilitates the conversation and a shared visualization is created. Yet, we recommend Domain Storytelling over Event Storming if the domain involves many actors (people or software) and you want to investigate how these actors cooperate with each other. Also, in my experience it is easier to design to-be processes with Domain Storytelling because a common perspective emerges more easily from a moderated, sentence-by-sentence approach.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Henning:&lt;/strong&gt; If you know Event Storming, the first difference that you will notice is the layout of the model. Domain stories do not follow a left-to-right timeline, because they need to show cooperation between actors. That works better with arrows. Another difference is that each domain story is about one scenario.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Stefan:&lt;/strong&gt; The workshop is also different. In Domain Storytelling, iterations of storming (discussing the next sentence) and consolidation (recording that sentence) are much smaller.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Can you walk us through a concrete example of how Domain Storytelling played out in a real project? What was the domain, what did the workshop look like, and what insights emerged that might not have surfaced otherwise?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Stefan:&lt;/strong&gt; Some time ago, I worked on a project which implemented a requirements specification: 80 use cases, 300 pages in total. Our customer&#39;s business analyst had done a great job, but as soon as we started working on the software, my colleagues and I realized that the use cases were not sufficient to really understand the domain. We did not know enough about the business processes and the context of the requirements. At our first meeting with the client&#39;s team I modeled three scenarios that covered the existing solution. This helped us to gain a better understanding of the shortcomings of the existing solution, which motivated several of the requirements. When we all met again about a month later, we used the same three scenarios as we did the first time, except that we had the domain experts tell us about the to-be processes. Now, we understood the interplay of the use cases. Another three months and several implemented use cases later, we had reached a point where we had to think about going live with the first release.&lt;/p&gt;&#xA;&lt;p&gt;Our customer had planned an incremental rollout. Some users would switch to our new software, and others would continue to use the old software. To make that possible, we had to enable collaboration between the two software systems. We needed to design a collaborative, software-supported workflow, and again we chose Domain Storytelling for that purpose. During the workshop, we realized that the newly designed, software-supported workflow was not backed by the requirements document. However, it was easy to derive new requirements from the domain story. Basically, we just had to transcribe the activities into text form.&lt;/p&gt;&#xA;&lt;h2 id=&#34;domain-storytelling-meets-ddd-and-event-sourcing&#34;&gt;Domain Storytelling Meets DDD and Event Sourcing&lt;a class=&#34;anchor&#34; href=&#34;#domain-storytelling-meets-ddd-and-event-sourcing&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; One of the big challenges in DDD is finding the right Bounded Contexts. How can Domain Storytelling help teams identify and validate their Bounded Context boundaries? Do you have a systematic approach for this?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Henning:&lt;/strong&gt; Existing boundaries (systems, teams, and departments) confine people&#39;s ability to design good Bounded Contexts. We recommend that you first model a &amp;quot;pure&amp;quot; version of your business processes, i.e., leave out existing systems and just focus on what needs to be done to make the process work. Then, we ask the domain experts which activities belong together because they serve a common goal. We then visually group the activities that belong together. The result is not a Bounded Context yet, but a &lt;em&gt;Subdomain&lt;/em&gt; – and a good starting point for Bounded Contexts. There&#39;s more to the design of Bounded Contexts, but that would go beyond what we can discuss in this article.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Event Sourcing is all about capturing what happened in a domain as a sequence of events. Do you see a natural bridge between Domain Storytelling and Event Sourcing? For example, can the stories that emerge from a workshop help teams discover their domain events and event flows?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Stefan:&lt;/strong&gt; You &lt;em&gt;can&lt;/em&gt; model events, commands, and views as &lt;em&gt;work objects&lt;/em&gt; with Domain Storytelling. But it does not have &amp;quot;native&amp;quot; support for Event Sourcing, like Event Storming (on the design level) or Event Modeling do. However, it makes sense to combine one of these techniques with Domain Storytelling. Here is an example from the insurance domain: a self-service web app that allows companies to sign their employees up for a corporate insurance policy.&lt;/p&gt;&#xA;&lt;p&gt;We used Domain Storytelling for the &amp;quot;broad strokes&amp;quot;. What is the purpose of the software that we are building? Who is supposed to use it? Does it require or provide data from/to other systems? What domain language(s) do stakeholders use? It also helped us define what is within the scope of the project (e.g., simple plausibility checks) and what is out of scope (e.g., elaborate data verification).&lt;/p&gt;&#xA;&lt;p&gt;Next, we decided how to slice the story for development. Usually, one to three sentences make a good slice. Then, we did a design-level Event Storming for the first slice and implemented it. Then, we modeled and implemented the next slice, etc.&lt;/p&gt;&#xA;&lt;h2 id=&#34;getting-started-and-looking-ahead&#34;&gt;Getting Started and Looking Ahead&lt;a class=&#34;anchor&#34; href=&#34;#getting-started-and-looking-ahead&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; You have built &lt;strong&gt;&lt;a href=&#34;https://egon.io/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Egon.io&lt;/a&gt;&lt;/strong&gt;, an open-source tool for Domain Storytelling. Can you tell us a bit about it and how it fits into the workflow? Are there other tools or resources you recommend for teams getting started?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Stefan:&lt;/strong&gt; You can model domain stories with sticky notes and pen, digital drawing tools, and digital whiteboards. That&#39;s okay for occasional modeling. But eventually, you want a tool that makes modeling easy and fast. Egon.io is that tool because it implements the notation and grammar of Domain Storytelling. And it runs in your browser, is free, does not require an account, does not track you, and does not process or store your data.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Henning:&lt;/strong&gt; A good resource for getting started is &lt;strong&gt;&lt;a href=&#34;https://domainstorytelling.org/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;domainstorytelling.org&lt;/a&gt;&lt;/strong&gt; and of course our &lt;strong&gt;&lt;a href=&#34;https://domainstorytelling.org/book&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;book&lt;/a&gt;&lt;/strong&gt; published by Addison-Wesley.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; If a team wanted to try Domain Storytelling for the first time next week, what would be your top three tips to make their first workshop a success?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Henning:&lt;/strong&gt; First, invite the right people. Real domain experts (not just proxies). Second, agree on a scenario. If the discussion drifts off, come back to the scenario. Third, agree if you model an as-is or to-be scenario and on a granularity (coarse-grained overview, fine-grained details, or something in between).&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; And looking ahead – what&#39;s next for Domain Storytelling? Are there new ideas, tools, or directions you&#39;re currently exploring?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Stefan:&lt;/strong&gt; Egon is actively being developed. The community has already contributed PlantUML support for Domain Stories, and an Egon-based extension for VS Code is in early development. Rumor has it that Mermaid might be next.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Henning:&lt;/strong&gt; Obviously, AI is a big driver of ideas. Domain stories are used to feed domain knowledge into LLMs, e.g., to generate mock-ups and APIs.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Golo:&lt;/strong&gt; Henning, Stefan, thank you so much for sharing your insights on Domain Storytelling with us. It&#39;s been a fascinating conversation, and I&#39;m sure our readers will find plenty of inspiration to try it out in their own projects.&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;Henning Schwentner is a coder, coach, and consultant at &lt;strong&gt;&lt;a href=&#34;https://www.wps.de/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;WPS – Workplace Solutions&lt;/a&gt;&lt;/strong&gt; in Hamburg, Germany, and one of the creators of Domain Storytelling. For more information or to get in touch, visit the &lt;strong&gt;&lt;a href=&#34;https://www.wps.de/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;WPS website&lt;/a&gt;&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;em&gt;Dr. Stefan Hofer is a software developer and consultant at &lt;strong&gt;&lt;a href=&#34;https://www.wps.de/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;WPS – Workplace Solutions&lt;/a&gt;&lt;/strong&gt; in Hamburg, Germany, and one of the creators of Domain Storytelling. For more information or to get in touch, visit the &lt;strong&gt;&lt;a href=&#34;https://www.wps.de/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;WPS website&lt;/a&gt;&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/too-many-islands-too-few-bridges</id>
    <title>Too Many Islands, Too Few Bridges: Notes from the Event Modeling Conference 2026</title>
    <link href="https://www.eventfoundation.io/resources/blog/too-many-islands-too-few-bridges"></link>
    <published>2026-06-29T00:00:00+02:00</published>
    <updated>2026-06-29T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">Every once in a while, you walk into a room and feel a particular kind of energy: the sense that a community is standing at the edge of something larger than itself. The first time I experienced this was in September 2011, in Brescia, at the first Node.js Conference Italy: around 250 people from across Europe, convinced they were looking at the next big thing in server-side software. I wrote an article about it back then, every bit as caught up in it as the people around me. As it turned out, we were right.</summary>
    <content type="html">&lt;p&gt;Every once in a while, you walk into a room and feel a particular kind of energy: the sense that a community is standing at the edge of something larger than itself. The first time I experienced this was in September 2011, in Brescia, at the first Node.js Conference Italy: around 250 people from across Europe, convinced they were looking at the next big thing in server-side software. &lt;strong&gt;&lt;a href=&#34;https://www.heise.de/hintergrund/Berechtigte-Chancen-fuer-Node-js-als-naechstes-grosses-Ding-1352310.html&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;I wrote an article about it back then&lt;/a&gt;&lt;/strong&gt;, every bit as caught up in it as the people around me. As it turned out, we were right.&lt;/p&gt;&#xA;&lt;p&gt;The second time was last week, in a converted industrial space in Munich, at the two-day &lt;strong&gt;&lt;a href=&#34;https://www.nebulit.de/eventmodeling-conference-2026&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Event Modeling Conference 2026&lt;/a&gt;&lt;/strong&gt;, with around forty other people. The same restlessness, the same sense of momentum. And one question that kept resurfacing all day, in talks and in hallway conversations alike. By the end of the second day, that question had a name, scribbled across a flip chart and signed by nearly everyone in the room.&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;figure&gt;&#xA;&lt;img src=&#34;/resources/blog/images/golo-roden-at-event-modeling-conference-2026.jpg&#34; alt=&#34;Golo Roden at the Event Modeling Conference 2026&#34; loading=&#34;lazy&#34; /&gt;&#xA;&lt;figcaption&gt;Golo Roden at the Event Modeling Conference 2026&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&lt;h2 id=&#34;forty-people-in-a-room-in-munich&#34;&gt;Forty People in a Room in Munich&lt;a class=&#34;anchor&#34; href=&#34;#forty-people-in-a-room-in-munich&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The conference took place at the Impact Hub Munich, a former industrial site with exposed concrete and a comfortably improvised feel. It was sold out, yet still small enough that you could realistically meet everyone over the course of two days. Nebulit organized it, and Martin Dilger held the whole thing together as host, something he did remarkably well.&lt;/p&gt;&#xA;&lt;p&gt;The format was deliberately varied: keynotes, talks, hands-on workshops, open-space sessions, and a marketplace of short pitches, all woven together across the two days. What I appreciated most was how much breathing room there was around the sessions. Between talks, there was more than enough time to network and, just as importantly, to disappear into a side room with a handful of people and dig into a single topic in a small circle. People used that opportunity eagerly, and some of the best moments of the conference happened in exactly those side rooms.&lt;/p&gt;&#xA;&lt;p&gt;The conference revolves around Event Modeling, the method Adam Dymitruk has done so much to shape: a way of describing how information flows through a system over time, blueprint-style, before a line of code is written. If you want a gentler introduction to the idea, we explored it in &lt;strong&gt;&lt;a href=&#34;/resources/blog/blueprinting-the-system-an-interview-on-event-modeling&#34;&gt;an interview on Event Modeling&lt;/a&gt;&lt;/strong&gt; a while ago. Adam opened the conference with a keynote. Unfortunately, he couldn&#39;t be there in person and had to join by video. We spend a fair amount of time at gatherings like this; not long ago we wrote about &lt;strong&gt;&lt;a href=&#34;/resources/blog/kandddinsky-2025&#34;&gt;our first KanDDDinsky&lt;/a&gt;&lt;/strong&gt;. But this one had a different texture, and most of it came down to a single, nagging question.&lt;/p&gt;&#xA;&lt;h2 id=&#34;too-many-islands-too-few-bridges&#34;&gt;Too Many Islands, Too Few Bridges&lt;a class=&#34;anchor&#34; href=&#34;#too-many-islands-too-few-bridges&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;That question, in one form or another, was this: &lt;strong&gt;why is it still so hard to find your way into this field?&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;Because the field that calls itself Event Sourcing, CQRS, and Domain-Driven Design isn&#39;t really one field. It is an archipelago. There are many schools and many flavors of how to do this work. Do you model with aggregates or with dynamic consistency boundaries? Do you do Event Storming, model using Event Modeling, or tell stories with Domain Storytelling? And even the words differ: is it an event or a domain event, a command or an intent, a read model or a projection? Each camp has its own vocabulary, its own preferred tools, and its own conventions, and each is internally coherent and externally hard to reconcile with the others.&lt;/p&gt;&#xA;&lt;p&gt;The fragmentation reaches further than methodology. It runs all the way down to the examples we teach with. For example, WPS reaches for a cinema box office to explain Domain Storytelling. Bastian Waidelich and Sara Pellegrini, in contrast, use a student enrolling in courses. We at the native web, for our part, usually tell the story of a city library. Three teachers, three canonical &amp;quot;hello world&amp;quot; examples, and a newcomer left to work out, entirely on their own, that &lt;strong&gt;all of these are really describing the same handful of underlying ideas&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;It shows up in tooling, too. One team builds on Axon, another on a framework it built in-house and never published; we build on EventSourcingDB, with &lt;strong&gt;&lt;a href=&#34;/resources/blog/celebrating-opencqrs-1-0&#34;&gt;OpenCQRS&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href=&#34;/resources/blog/welcoming-nimbus-event-driven-development-for-typescript&#34;&gt;Nimbus&lt;/a&gt;&lt;/strong&gt;. Each choice is reasonable on its own island, and each comes with assumptions that quietly leak into how people think the whole field works. The result is that what you learn in one camp is only ever a slice of the whole, and it takes a very long time to figure out who belongs where and how the pieces actually fit together.&lt;/p&gt;&#xA;&lt;p&gt;That cost lands squarely on the people we should most want to welcome. There is remarkably little that we, as a community, have actually agreed on. We all do something we call Domain-Driven Design, with something event-shaped around it, but there are almost no shared standards and almost no shared examples to point a beginner toward. This is exactly the spirit behind something we have argued before: that &lt;strong&gt;&lt;a href=&#34;/resources/blog/great-minds-should-not-think-alike-they-should-think-together&#34;&gt;great minds should not think alike, they should think together&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;None of this is a complaint about diversity. The variety is a sign of a living, curious, genuinely creative field, and I would not trade it away. &lt;strong&gt;But diversity without shared reference points isn&#39;t richness. It is fragmentation.&lt;/strong&gt; We have plenty of islands. &lt;strong&gt;What we are short on are bridges.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-if-we-built-a-foundation&#34;&gt;What If We Built a Foundation?&lt;a class=&#34;anchor&#34; href=&#34;#what-if-we-built-a-foundation&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;On the afternoon of the second day, that diffuse frustration finally crystallized into a concrete proposal. &lt;strong&gt;What if this community had a foundation of its own&lt;/strong&gt;, something in the spirit of the Linux Foundation or the CNCF, to give it a shared backbone?&lt;/p&gt;&#xA;&lt;p&gt;The discussion that followed was lively, and it produced far more ideas and open questions than a single afternoon could ever resolve. Who would steward it? What would it own first? How do you create something neutral enough that every island trusts it, yet opinionated enough to be useful? No one walked away holding a charter or a set of bylaws. But everyone in the room did agree on one thing: &lt;strong&gt;this is worth pursuing.&lt;/strong&gt; That shared commitment, written out and signed by the room on a flip chart, became &amp;quot;The Munich Event.&amp;quot;&lt;/p&gt;&#xA;&lt;p&gt;The aims, as they took shape in the conversation, started exactly where the pain is sharpest: standardization. A shared terminology, shared file formats, shared examples, all the connective tissue that lets one island&#39;s work mean something on the next. And beyond that, a second goal: spreading the word, so that Event Sourcing and Event Modeling reach the wider audience they deserve, rather than staying the quiet specialty of a few.&lt;/p&gt;&#xA;&lt;p&gt;It is a modest artifact, that flip chart. A few sentences and a column of signatures on a sheet of paper. But the most consequential things often start out looking modest, and what mattered was never the paper. &lt;strong&gt;It was that a roomful of people who usually inhabit different islands had, for one afternoon, agreed to start building toward each other.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;your-domain-model-belongs-in-the-repository&#34;&gt;Your Domain Model Belongs in the Repository&lt;a class=&#34;anchor&#34; href=&#34;#your-domain-model-belongs-in-the-repository&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;And then it was my turn, with timing I couldn&#39;t have arranged if I had tried. My talk was titled &amp;quot;Your Domain Model Belongs in the Repository,&amp;quot; and its argument ran straight through the fault line the room had spent the afternoon circling.&lt;/p&gt;&#xA;&lt;p&gt;It rested on two claims. The first: &lt;strong&gt;we need far more standardization than we have&lt;/strong&gt;, from the words we use to the file formats we exchange, because meaningful exchange is impossible without it. The second is the uncomfortable one. Nearly every modeling effort, no matter which method it uses, ends the same way. The whiteboard fills up, someone takes a photo, the photo gets pasted into a wiki, and then no one ever looks at it again, let alone keeps it up to date. &lt;strong&gt;The model dies the moment the workshop ends.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;The word &amp;quot;repository&amp;quot; in the title is meant literally: a Git repository. The domain model belongs right next to the code, so that the two can be changed together, in a single coherent pull request, and reviewed as one unit. When the model is a versioned, plain-text artifact, it gains everything the code already has: diffs, history, review, blame, and a single source of truth that can&#39;t quietly drift out of date. &lt;strong&gt;A model that lives beside the code is a model that stays alive.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;This is where &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;ESDM&lt;/a&gt;&lt;/strong&gt; (Event-Sourced Domain Model) entered the talk, not as the point of it, but as one concrete attempt to build a bridge. ESDM is a YAML-based language for describing event-driven systems. It captures the building blocks of Domain-Driven Design, CQRS, and Event Sourcing (aggregates, events, commands) in a single shared format. Because it is plain text, it lives in your repository. Because it is structured, tools can read and write it. And because it is structured, so can an AI.&lt;/p&gt;&#xA;&lt;p&gt;That last point was the heart of the live demo. While I talked through the concept, I had an AI assistant working in the background. First it ran backward: it took an existing application, analyzed it, and reverse-engineered an ESDM model out of the code. Then it ran forward: from that single model, it generated two working implementations, one in Kotlin and one in Python. &lt;strong&gt;One model, two languages, derived live on stage&lt;/strong&gt;, the closest thing to a bridge you can show in five minutes. It is the same conviction that drives our work on &lt;strong&gt;&lt;a href=&#34;https://www.eventsourcing.ai&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Event Sourcing and AI&lt;/a&gt;&lt;/strong&gt;: structured, machine-readable history and models are exactly what these tools need to be useful.&lt;/p&gt;&#xA;&lt;p&gt;The reaction is the part I keep coming back to. People were glad it simply worked, which is never a given with a live demo. But what they responded to most was the direction of it: &lt;strong&gt;here was a tool aimed squarely at the gap we had been circling all day&lt;/strong&gt;. If you would like to see what a model that lives beside your code looks like, &lt;strong&gt;&lt;a href=&#34;https://www.esdm.io&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;ESDM is free and open for commercial use&lt;/a&gt;&lt;/strong&gt;, a good place to start.&lt;/p&gt;&#xA;&lt;figure&gt;&#xA;&lt;img src=&#34;/resources/blog/images/golo-roden-and-martin-dilger-at-the-event-modeling-conference-2026.jpg&#34; alt=&#34;Martin Dilger and Golo Roden at the Event Modeling Conference 2026&#34; loading=&#34;lazy&#34; /&gt;&#xA;&lt;figcaption&gt;Martin Dilger and Golo Roden at the Event Modeling Conference 2026&lt;/figcaption&gt;&#xA;&lt;/figure&gt;&#xA;&lt;h2 id=&#34;where-ive-felt-this-before&#34;&gt;Where I&#39;ve Felt This Before&lt;a class=&#34;anchor&#34; href=&#34;#where-ive-felt-this-before&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The thing about Brescia in 2011 is that the bet paid off. Node.js did not take over the world because the technology was flawless; it took over because a community held on to its enthusiasm long enough for adoption to catch up. The energy came first, and the ubiquity followed years later.&lt;/p&gt;&#xA;&lt;p&gt;Munich felt like that earlier moment: not the part where everyone already agrees you were right, but the part before it, when a small group can sense the shape of what is coming and quietly decides to build toward it. The bridges aren&#39;t built yet. The Munich Event is still a sheet of paper rather than a foundation, and ESDM is young. &lt;strong&gt;But for the first time, the will to build those bridges felt shared rather than scattered.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;That, more than any single talk, is what I carried home. And it comes with an invitation. If you care about giving this community shared standards, shared examples, and shared tools, if the idea of a foundation resonates with you, or if you simply want to tell us where ESDM gets it wrong, we would genuinely like to hear from you at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The islands aren&#39;t going anywhere. Let&#39;s start building the bridges.&lt;/strong&gt;&lt;/p&gt;&#xA;</content>
  </entry>
  <entry>
    <id>https://www.eventfoundation.io/resources/blog/dont-put-that-pdf-in-your-event</id>
    <title>Don&#39;t Put That PDF in Your Event</title>
    <link href="https://www.eventfoundation.io/resources/blog/dont-put-that-pdf-in-your-event"></link>
    <published>2026-06-25T00:00:00+02:00</published>
    <updated>2026-06-25T00:00:00+02:00</updated>
    <author>
      <name>Golo Roden</name>
    </author>
    <summary type="text">Someone asked us recently what to do when an event has to carry a document. A signed contract as a PDF, a Word file, a scanned invoice. The same question comes up every few weeks, and over the years it has arrived in every shape imaginable: not just documents, but images, audio files, and more than once a video. The phrasing changes, the underlying worry does not. Something big needs to be remembered, and the event feels like the place to keep it.</summary>
    <content type="html">&lt;p&gt;Someone asked us recently what to do when an event has to carry a document. A signed contract as a PDF, a Word file, a scanned invoice. The same question comes up every few weeks, and over the years it has arrived in every shape imaginable: not just documents, but images, audio files, and more than once a video. The phrasing changes, the underlying worry does not. Something big needs to be remembered, and the event feels like the place to keep it.&lt;/p&gt;&#xA;&lt;p&gt;The short answer fits in a single sentence: keep the big thing out of the event and put a reference to it inside. But the short answer is the least interesting part. The interesting part is &lt;em&gt;why&lt;/em&gt;, because the reasoning leads straight back to a question most of us never stop to ask. What is an event, really, and what is it for?&lt;/p&gt;&#xA;&lt;!-- more --&gt;&#xA;&lt;h2 id=&#34;the-question-behind-the-question&#34;&gt;The Question Behind the Question&lt;a class=&#34;anchor&#34; href=&#34;#the-question-behind-the-question&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Often the question arrives with a fact attached. In EventSourcingDB, a single event may not exceed &lt;strong&gt;64 KiB&lt;/strong&gt;, and people quite reasonably ask what they are supposed to do when their data is larger than that. There is a faint note of complaint in it, as if the limit were an obstacle to work around.&lt;/p&gt;&#xA;&lt;p&gt;We see it the other way. &lt;strong&gt;The limit is a feature, not a restriction.&lt;/strong&gt; Without it, the easy thing and the right thing would diverge, and the easy thing usually wins. You could drop a ten-megabyte PDF into an event, the write would succeed, and nothing would object until much later, when the bill comes due in a place far from where the decision was made. The limit moves that bill forward. It forces the conversation at the moment you are designing the event, which is exactly the moment you can still do something about it.&lt;/p&gt;&#xA;&lt;p&gt;So when someone hits the limit and stops to ask, that is the system working as intended. The question is not a sign that something is wrong. It is a sign that someone is thinking about how big an event can be, and, more importantly, how big it &lt;em&gt;should&lt;/em&gt; be.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-an-event-actually-is&#34;&gt;What an Event Actually Is&lt;a class=&#34;anchor&#34; href=&#34;#what-an-event-actually-is&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;To answer the question well, you have to be clear about what an event is in the first place. An event is the record that something of significance happened in your domain. It answers the obvious questions a story needs to answer: what happened, when, and to what. Depending on the context it may also carry the why and the who. &lt;code&gt;ContractSigned&lt;/code&gt;, &lt;code&gt;InvoiceIssued&lt;/code&gt;, &lt;code&gt;DocumentApproved&lt;/code&gt; – each one is a fact, stated in the language of the business, that will never change again.&lt;/p&gt;&#xA;&lt;p&gt;Notice what is &lt;em&gt;not&lt;/em&gt; in that description. An event is not a container for moving bytes around. It is not a filesystem, not a content delivery network, not a place to park a blob because no better home was handy. &lt;strong&gt;An event is a statement about the world, and a statement should be small enough to read.&lt;/strong&gt; When you reach for an event to hold a video file, you are using a sentence to do the work of a warehouse.&lt;/p&gt;&#xA;&lt;p&gt;This is the same foundation we laid out in &lt;strong&gt;&lt;a href=&#34;/resources/blog/thinking-in-events&#34;&gt;Thinking in Events&lt;/a&gt;&lt;/strong&gt;: events are the natural language of stories, immutable and ordered. A story told well uses words that name what happened. &lt;code&gt;ContractSigned&lt;/code&gt; is such a word. The forty pages of the contract itself are not part of the sentence – they are the thing the sentence refers to.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-every-kilobyte-travels&#34;&gt;Why Every Kilobyte Travels&lt;a class=&#34;anchor&#34; href=&#34;#why-every-kilobyte-travels&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;There is a concrete, physical reason this matters, and it has nothing to do with taste. Events move. They cross the network when they are written, when they are read, when they are projected into a read model, and when a downstream context subscribes to them. A single event being a few kilobytes larger than it needs to be is invisible. The trouble is that events rarely come one at a time.&lt;/p&gt;&#xA;&lt;p&gt;The defining operation of an event-sourced system is the replay: reading the history back, in order, to rebuild state. With a few hundred events, the difference between a lean event and a bloated one is academic. With a few hundred thousand, it is the difference between a replay that finishes over a coffee break and one you schedule for the weekend. We once worked out that &lt;strong&gt;&lt;a href=&#34;/resources/blog/18-months-of-events-fit-on-four-floppy-disks&#34;&gt;eighteen months of real events fit on four floppy disks&lt;/a&gt;&lt;/strong&gt; – but that is only true because those events carried domain facts and not attachments. Slip one video into each event and the same history would no longer fit on a stack of hard drives.&lt;/p&gt;&#xA;&lt;p&gt;If this argument feels familiar, it should. It is the same one the industry settled decades ago about storing files in a relational database. The usual advice was, and still is, to keep the file outside and store only a reference to it, for precisely this reason: when you load one row you accept the cost of that row, and when you load a million rows you do not want that cost multiplied by a blob you did not even need. Event-sourced systems load the equivalent of every row, every time they replay. The discipline matters more here, not less.&lt;/p&gt;&#xA;&lt;h2 id=&#34;an-old-answer-to-an-old-question&#34;&gt;An Old Answer to an Old Question&lt;a class=&#34;anchor&#34; href=&#34;#an-old-answer-to-an-old-question&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;The pattern that solves this has a name. In the world of messaging it is called the &lt;strong&gt;Claim Check&lt;/strong&gt;: instead of sending the heavy payload through the system, you store it once in a place built for heavy payloads and pass around a small claim ticket that can retrieve it. The name comes from a coat check. You do not carry your coat into the theatre; you carry the numbered stub and leave the coat where coats belong.&lt;/p&gt;&#xA;&lt;p&gt;In practice the place built for heavy payloads is object storage – S3 or one of its many relatives – or even just a directory on a filesystem. You write the document there once, you get back an identifier or a URL, and &lt;em&gt;that&lt;/em&gt; is what goes into the event. The event stays a crisp statement of fact, &lt;code&gt;DocumentUploaded&lt;/code&gt; with a reference and a little metadata, and the bytes live where bytes are cheap to keep and cheap to serve. This is not a workaround we invented to dodge a limit; it is the documented best practice, spelled out under &lt;strong&gt;&lt;a href=&#34;/concepts/common-mistakes&#34;&gt;writing oversized events&lt;/a&gt;&lt;/strong&gt;, and the limit is simply what makes the right choice the path of least resistance.&lt;/p&gt;&#xA;&lt;p&gt;What goes into the event alongside the reference is worth a moment&#39;s thought, too. The useful metadata – a filename, a content type, a size, perhaps a human-readable label – is small, domain-relevant, and exactly the kind of thing an event should carry. It is enough to reason about the document, to display it in a list, to decide whether to fetch it at all, without ever touching the bytes.&lt;/p&gt;&#xA;&lt;h2 id=&#34;a-reference-and-maybe-a-hash&#34;&gt;A Reference, and Maybe a Hash&lt;a class=&#34;anchor&#34; href=&#34;#a-reference-and-maybe-a-hash&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;A bare URL works, but you can do something more elegant for very little extra effort. Compute a cryptographic hash of the content – a SHA-256, say – and put that in the event next to the reference. Now the event does not merely point at a document; it pins down &lt;em&gt;which&lt;/em&gt; document, exactly, down to the last byte. When you later fetch the file, you can hash what you got and compare. If the two agree, you are holding precisely what was referenced when the event was written. If they do not, something changed underneath you, and you want to know.&lt;/p&gt;&#xA;&lt;p&gt;This buys you integrity almost for free, and it fits the spirit of the system perfectly. EventSourcingDB already hashes every event it stores, following the CloudEvents specification, which is what makes the history tamper-evident in the first place – the property we explored in &lt;strong&gt;&lt;a href=&#34;/resources/blog/proving-without-revealing-merkle-trees&#34;&gt;Proving Without Revealing&lt;/a&gt;&lt;/strong&gt; and the same instinct behind &lt;strong&gt;&lt;a href=&#34;/resources/blog/what-aviation-teaches-us-about-auditing&#34;&gt;what aviation teaches us about auditing&lt;/a&gt;&lt;/strong&gt;. Extending that discipline to the documents you keep outside the store is a natural continuation, not a new idea. The event guarantees the fact; the hash guarantees the artifact the fact refers to.&lt;/p&gt;&#xA;&lt;p&gt;You can go one step further and let the hash &lt;em&gt;be&lt;/em&gt; the reference, storing the content under its own fingerprint. This is content-addressable storage, and when it fits it is genuinely lovely: identical files collapse to a single copy automatically, and the reference can never point at the wrong thing, because the reference &lt;em&gt;is&lt;/em&gt; the thing&#39;s identity. It does not fit everywhere – it complicates deletion, and it assumes the content is meant to be immutable – but where those conditions hold, it is hard to beat. &lt;strong&gt;Reach for it when it earns its place, not by reflex.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;load-it-only-when-you-need-it&#34;&gt;Load It Only When You Need It&lt;a class=&#34;anchor&#34; href=&#34;#load-it-only-when-you-need-it&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;There is one more benefit to keeping the document outside, and it is the one teams appreciate most once they live with it. When the payload is not in the event, fetching the event no longer means fetching the payload. You get the fact and its metadata immediately, and the heavy content stays put until you actually ask for it.&lt;/p&gt;&#xA;&lt;p&gt;In real systems, you ask for it far less often than you would guess. Rendering a list of uploaded contracts needs the names, the dates, the statuses – not the contracts. Rebuilding a read model needs the facts, not the forty-page attachments behind them. Auditing who did what and when needs the events, not the bytes. The document itself is needed at one specific moment, when a user clicks to open it, and at that moment one extra network call is a perfectly fair price. &lt;strong&gt;The point is that you decide when to pay it, instead of paying it on every read whether you needed the document or not.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;That inversion of control is the quiet reward. Embedding the blob makes the expensive case the default and gives you no way out. Referencing it makes the cheap case the default and the expensive case a deliberate, on-demand choice. Most of the time the metadata is all anyone wanted anyway.&lt;/p&gt;&#xA;&lt;h2 id=&#34;and-when-it-has-to-disappear&#34;&gt;And When It Has to Disappear&lt;a class=&#34;anchor&#34; href=&#34;#and-when-it-has-to-disappear&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;There is a subtler payoff, and it is the one that turns a performance argument into a design principle. Event Sourcing makes a promise that sits awkwardly next to privacy law: events are immutable, and we never erase them. That is a feature when the events hold facts, and a genuine problem when they hold a customer&#39;s signed contract and that customer invokes their right to be forgotten. You cannot honor the request without rewriting history, which is precisely the thing the whole approach exists not to do.&lt;/p&gt;&#xA;&lt;p&gt;Keeping the payload outside dissolves the conflict. The sensitive bytes live in storage you control and can delete, while the event keeps only a reference and a hash. When the law, or simply good housekeeping, says the document must go, you delete the document. The event stays exactly as it was: &lt;code&gt;DocumentUploaded&lt;/code&gt; still happened, the fact is still true, the history is still intact. A reference that now points at nothing is not a bug – it is an honest statement that the thing existed and was later, deliberately, removed. &lt;strong&gt;You do not falsify the past to forget something; you stop storing the part that was never the fact in the first place.&lt;/strong&gt; That is the same instinct we followed in &lt;strong&gt;&lt;a href=&#34;/resources/blog/soft-delete-is-a-workaround&#34;&gt;Soft Delete Is a Workaround&lt;/a&gt;&lt;/strong&gt;: the history is sacred, but what hangs off it does not have to be.&lt;/p&gt;&#xA;&lt;h2 id=&#34;so-about-that-pdf&#34;&gt;So, About That PDF&lt;a class=&#34;anchor&#34; href=&#34;#so-about-that-pdf&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Back to the question we started with. What do you do when an event has to carry a document? You realize that it does not have to, and that it never should. The document goes into storage built for documents, a reference goes into the event, a hash goes alongside it if integrity matters, and the bytes are fetched only when someone genuinely needs them. The event goes back to doing its one job: stating, briefly and permanently, that something happened.&lt;/p&gt;&#xA;&lt;p&gt;The 64 KiB limit, the one that prompted the question, turns out to have been pointing at the answer the whole time. It is not in your way. It is the nudge that keeps your events small, portable, and meaningful, the gentle insistence that a fact and a file are two different things that deserve two different homes.&lt;/p&gt;&#xA;&lt;p&gt;This is the kind of question we love, because it is never really about the limit – it is about what an event is for. If you are weighing how to model large payloads in your own system, or wrestling with a case where the line is genuinely hard to draw, we would enjoy thinking it through with you. Write to us at &lt;strong&gt;&lt;a href=&#34;mailto:hello@thenativeweb.io&#34;&gt;hello@thenativeweb.io&lt;/a&gt;&lt;/strong&gt;, and tell us what your events are trying to carry.&lt;/p&gt;&#xA;</content>
  </entry>
</feed>