<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Domain-Driven-Design on Brian Faust</title><link>https://vacua.dev/tags/domain-driven-design/</link><description>Recent content in Domain-Driven-Design on Brian Faust</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Mon, 01 Jan 0001 00:00:00 +0000</lastBuildDate><atom:link href="https://vacua.dev/tags/domain-driven-design/rss.xml" rel="self" type="application/rss+xml"/><item><title>Test Process State, Not Message Choreography</title><link>https://vacua.dev/test-process-state-not-message-choreography/</link><pubDate>Sun, 01 Oct 2023 00:00:00 +0000</pubDate><guid>https://vacua.dev/test-process-state-not-message-choreography/</guid><description>&lt;p&gt;A strict mock test expects five calls in order:&lt;/p&gt;
&lt;figure class="code-figure"&gt;
 &lt;div class="code-block" data-copy-code&gt;
 &lt;button class="copy-code" type="button" aria-label="Copy code"&gt;copy&lt;/button&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;load process
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;check message ID
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dispatch ReserveInventory
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;save process
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;record inbox&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Every expectation passes. Unfortunately, &lt;code&gt;save process&lt;/code&gt; writes the old
&lt;code&gt;awaiting_payment&lt;/code&gt; state. The command says inventory should be reserved while
the durable process still says it is waiting for payment.&lt;/p&gt;
&lt;p&gt;Now fix the persistence and move &lt;code&gt;record inbox&lt;/code&gt; before &lt;code&gt;save process&lt;/code&gt; inside
the same database transaction. The committed process, inbox, and outbox rows
are identical. The strict test fails because two calls changed places.&lt;/p&gt;</description></item><item><title>A Process Manager Remembers What the Queue Cannot</title><link>https://vacua.dev/process-managers-long-running-business-processes/</link><pubDate>Fri, 01 Sep 2023 00:00:00 +0000</pubDate><guid>https://vacua.dev/process-managers-long-running-business-processes/</guid><description>&lt;p&gt;An order is paid at 10:00. Inventory is reserved at 10:01. The warehouse does
not respond until 14:30. Shipping fails the next morning, after two worker
deployments and a payment-provider timeout.&lt;/p&gt;
&lt;p&gt;No request, transaction, or queue worker owns that whole timeline. Chaining
jobs can move execution from one step to the next, but the queue does not know
whether the business process is waiting, complete, compensating, or stuck.&lt;/p&gt;
&lt;p&gt;A Process Manager exists to remember that state and decide what should happen
when a relevant message arrives. Its value begins where call-stack control
ends.&lt;/p&gt;</description></item><item><title>An Outbox Moves the Failure Window</title><link>https://vacua.dev/an-outbox-moves-the-failure-window/</link><pubDate>Tue, 01 Aug 2023 00:00:00 +0000</pubDate><guid>https://vacua.dev/an-outbox-moves-the-failure-window/</guid><description>&lt;p&gt;Place an order, commit it, then publish &lt;code&gt;OrderPlaced&lt;/code&gt;. If the broker rejects
the send, the order remains placed and nothing durable says publication is
still required.&lt;/p&gt;
&lt;p&gt;Reverse the sequence. Publish &lt;code&gt;OrderPlaced&lt;/code&gt;, then commit the order. If the
database rolls back, the broker now carries a fact which the source database
never accepted.&lt;/p&gt;
&lt;p&gt;A transactional outbox changes the sequence again. Insert the order and a
pending message in one local transaction. Rollback removes both. Commit makes
both durable. A separate relay can keep trying the message after the request
which placed the order has ended.&lt;/p&gt;</description></item><item><title>Publishing Events Is Not Event Sourcing</title><link>https://vacua.dev/publishing-events-is-not-event-sourcing/</link><pubDate>Sat, 01 Jul 2023 00:00:00 +0000</pubDate><guid>https://vacua.dev/publishing-events-is-not-event-sourcing/</guid><description>&lt;p&gt;Two wallet applications accept the same credits and publish the same
&lt;code&gt;FundsCredited&lt;/code&gt; event. Both end with a balance of 3,500 cents. Both can update
a listener-owned read model.&lt;/p&gt;
&lt;p&gt;Now delete the wallet row and ask each application to recover.&lt;/p&gt;
&lt;p&gt;The first application cannot. Its &lt;code&gt;wallets.balance_minor&lt;/code&gt; column was the
authoritative value. The events were notifications sent after accepted
changes, and their payloads do not contain enough information to reconstruct
the balance.&lt;/p&gt;</description></item><item><title>A Domain Event Is a Fact, Not a Question</title><link>https://vacua.dev/a-domain-event-is-a-fact-not-a-question/</link><pubDate>Thu, 01 Jun 2023 00:00:00 +0000</pubDate><guid>https://vacua.dev/a-domain-event-is-a-fact-not-a-question/</guid><description>&lt;p&gt;An order cannot be placed until a credit policy accepts it. The handler needs
that answer before it can continue:&lt;/p&gt;
&lt;figure class="code-figure"&gt;
 &lt;div class="code-block" data-copy-code&gt;
 &lt;button class="copy-code" type="button" aria-label="Copy code"&gt;copy&lt;/button&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-php" data-lang="php"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;credit&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;allows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$amountCents&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;CreditRefused&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="na"&gt;place&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$amountCents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$placedOn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Turn that call into a &lt;code&gt;CheckCredit&lt;/code&gt; event and the question does not disappear.
