{"id":2376,"date":"2026-09-05T19:19:17","date_gmt":"2026-09-05T18:19:17","guid":{"rendered":"https:\/\/denbeke.be\/blog\/?p=2376"},"modified":"2026-09-05T19:19:18","modified_gmt":"2026-09-05T18:19:18","slug":"dynamic-consistency-boundaries-dcb-for-event-sourcing","status":"publish","type":"post","link":"https:\/\/denbeke.be\/blog\/software\/dynamic-consistency-boundaries-dcb-for-event-sourcing\/","title":{"rendered":"Dynamic Consistency Boundaries (DCB) for Event Sourcing"},"content":{"rendered":"\n<p>What if we could get rid of the aggregate in event sourcing? The aggregate gives strong consistency, and what happens the day a business rule has to reach across two of them. This post is about that, and about a pattern I&#8217;ve come to rely on called&nbsp;<strong>Dynamic Consistency Boundaries (DCB)<\/strong>. The short version: instead of carving consistency into fixed structures at analysis time, you express it as a per-command predicate over your event log.<\/p>\n\n\n\n<p>I&#8217;ll keep this at the level I&#8217;d want if I were picking this up myself: no safety net around event sourcing and aggregates, straight into the concurrency model, the storage side, and the trade-offs. There&#8217;s Go, because that&#8217;s what the store is written in. If you&#8217;d rather have the pitch first, this&nbsp;<a href=\"https:\/\/tandemstudio.be\/blog\/wat-is-dynamic-consistency-boundary-event-sourcing\">overview post about DCB<\/a>&nbsp;(Dutch) walks the same idea without the code.<\/p>\n\n\n\n<h2 id=\"the-aggregate-as-a-static-consistency-boundary\">The aggregate as a static consistency boundary<\/h2>\n\n\n\n<p>In classic event sourcing you pick a bounded fragment of your domain (one customer, one order, one product) and give it its own event stream. The rule is simple:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p>The aggregate is the unit of strong consistency.<\/p><\/blockquote>\n\n\n\n<p>Enforcing it is straightforward. Every aggregate stream carries a monotonically increasing version. The command handler reads the stream up to version&nbsp;<code>N<\/code>, makes its decision against that snapshot, and appends new events&nbsp;<strong>only if<\/strong>&nbsp;the stream is still at version&nbsp;<code>N<\/code>. Plain optimistic concurrency:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>append(stream, expectedVersion = N, events)\n  succeeds only if head(stream).version == N\n  otherwise Conflict, then reload and retry\n<\/code><\/pre>\n\n\n\n<p>That&#8217;s a compare-and-swap on a single stream head. Cheap, embarrassingly parallel (streams are independent), and it guarantees serializability of everything that touches one stream. The catch: two things that must always change together&nbsp;<em>must<\/em>&nbsp;live in the same aggregate. There&#8217;s no way around it.<\/p>\n\n\n\n<p>The problem is that this boundary gets locked in during analysis, before any code runs. Whether a rule can be enforced atomically is decided by how you carved up your aggregates, not by the shape of the rule itself.<\/p>\n\n\n\n<h2 id=\"when-the-rule-crosses-the-boundary\">When the rule crosses the boundary<\/h2>\n\n\n\n<p>Take an order placement that has to check two things on its own:<\/p>\n\n\n\n<ul><li>this product still has stock (the&nbsp;<code>product:123<\/code>&nbsp;aggregate)<\/li><li>this customer is still within their credit limit (the&nbsp;<code>customer:456<\/code>&nbsp;aggregate)<\/li><\/ul>\n\n\n\n<p>Those live in separate aggregates. There&#8217;s no single stream head to compare-and-swap against, so you can&#8217;t express &#8220;append order events iff both of these still hold&#8221; in one atomic write. A decision that&#8217;s conceptually one transition has been split into two.<\/p>\n\n\n\n<p>The usual workarounds are all compromises:<\/p>\n\n\n\n<p><strong>The God aggregate.<\/strong>&nbsp;Fold everything into one giant stream. Now the rule is checkable, but you&#8217;ve serialized the whole domain on one lock. Two unrelated customers now contend for the same stream head, parallelism evaporates, and the aggregate turns into an object nobody wants to maintain.<\/p>\n\n\n\n<p><strong>Sagas \/ two-phase commit.<\/strong>&nbsp;Coordinate the two boundaries with a saga. You add coordinating events, a temporary inconsistent intermediate state that other readers can observe, and a compensation path for when the second leg fails. It works, but the machinery is real: an invariant that&#8217;s naturally atomic has become a distributed transaction by construction.<\/p>\n\n\n\n<p><strong>Re-carving the boundaries.<\/strong>&nbsp;If you guessed your granularity wrong, fixing it means re-streaming your event history into new streams. That&#8217;s a migration you don&#8217;t want to run against a live system.<\/p>\n\n\n\n<h2 id=\"dcb-consistency-as-a-predicate-not-a-partition\">DCB: consistency as a predicate, not a partition<\/h2>\n\n\n\n<p>DCB keeps the optimistic-append mechanism from classic event sourcing but changes what you compare-and-swap against. The unit of concurrency stops being &#8220;a stream&#8221; and becomes&nbsp;<strong>a query over event tags<\/strong>.<\/p>\n\n\n\n<p>Concretely:<\/p>\n\n\n\n<ol><li><strong>Every event is tagged<\/strong>&nbsp;when it&#8217;s written. A&nbsp;<code>UserSignedUp<\/code>&nbsp;event carries a tag like&nbsp;<code>username:alice<\/code>. Tags are the raw material of your boundaries.<\/li><li><strong>A consistency boundary is a predicate over tags<\/strong>&nbsp;(and event types). For a uniqueness rule, the predicate is &#8220;events tagged&nbsp;<code>username:alice<\/code>.&#8221;<\/li><li><strong>A command is a read-then-conditional-append.<\/strong>&nbsp;You read the events in scope up to position&nbsp;<code>P<\/code>, make your decision, and append your new events with the condition&nbsp;<em>&#8220;fail unless nothing matching my predicate has been appended since position&nbsp;<code>P<\/code>.&#8221;<\/em><\/li><\/ol>\n\n\n\n<p>That last sentence is the whole trick, which is why the interface matters more than the plumbing.<\/p>\n\n\n\n<h2 id=\"the-interface\">The interface<\/h2>\n\n\n\n<p>Here&#8217;s the store interface the idea needs. It&#8217;s smaller than you&#8217;d expect, because the hard part lives entirely in&nbsp;<code>AppendCondition<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>package eventstore\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"iter\"\n)\n\n\/\/ AppendCondition enforces optimistic concurrency: the append fails with\n\/\/ ErrAppendConditionViolated if the store contains any event matching Query with\n\/\/ a sequence number strictly greater than After, i.e. someone changed relevant\n\/\/ state since you last read it. After 0 checks against all events.\n\/\/\n\/\/ Query uses the usual semantics, so a nil\/empty Query matches EVERY event: such\n\/\/ a condition fails if any event at all was appended after After (a global\n\/\/ \"nothing changed since\" guard, rarely what you want for a scoped boundary).\ntype AppendCondition struct {\n\tQuery Query\n\tAfter uint64\n}\n\n\/\/ ErrAppendConditionViolated is returned by Append when the AppendCondition is\n\/\/ violated, meaning relevant state changed since the caller last read.\nvar ErrAppendConditionViolated = errors.New(\"append condition violated\")\n\n\/\/ Store reads and appends events.\ntype Store interface {\n\t\/\/ Read returns an iterator over events matching the query with a sequence\n\t\/\/ number strictly greater than after. Pass 0 to read from the beginning.\n\tRead(ctx context.Context, query Query, after uint64) iter.Seq2&#91;SequencedEvent, error]\n\n\t\/\/ Append atomically persists events. If condition is non-nil, the append\n\t\/\/ fails with ErrAppendConditionViolated when the store contains any event\n\t\/\/ matching condition.Query with a sequence number greater than condition.After.\n\tAppend(ctx context.Context, events &#91;]Event, condition *AppendCondition) error\n}<\/code><\/pre>\n\n\n\n<p>Three decisions are doing the work here, and they&#8217;re worth calling out:<\/p>\n\n\n\n<p><strong>1. The boundary is a&nbsp;<code>Query<\/code>, not a stream name.<\/strong>&nbsp;In classic event sourcing this type would be a stream ID plus an expected version; the boundary is baked into the data model. Here the boundary is an argument the caller constructs. The store has no notion of a &#8220;stream&#8221; at all, only tags and sequence numbers. That&#8217;s the whole point: the store can&#8217;t lock you into a fixed granularity because it never knew one.<\/p>\n\n\n\n<p><strong>2.&nbsp;<code>After<\/code>&nbsp;is a monotonic sequence, not a version.<\/strong>&nbsp;A per-stream version only tells you about that stream. A sequence number lets one condition cover events across many tags: you read &#8220;product:123 OR customer:456&#8221; and record a single high-water mark&nbsp;<code>After<\/code>. Classic optimistic concurrency would force you to check two version numbers separately, which is two races to worry about. Here the store compares&nbsp;<code>After<\/code>&nbsp;against any matching event; that&#8217;s the &#8220;did anything I depend on move?&#8221; check you actually want.<\/p>\n\n\n\n<p><strong>3.&nbsp;<code>Query<\/code>&nbsp;needs a scoped boundary.<\/strong>&nbsp;This isn&#8217;t hygiene, it&#8217;s a hard storage constraint. A nil or type-only query matches everything, which means there&#8217;s no boundary to lock on. Our store refuses those conditions, because enforcing them would serialize the whole type. Every condition has to carry at least one event-type-plus-tag combination you can lock on. I&#8217;ll come back to why in the storage section.<\/p>\n\n\n\n<h2 id=\"making-decisions-with-projections\">Making decisions with projections<\/h2>\n\n\n\n<p>The practical bit is that building a condition is usually driven by a&nbsp;<em>projection<\/em>, not by fiddling with raw events. We read the facts we need into a projection state, and&nbsp;<code>BuildDecisionModel<\/code>&nbsp;both fills that state and records the&nbsp;<code>After<\/code>&nbsp;position for us. The decision is then a plain check against the projection, and the append is guarded by the same boundary we read from.<\/p>\n\n\n\n<p>Here&#8217;s user sign-up, where the invariant is &#8220;the username is unique&#8221;:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ signUp registers a new user, failing if the username is already taken.\nfunc signUp(ctx context.Context, store eventstore.Store, username string) error {\n\texists := usernameExistsProjection(username)\n\tcondition, err := eventstore.BuildDecisionModel(ctx, store, &amp;exists)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not build decision model: %w\", err)\n\t}\n\tif exists.State {\n\t\treturn fmt.Errorf(\"username %q is already taken\", username)\n\t}\n\tevent, err := eventstore.NewEvent(\"UserSignedUp\", username, eventstore.NewTags(\"username\", username))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not create event: %w\", err)\n\t}\n\treturn store.Append(ctx, &#91;]eventstore.Event{event}, condition)\n}<\/code><\/pre>\n\n\n\n<p>Follow the concurrency through, because the pieces line up neatly:<\/p>\n\n\n\n<ol><li><code>usernameExistsProjection(username)<\/code>&nbsp;declares the query, scoped to the tag&nbsp;<code>username:&lt;this name&gt;<\/code>.<\/li><li><code>BuildDecisionModel<\/code>&nbsp;reads the matching events, folds them into the projection (<code>exists.State<\/code>), and captures the&nbsp;<code>After<\/code>&nbsp;position the append will check later.<\/li><li>If&nbsp;<code>exists.State<\/code>&nbsp;is true, someone already signed up with this name and we reject. No conditional append needed.<\/li><li>Otherwise we create the&nbsp;<code>UserSignedUp<\/code>&nbsp;event, tagged with the same&nbsp;<code>username<\/code>&nbsp;it guards, and append with the condition built in step 2.<\/li><\/ol>\n\n\n\n<p>The subtle part is that the event is tagged with exactly the same tag the uniqueness boundary locks on. Two concurrent&nbsp;<code>signUp(\"alice\")<\/code>&nbsp;calls both read &#8220;no&nbsp;<code>username:alice<\/code>&nbsp;event present,&#8221; both build a condition with the same&nbsp;<code>After<\/code>, and only one wins the append. The loser gets&nbsp;<code>ErrAppendConditionViolated<\/code>&nbsp;and retries against a fresher read, where&nbsp;<code>exists.State<\/code>&nbsp;is now true. So &#8220;exactly one&nbsp;<code>alice<\/code>&#8221; falls out of the same guard that checks stock and credit. No reservation table, no separate uniqueness index, and atomicity spans however many tags the rule needs in a single guarded append.<\/p>\n\n\n\n<p>The intro&#8217;s order example is the same pattern. Instead of a fixed aggregate that forces stock and credit into one stream, you model two projections (stock for&nbsp;<code>product:123<\/code>, outstanding credit for&nbsp;<code>customer:456<\/code>), build a decision model whose boundary spans both tags, decide against both states, and append the order event tagged with both&nbsp;<code>product:123<\/code>&nbsp;and&nbsp;<code>customer:456<\/code>. Two invariants over two independent things, atomically enforced in one conditional append.<\/p>\n\n\n\n<h2 id=\"the-storage-contract\">The storage contract<\/h2>\n\n\n\n<p>Everything above leans on a single guarantee, and the rest is implementation detail:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\"><p><strong>Atomic read-and-conditional-append.<\/strong>&nbsp;A command reads the events it needs, notes the position&nbsp;<code>After<\/code>, and appends its new events such that the guard &#8220;no event matching&nbsp;<code>condition.Query<\/code>&nbsp;has a sequence greater than&nbsp;<code>condition.After<\/code>&#8221; is evaluated atomically at the moment of write, with the event durably stored before the call returns. If relevant state changed since your read,&nbsp;<code>Append<\/code>&nbsp;returns&nbsp;<code>ErrAppendConditionViolated<\/code>&nbsp;and you retry.<\/p><\/blockquote>\n\n\n\n<p>If that guard is ever evaluated against something other than the point of write, DCB silently turns into an expensive per-event race. Keep that one sentence in mind and every storage design becomes an answer to &#8220;how do I make this atomic under concurrency without going slow?&#8221;<\/p>\n\n\n\n<p>We implement it on Postgres. The wrinkle there is sequence-number assignment: give each boundary its own lock and positions commit out of order, which breaks a plain monotonic cursor. My&nbsp;<a href=\"https:\/\/denbeke.be\/blog\/software\/can-dcb-event-sourcing-be-fast-and-flexible-a-postgres-benchmark\/\">Postgres benchmark post<\/a>&nbsp;goes through the locking strategies (global, per-boundary, hybrid, and the staging + flusher design we use) and shows that spread, human-paced workloads get single-digit-thousands of appends per second on managed Postgres. If you want the &#8220;how,&#8221; that&#8217;s the place to read it. Here it&#8217;s enough to know the guarantee everything depends on.<\/p>\n\n\n\n<p>One consequence of that guarantee does shape your code, so I&#8217;ll pull it into this article:&nbsp;<strong>an append condition must be scoped to a real boundary.<\/strong>&nbsp;It needs at least one event-type-plus-tag combination (like&nbsp;<code>UserCreated<\/code>&nbsp;tagged&nbsp;<code>user:123<\/code>) to serve as the lock key. A bare type-level condition such as &#8220;no&nbsp;<code>UserCreated<\/code>&nbsp;exists anywhere&#8221; has no boundary to lock on, so we disallow it; it would force a global lock over the whole type and kill the per-boundary concurrency every fast strategy relies on. When you build decision models, scope them to real tags.<\/p>\n\n\n\n<h2 id=\"what-becomes-trivial\">What becomes trivial<\/h2>\n\n\n\n<p>Once uniqueness is just &#8220;append failed because something touched my tag,&#8221; a few things stop being problems:<\/p>\n\n\n\n<ul><li><strong>Uniqueness constraints<\/strong>&nbsp;(at most one reservation for slot T, a username that can&#8217;t be registered twice) stop being separate reservation tables or read-model gymnastics. They&#8217;re a predicate over a tag (<code>slot:T<\/code>,&nbsp;<code>username:alice<\/code>) that the append already checks. The&nbsp;<code>signUp<\/code>&nbsp;example above is the whole pattern.<\/li><li><strong>Exactly-once \/ double-submit.<\/strong>&nbsp;Tag with the idempotency key; a replayed submit is just a conflicting append.<\/li><li><strong>New invariants without re-streaming.<\/strong>&nbsp;A new rule over existing tags is a new query, not a new structure. Nothing to migrate. You only re-stream if you discover you failed to tag something you later need, which is why tag selection is design, not an afterthought. We measured this and it held up; see the&nbsp;<a href=\"https:\/\/tandemstudio.be\/blog\/dcb-event-sourcing-benchmark\">DCB benchmark<\/a>&nbsp;(Dutch).<\/li><\/ul>\n\n\n\n<h2 id=\"the-trade-offs\">The trade-offs<\/h2>\n\n\n\n<p>DCB isn&#8217;t free, and if you reach for it you should be able to say why it beats a saga for your particular invariant:<\/p>\n\n\n\n<ul><li><strong>Read amplification and hot tags.<\/strong>&nbsp;The guard is a query over your tags. A&nbsp;<code>customer:456<\/code>&nbsp;that participates in half your writes becomes a contention hot spot, exactly like a single stream would. The skill is tagging just precisely enough: the boundary should be the minimal set of tags the rule truly needs. Tag too broadly (&#8220;this busy customer&#8217;s everything must always move together&#8221;) and writers pile onto the same tag and retry in lockstep, and your parallel throughput collapses to aggregate-class serialization.<\/li><li><strong>Boundary-scoped conditions are mandatory.<\/strong>&nbsp;An empty or type-only query has no lock key, so it&#8217;s disallowed. That&#8217;s a real constraint on how you write decision models, not a style preference, and it follows directly from the storage contract above.<\/li><li><strong>The read and the append must share the atomicity boundary.<\/strong>&nbsp;If the position captured by&nbsp;<code>BuildDecisionModel<\/code>&nbsp;and the guard inside&nbsp;<code>Append<\/code>&nbsp;aren&#8217;t evaluated against the same moment of write, DCB silently degenerates into an expensive per-event race. Most of the painful production stories I&#8217;ve seen come from splitting this, not from the concept.<\/li><li><strong>Retry is part of the contract.<\/strong>&nbsp;Because the guard can fail under real concurrency, handlers have to be written as read, decide, append, and on conflict re-read with idempotent decision logic. That&#8217;s a discipline, not a bug.<\/li><li><strong>It doesn&#8217;t fix distributed systems.<\/strong>&nbsp;DCB gives you atomicity across tagged scopes on one log. It doesn&#8217;t hand you multi-node linearizable transactions for free; if your boundaries span physical partitions you&#8217;ve shipped a distributed transaction and all its problems, whatever you call the boundary. Within a single node, where most event-sourced systems actually live, this is a non-issue.<\/li><\/ul>\n\n\n\n<p>The log already is the tagged, append-only substrate a predicate needs, and the read model is just another&nbsp;<a href=\"https:\/\/tandemstudio.be\/blog\/wat-is-een-projectie-event-sourcing\">projection<\/a>&nbsp;(Dutch) over it. You don&#8217;t add a consistency engine to an event-sourced system; you add tags at write time and a scoped query plus an&nbsp;<code>After<\/code>&nbsp;at command time.<\/p>\n\n\n\n<h2 id=\"wrapping-up\">Wrapping up<\/h2>\n\n\n\n<p>Event sourcing gives you a memory that grows with your domain. DCB makes the rules spanning several parts of that memory reliably enforceable, with an interface small enough to fit on a slide:&nbsp;<code>Read(query, after)<\/code>&nbsp;and&nbsp;<code>Append(events, &amp;AppendCondition{Query, After})<\/code>. The boundary is no longer a structure you committed to during analysis; it&#8217;s a predicate you choose per command, built from a projection and guarded by a scoped condition. For a business rule that&#8217;s genuinely dynamic, that&#8217;s where the decision belongs.<\/p>\n\n\n\n<p>I keep coming back to a simple test for a pattern: does it remove machinery? If you find yourself building a saga to stitch together two aggregates that a single invariant actually binds, it&#8217;s worth asking whether the boundary should have been a scoped query all along. The&nbsp;<a href=\"https:\/\/dcb.events\/specification\/\">spec<\/a>&nbsp;has the full details if you want them.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>What if we could get rid of the aggregate in event sourcing? The aggregate gives strong consistency, and what happens the day a business rule has to reach across two of them. This post is about that, and about a pattern I&#8217;ve come to rely on called&nbsp;Dynamic Consistency Boundaries (DCB). The short version: instead of [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[161],"tags":[291,231,232],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v15.6.2 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Dynamic Consistency Boundaries (DCB) for Event Sourcing &ndash; DenBeke<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/denbeke.be\/blog\/software\/dynamic-consistency-boundaries-dcb-for-event-sourcing\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Dynamic Consistency Boundaries (DCB) for Event Sourcing &ndash; DenBeke\" \/>\n<meta property=\"og:description\" content=\"What if we could get rid of the aggregate in event sourcing? The aggregate gives strong consistency, and what happens the day a business rule has to reach across two of them. This post is about that, and about a pattern I&#8217;ve come to rely on called&nbsp;Dynamic Consistency Boundaries (DCB). The short version: instead of [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/denbeke.be\/blog\/software\/dynamic-consistency-boundaries-dcb-for-event-sourcing\/\" \/>\n<meta property=\"og:site_name\" content=\"DenBeke\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-05T18:19:17+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-05T18:19:18+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary\" \/>\n<meta name=\"twitter:creator\" content=\"@MthsBk\" \/>\n<meta name=\"twitter:site\" content=\"@MthsBk\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\">\n\t<meta name=\"twitter:data1\" content=\"11 minutes\">\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebSite\",\"@id\":\"https:\/\/denbeke.be\/blog\/#website\",\"url\":\"https:\/\/denbeke.be\/blog\/\",\"name\":\"DenBeke\",\"description\":\"Mathias Beke\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":\"https:\/\/denbeke.be\/blog\/?s={search_term_string}\",\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/denbeke.be\/blog\/software\/dynamic-consistency-boundaries-dcb-for-event-sourcing\/#webpage\",\"url\":\"https:\/\/denbeke.be\/blog\/software\/dynamic-consistency-boundaries-dcb-for-event-sourcing\/\",\"name\":\"Dynamic Consistency Boundaries (DCB) for Event Sourcing &ndash; DenBeke\",\"isPartOf\":{\"@id\":\"https:\/\/denbeke.be\/blog\/#website\"},\"datePublished\":\"2026-09-05T18:19:17+00:00\",\"dateModified\":\"2026-09-05T18:19:18+00:00\",\"author\":{\"@id\":\"https:\/\/denbeke.be\/blog\/#\/schema\/person\/386878f712fe3fe22227216f087772dc\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/denbeke.be\/blog\/software\/dynamic-consistency-boundaries-dcb-for-event-sourcing\/\"]}]},{\"@type\":\"Person\",\"@id\":\"https:\/\/denbeke.be\/blog\/#\/schema\/person\/386878f712fe3fe22227216f087772dc\",\"name\":\"Mathias Beke\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\/\/denbeke.be\/blog\/#personlogo\",\"inLanguage\":\"en-US\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/015ba35e6ce4f5859e3888ca99807575?s=96&d=mm&r=g\",\"caption\":\"Mathias Beke\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","_links":{"self":[{"href":"https:\/\/denbeke.be\/blog\/wp-json\/wp\/v2\/posts\/2376"}],"collection":[{"href":"https:\/\/denbeke.be\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/denbeke.be\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/denbeke.be\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/denbeke.be\/blog\/wp-json\/wp\/v2\/comments?post=2376"}],"version-history":[{"count":1,"href":"https:\/\/denbeke.be\/blog\/wp-json\/wp\/v2\/posts\/2376\/revisions"}],"predecessor-version":[{"id":2377,"href":"https:\/\/denbeke.be\/blog\/wp-json\/wp\/v2\/posts\/2376\/revisions\/2377"}],"wp:attachment":[{"href":"https:\/\/denbeke.be\/blog\/wp-json\/wp\/v2\/media?parent=2376"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/denbeke.be\/blog\/wp-json\/wp\/v2\/categories?post=2376"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/denbeke.be\/blog\/wp-json\/wp\/v2\/tags?post=2376"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}