<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Commands, Events, and Processes on Brian Faust</title><link>https://vacua.dev/series/commands-events-and-processes/</link><description>Recent content in Commands, Events, and Processes 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/series/commands-events-and-processes/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></channel></rss>