It becomes harder to see. The handler still needs an answer, but an event
dispatcher normally gives it a list of listeners, not a typed decision. Now
the code needs a mutable payload, stopped propagation, or some other private
convention to recover the response.&lt;/p&gt;</description></item><item><title>CQRS Starts When Reads Need a Different Model</title><link>https://vacua.dev/command-query-separation-vs-cqrs/</link><pubDate>Mon, 01 May 2023 00:00:00 +0000</pubDate><guid>https://vacua.dev/command-query-separation-vs-cqrs/</guid><description>&lt;p&gt;An order-details page needs customer name, payment status, shipment progress,
line totals, tax labels, and the latest support case. The write model needs to
protect payment, cancellation, and fulfilment invariants.&lt;/p&gt;
&lt;p&gt;Trying to make one Eloquent graph serve both jobs creates a familiar mess:
relationships exist for screen composition, write methods expose persistence
concerns, and every request loads a different subset of a model that is never
quite complete.&lt;/p&gt;
&lt;p&gt;That tension can justify different read and write models. It does not
automatically justify two databases, an event store, a command bus, or eventual
consistency. CQRS is the model split; the infrastructure is a separate set of
decisions.&lt;/p&gt;</description></item><item><title>A Queued Command Changes the Contract</title><link>https://vacua.dev/a-queued-command-changes-the-contract/</link><pubDate>Sat, 01 Apr 2023 00:00:00 +0000</pubDate><guid>https://vacua.dev/a-queued-command-changes-the-contract/</guid><description>&lt;p&gt;Send the same payment-capture request through two Laravel routes.&lt;/p&gt;
&lt;p&gt;The synchronous route returns 204 after the provider has created charge
&lt;code&gt;ch_001&lt;/code&gt; and the order records that reference. When the provider returns 503,
the HTTP request returns 500 and the order remains unpaid.&lt;/p&gt;
&lt;p&gt;The queued route returns 202 while the order is still unpaid and the provider
ledger is empty. Its response contains an operation URL. Cancel the order
before a worker starts and that operation later becomes &lt;code&gt;refused&lt;/code&gt;; the
provider is never called.&lt;/p&gt;</description></item><item><title>A Command Bus Must Pay for Its Indirection</title><link>https://vacua.dev/a-command-bus-must-pay-for-its-indirection/</link><pubDate>Wed, 01 Mar 2023 00:00:00 +0000</pubDate><guid>https://vacua.dev/a-command-bus-must-pay-for-its-indirection/</guid><description>&lt;p&gt;Four Laravel routes handle two commands in the &lt;a href="https://vacua.dev/reproductions/cmd-02/"&gt;reproducible
fixture&lt;/a&gt;
. Two routes inject typed handlers. Two inject Laravel&amp;rsquo;s
dispatcher. All four return 204, and both call graphs leave the same SQLite
rows behind.&lt;/p&gt;
&lt;p&gt;Then one line disappears from the dispatcher&amp;rsquo;s handler map. The bus route
still resolves its controller, constructs &lt;code&gt;CancelOrder&lt;/code&gt;, and enters the
pipeline. It returns 500 without selecting a handler. The direct controller
has no equivalent registry entry to lose because its constructor already
names &lt;code&gt;DirectCancelOrderHandler&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>A Valid Command Can Still Be Refused</title><link>https://vacua.dev/a-valid-command-can-still-be-refused/</link><pubDate>Wed, 01 Feb 2023 00:00:00 +0000</pubDate><guid>https://vacua.dev/a-valid-command-can-still-be-refused/</guid><description>&lt;p&gt;Send this cancellation request to two orders:&lt;/p&gt;
&lt;figure class="code-figure"&gt;
 &lt;div class="code-block" data-copy-code&gt;
 &lt;button class="copy-code" type="button" aria-label="Copy code"&gt;copy&lt;/button&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;reason&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;customer_request&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;The placed order returns 204 and becomes cancelled. The shipped order returns
