<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Integration Without Illusions on Brian Faust</title><link>https://vacua.dev/series/integration-without-illusions/</link><description>Recent content in Integration Without Illusions 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/integration-without-illusions/rss.xml" rel="self" type="application/rss+xml"/><item><title>A Capability Interface Is Smaller Than a Vendor Client</title><link>https://vacua.dev/thoughts/a-capability-interface-is-smaller-than-a-vendor-client/</link><pubDate>Sat, 01 Dec 2018 00:00:00 +0000</pubDate><guid>https://vacua.dev/thoughts/a-capability-interface-is-smaller-than-a-vendor-client/</guid><description>&lt;p&gt;A controller needs to register an order webhook. It constructs a vendor SDK
connection, accesses a dynamic resource property, calls its creation method,
names the remote event, chooses response fields, and catches the SDK&amp;rsquo;s base
exception.&lt;/p&gt;
&lt;p&gt;Its test needs to intercept construction of that concrete connection and mock a
dynamic property chain. The two useful behavior tests are skipped with the same
explanation: the code needs an interface and dependency injection.&lt;/p&gt;</description></item><item><title>Renaming a Field Is a Protocol Migration</title><link>https://vacua.dev/thoughts/renaming-a-field-is-a-protocol-migration/</link><pubDate>Thu, 01 Nov 2018 00:00:00 +0000</pubDate><guid>https://vacua.dev/thoughts/renaming-a-field-is-a-protocol-migration/</guid><description>&lt;p&gt;An API changes &lt;code&gt;serviceIds&lt;/code&gt; to &lt;code&gt;deliveryOptionIds&lt;/code&gt;. The new name describes the
domain better. The implementation becomes easier to read.&lt;/p&gt;
&lt;p&gt;An existing client still sends:&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;serviceIds&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;express&amp;#34;&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;The request now fails validation before any application code runs.&lt;/p&gt;
&lt;p&gt;Nothing about the client&amp;rsquo;s intent changed. The server chose a cleaner name and
turned that preference into a breaking protocol change.&lt;/p&gt;
&lt;p&gt;Once another process sends a field, renaming it is no longer a refactor. It is
a migration involving deployed callers whose release schedule the server may
not control.&lt;/p&gt;</description></item><item><title>A Cache Must Distinguish Empty from Unavailable</title><link>https://vacua.dev/thoughts/a-cache-must-distinguish-empty-from-unavailable/</link><pubDate>Mon, 01 Oct 2018 00:00:00 +0000</pubDate><guid>https://vacua.dev/thoughts/a-cache-must-distinguish-empty-from-unavailable/</guid><description>&lt;p&gt;A cached search returns two pickup locations. One location is deleted locally,
but the search keeps returning both.&lt;/p&gt;
&lt;p&gt;The tempting repair is a shorter time to live. That trades a known correctness
bug for a shorter known correctness bug. The system already knows the cached
answer is false; waiting five minutes instead of an hour does not make it less
false.&lt;/p&gt;
&lt;p&gt;The opposite failure looks similar. A remote lookup returns nothing during an
outage. Replacing the cache with an empty list would remove locations which may
still exist.&lt;/p&gt;</description></item><item><title>Partial Work Needs a Child Ledger</title><link>https://vacua.dev/thoughts/partial-work-needs-a-child-ledger/</link><pubDate>Sat, 01 Sep 2018 00:00:00 +0000</pubDate><guid>https://vacua.dev/thoughts/partial-work-needs-a-child-ledger/</guid><description>&lt;p&gt;A queue job receives 1,000 events in one remote response. It dispatches 600
child jobs, then times out.&lt;/p&gt;
&lt;p&gt;The parent row says processing failed. Some children have already finished.
Others are waiting in the queue. The remaining 400 were never dispatched. A
counter can report 573 completed events, but it cannot say which 573 they were.&lt;/p&gt;
&lt;p&gt;Running the parent again may duplicate the first 600. Leaving it alone loses
the last 400. Neither choice is defensible because the system retained a total
and discarded the membership behind it.&lt;/p&gt;</description></item><item><title>Retry Budgets Multiply Across Layers</title><link>https://vacua.dev/thoughts/retry-budgets-multiply-across-layers/</link><pubDate>Wed, 01 Aug 2018 00:00:00 +0000</pubDate><guid>https://vacua.dev/thoughts/retry-budgets-multiply-across-layers/</guid><description>&lt;p&gt;An HTTP connector allows three attempts. The queue job calling it also allows
three attempts.&lt;/p&gt;
&lt;p&gt;When the remote service keeps failing, the job does not make three calls. It
makes nine:&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;job attempt 1 -&amp;gt; connector calls 1, 2, 3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;job attempt 2 -&amp;gt; connector calls 4, 5, 6
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;job attempt 3 -&amp;gt; connector calls 7, 8, 9&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Both settings look modest in isolation. The remote system receives their
product.&lt;/p&gt;</description></item><item><title>A Rate Limit Belongs to All the Workers Sharing It</title><link>https://vacua.dev/thoughts/a-rate-limit-belongs-to-all-the-workers-sharing-it/</link><pubDate>Sun, 01 Jul 2018 00:00:00 +0000</pubDate><guid>https://vacua.dev/thoughts/a-rate-limit-belongs-to-all-the-workers-sharing-it/</guid><description>&lt;p&gt;One polling job receives a 429 response saying to try again in fifteen seconds.
It releases itself for fifteen seconds.&lt;/p&gt;
&lt;p&gt;During that pause, twenty other workers poll the same provider. They have not
seen the response, so each sends another request into the closed window.&lt;/p&gt;
&lt;p&gt;The first job obeyed the limit. The application did not.&lt;/p&gt;
&lt;h2 id="find-the-scope-before-choosing-the-delay"&gt;Find the scope before choosing the delay&lt;/h2&gt;
&lt;p&gt;A rate limit applies to some shared identity: an API credential, account,
tenant, endpoint family, source address, or a combination of them. Every worker
using that identity spends the same remote capacity.&lt;/p&gt;</description></item><item><title>An Idempotency Key Is Not an Identity</title><link>https://vacua.dev/thoughts/an-idempotency-key-is-not-an-identity/</link><pubDate>Fri, 01 Jun 2018 00:00:00 +0000</pubDate><guid>https://vacua.dev/thoughts/an-idempotency-key-is-not-an-identity/</guid><description>&lt;p&gt;A booking test sends the same request twice with the same idempotency key. The
remote booking service is called once. Both callers receive the same successful
response.&lt;/p&gt;
&lt;p&gt;Then one field changes while the key stays the same. The remote service is
still called once, but the second request receives a conflict instead of the
first result.&lt;/p&gt;
&lt;p&gt;The key did not establish sameness. It only gave the application somewhere to
look.&lt;/p&gt;</description></item><item><title>A Webhook Delivery Is Not a Unique Event</title><link>https://vacua.dev/thoughts/a-webhook-delivery-is-not-a-unique-event/</link><pubDate>Tue, 01 May 2018 00:00:00 +0000</pubDate><guid>https://vacua.dev/thoughts/a-webhook-delivery-is-not-a-unique-event/</guid><description>&lt;p&gt;A subscription callback had already created this local record:&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;carrier: external-freight
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;subscription_id: sub-demo-17
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;secret: old-secret
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;expires_at: 2018-05-08T12:00:00Z&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 sender delivered another validation message for &lt;code&gt;sub-demo-17&lt;/code&gt;, now with a
new secret and expiry. The handler tried to insert a second row. A unique
constraint rejected it, so the activation step never ran.&lt;/p&gt;
&lt;p&gt;The database was right. The handler had mistaken another delivery for another
subscription.&lt;/p&gt;
&lt;h2 id="start-with-the-identity-the-sender-repeated"&gt;Start with the identity the sender repeated&lt;/h2&gt;
&lt;p&gt;A webhook request is evidence that a message was delivered. It is not evidence
that this message has never arrived before.&lt;/p&gt;</description></item><item><title>Map External Data Before It Enters Your Model</title><link>https://vacua.dev/thoughts/map-external-data-before-it-enters-your-model/</link><pubDate>Sun, 01 Apr 2018 00:00:00 +0000</pubDate><guid>https://vacua.dev/thoughts/map-external-data-before-it-enters-your-model/</guid><description>&lt;p&gt;A payment callback arrives with these fields:&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;status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;COMPLETED&amp;#34;&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="nt"&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;7.79&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="nt"&gt;&amp;#34;currency&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;EUR&amp;#34;&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="nt"&gt;&amp;#34;reference&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;session-demo-8&amp;#34;&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="nt"&gt;&amp;#34;transaction&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;remote-demo-31&amp;#34;&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="nt"&gt;&amp;#34;message_time&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2026-04-01T09:15:00Z&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 application does not need a &lt;code&gt;COMPLETED&lt;/code&gt; string or a floating-point amount.
It needs a trusted result: which local operation completed, for how much, in
which currency, under which remote identity, and at what instant.&lt;/p&gt;
&lt;p&gt;Passing the decoded array into business code postpones that translation. It
does not avoid it.&lt;/p&gt;
&lt;h2 id="authenticity-comes-before-interpretation"&gt;Authenticity comes before interpretation&lt;/h2&gt;
&lt;p&gt;The retained callback boundary receives a JSON string and a message
authentication code. It rejects a missing or invalid code before the payment
driver finalizes anything.&lt;/p&gt;</description></item><item><title>Validate at the Boundary You Control</title><link>https://vacua.dev/thoughts/validate-at-the-boundary-you-control/</link><pubDate>Thu, 01 Mar 2018 00:00:00 +0000</pubDate><guid>https://vacua.dev/thoughts/validate-at-the-boundary-you-control/</guid><description>&lt;p&gt;A booking request contains a recipient email and a phone number made entirely
of spaces.&lt;/p&gt;
&lt;p&gt;For one delivery service, email is enough. For the same service with a
particular handoff option, a phone number is mandatory. For another service,
phone is always mandatory.&lt;/p&gt;
&lt;p&gt;There is no honest global rule such as “phone is nullable” or “phone is
required.” The invariant depends on the operation the request asks the system
to perform.&lt;/p&gt;</description></item><item><title>A Third-Party API Is an Unreliable Collaborator</title><link>https://vacua.dev/thoughts/a-third-party-api-is-an-unreliable-collaborator/</link><pubDate>Thu, 01 Feb 2018 00:00:00 +0000</pubDate><guid>https://vacua.dev/thoughts/a-third-party-api-is-an-unreliable-collaborator/</guid><description>&lt;p&gt;A scheduled import begins by listing remote orders. The list request returns a
502 HTML page, so there are no trustworthy item identifiers to process.&lt;/p&gt;
&lt;p&gt;On another run the list succeeds. One order then returns a 504 while its
details are being loaded. The remaining orders are still independent and can
continue.&lt;/p&gt;
&lt;p&gt;These failures share a provider, but not a recovery boundary. Treating both as
“the API is down” either discards useful work or pretends to know more than the
response established.&lt;/p&gt;</description></item><item><title>An HTTP 200 Does Not Mean the Operation Succeeded</title><link>https://vacua.dev/thoughts/an-http-200-does-not-mean-the-operation-succeeded/</link><pubDate>Mon, 01 Jan 2018 00:00:00 +0000</pubDate><guid>https://vacua.dev/thoughts/an-http-200-does-not-mean-the-operation-succeeded/</guid><description>&lt;p&gt;Two requests reach the same JSON application.&lt;/p&gt;
&lt;p&gt;The first lacks authentication. The server returns HTTP 200 with this body:&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;status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&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="nt"&gt;&amp;#34;error&amp;#34;&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="nt"&gt;&amp;#34;code&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&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="nt"&gt;&amp;#34;message&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Unauthenticated&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;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 second is authenticated but lacks permission to browse a collection. It
also returns HTTP 200, this time with an empty array:&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Both behaviors are pinned by retained tests. Neither can be interpreted from
the status code alone. Worse, the empty array is indistinguishable from an
authorized search which genuinely found nothing.&lt;/p&gt;</description></item></channel></rss>