409 and remains shipped. Both requests pass the same Laravel Form Request.&lt;/p&gt;
&lt;p&gt;Change the reason to &lt;code&gt;unknown&lt;/code&gt; and the result is different again: Laravel
returns 422 before the controller constructs a command. Three requests crossed
two different decision boundaries. Calling one result “valid” and the others
“invalid” hides the part of the system which made each decision.&lt;/p&gt;</description></item><item><title>A Mapper Connects Two Honest Models</title><link>https://vacua.dev/a-mapper-connects-two-honest-models/</link><pubDate>Tue, 01 Nov 2022 00:00:00 +0000</pubDate><guid>https://vacua.dev/a-mapper-connects-two-honest-models/</guid><description>&lt;p&gt;An order in the domain has an &lt;code&gt;OrderNumber&lt;/code&gt;, a &lt;code&gt;Money&lt;/code&gt; total, typed lines, and
an operation named &lt;code&gt;place()&lt;/code&gt;. The same order in a relational database has an
&lt;code&gt;orders&lt;/code&gt; row, several &lt;code&gt;order_lines&lt;/code&gt; rows, integer minor units, a nullable
deadline, check constraints, a foreign key, and an index used by fulfilment.&lt;/p&gt;
&lt;p&gt;Forcing those shapes to match exactly damages one side or the other.&lt;/p&gt;
&lt;p&gt;Make the domain object mirror the tables and it acquires column names, nullable
intermediate state, relationship-loading concerns, and methods shaped around
persistence. Store the object as one opaque document and the database loses
straightforward constraints and access paths required by the selected query.&lt;/p&gt;</description></item><item><title>An Open Host Service Is Not a Published Language</title><link>https://vacua.dev/an-open-host-service-is-not-a-published-language/</link><pubDate>Fri, 01 Jul 2022 00:00:00 +0000</pubDate><guid>https://vacua.dev/an-open-host-service-is-not-a-published-language/</guid><description>&lt;p&gt;Inventory exposes one endpoint to Storefront, Fulfilment, and Replenishment:&lt;/p&gt;
&lt;figure class="code-figure"&gt;
 &lt;div class="code-block" data-copy-code&gt;
 &lt;button class="copy-code" type="button" aria-label="Copy code"&gt;copy&lt;/button&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GET /availability/{sku}?location={location}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;The route, authentication, media type, and JSON shape remain unchanged. One
release quietly redefines &lt;code&gt;available_quantity&lt;/code&gt;. Yesterday it meant physical
stock at the location. Today it means stock which may still be promised after
reservations and holds.&lt;/p&gt;
&lt;p&gt;Every request still returns &lt;code&gt;200&lt;/code&gt;. Schema validation still sees an integer.
Storefront, Fulfilment, and Replenishment now act on a different fact.&lt;/p&gt;</description></item><item><title>An API Cannot Tell You Who Has Influence</title><link>https://vacua.dev/an-api-cannot-tell-you-who-has-influence/</link><pubDate>Wed, 01 Jun 2022 00:00:00 +0000</pubDate><guid>https://vacua.dev/an-api-cannot-tell-you-who-has-influence/</guid><description>&lt;p&gt;Two Order Management features call the same Payments endpoint. They use the
same schema, service credential, and provider-managed release process.&lt;/p&gt;
&lt;p&gt;The first feature needs a &lt;code&gt;reversal_reason&lt;/code&gt;. Orders raises the request,
Payments accepts it into quarterly planning, and a Payments owner commits to a
compatibility window.&lt;/p&gt;
&lt;p&gt;The second feature asks for another Orders-specific field. Payments declines
it as outside the product roadmap. Orders decides that the generic refund
status is adequate for its support view and accepts the cost of adapting when
the provider contract changes.&lt;/p&gt;</description></item><item><title>An Anti-Corruption Layer Must Own the Translation</title><link>https://vacua.dev/an-anti-corruption-layer-must-own-the-translation/</link><pubDate>Sun, 01 May 2022 00:00:00 +0000</pubDate><guid>https://vacua.dev/an-anti-corruption-layer-must-own-the-translation/</guid><description>&lt;p&gt;Payments adds a refund state named &lt;code&gt;reversed&lt;/code&gt;. Order Management already has a
default branch for provider failures, so the new event keeps moving:&lt;/p&gt;
&lt;figure class="code-figure"&gt;
 &lt;div class="code-block" data-copy-code&gt;
 &lt;button class="copy-code" type="button" aria-label="Copy code"&gt;copy&lt;/button&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;anything except completed ──▶ provider-refund-failed&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Nothing crashes. That is the problem.&lt;/p&gt;
&lt;p&gt;The provider has reported a fact, but Orders has invented its consequence. A
reversed refund might reopen the order, create a debt, require manual recovery,
or leave the order cancelled while Finance resolves the money. The provider
enum cannot choose among those policies.&lt;/p&gt;</description></item><item><title>A Context Map Must Say What the Arrow Means</title><link>https://vacua.dev/a-context-map-must-say-what-the-arrow-means/</link><pubDate>Fri, 01 Apr 2022 00:00:00 +0000</pubDate><guid>https://vacua.dev/a-context-map-must-say-what-the-arrow-means/</guid><description>&lt;p&gt;The cancellation workshop ends with three boxes: Orders, Payments, and
Fulfilment. Someone connects them in event order. The diagram looks plausible.&lt;/p&gt;
&lt;p&gt;It is also impossible to read.&lt;/p&gt;
&lt;p&gt;Does the arrow mean an HTTP request, a domain event, a dependency, ownership,
or influence over another model? Does it describe the system that exists or
the one the team hopes to build? Who translates “refund declined” into an
order state? The line answers none of these questions.&lt;/p&gt;</description></item><item><title>Shared Kernel or Published Language: Choose Where Coordination Lives</title><link>https://vacua.dev/integration-patterns-shared-kernel-vs-published-language/</link><pubDate>Tue, 01 Mar 2022 00:00:00 +0000</pubDate><guid>https://vacua.dev/integration-patterns-shared-kernel-vs-published-language/</guid><description>&lt;p&gt;Billing and fulfilment both use the word “order.” Sharing an &lt;code&gt;Order&lt;/code&gt; class
looks like the obvious way to keep them consistent.&lt;/p&gt;
&lt;p&gt;Then billing adds credit notes, tax evidence, and settlement state. Fulfilment
adds picking waves, split shipments, and packing exceptions. The shared class
either accumulates both models or becomes a negotiation every time one team
changes it.&lt;/p&gt;
&lt;p&gt;Integration always creates coordination. A Shared Kernel and a Published
Language place that coordination in different locations. The first shares a
small piece of model and requires teams to change it together. The second
shares a versioned language at the boundary and lets each side translate it
into its own model.&lt;/p&gt;</description></item><item><title>EventStorming Finds Disagreement Before It Finds Services</title><link>https://vacua.dev/domain-modeling-workshops-eventstorming-in-practice/</link><pubDate>Tue, 01 Feb 2022 00:00:00 +0000</pubDate><guid>https://vacua.dev/domain-modeling-workshops-eventstorming-in-practice/</guid><description>&lt;p&gt;Ask sales, support, finance, and engineering when an order becomes cancelled
and the answers may sound compatible until they share a wall.&lt;/p&gt;
&lt;p&gt;Sales says cancellation happens when the customer asks. Support says it
happens after a case is approved. Finance says a paid order remains active
until the refund clears. Engineering points to a &lt;code&gt;cancelled_at&lt;/code&gt; column written
before the payment provider responds.&lt;/p&gt;
&lt;p&gt;That disagreement is not workshop noise. It is the model.&lt;/p&gt;</description></item></channel></rss>