<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Integration on Brian Faust</title><link>https://wimdy.works/tags/integration/</link><description>Recent content in Integration 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://wimdy.works/tags/integration/rss.xml" rel="self" type="application/rss+xml"/><item><title>Parallel Implementations Are Not a Replacement Seam</title><link>https://wimdy.works/thoughts/parallel-implementations-are-not-a-replacement-seam/</link><pubDate>Mon, 01 Apr 2019 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/parallel-implementations-are-not-a-replacement-seam/</guid><description>&lt;p&gt;An old shipping integration and its proposed replacement lived beside each
other. Both had an interface, a driver, request builders, and tests. From the
directory tree, that looked like a seam.&lt;/p&gt;
&lt;p&gt;When the replacement was finally made canonical, the change still crossed
dozens of files. The old interface disappeared, the new one moved, the
canonical driver changed its pickup and shipment flows, and a parallel driver
tree was deleted. Follow-up work then corrected a request shape and added
serialization tests.&lt;/p&gt;</description></item><item><title>A Capability Interface Is Smaller Than a Vendor Client</title><link>https://wimdy.works/thoughts/a-capability-interface-is-smaller-than-a-vendor-client/</link><pubDate>Sat, 01 Dec 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-capability-interface-is-smaller-than-a-vendor-client/</guid><description>&lt;p&gt;A controller needs to register an order webhook. Instead, 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. That is a lot of vendor vocabulary for one product decision.&lt;/p&gt;
&lt;p&gt;The test is no better. It has to intercept construction of that concrete
connection and mock a dynamic property chain. Two useful behavior tests ended
up skipped with the same explanation: the code needed an interface and
dependency injection.&lt;/p&gt;</description></item><item><title>Renaming a Field Is a Protocol Migration</title><link>https://wimdy.works/thoughts/renaming-a-field-is-a-protocol-migration/</link><pubDate>Thu, 01 Nov 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/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, and the implementation is easier to read. It is probably the
better name. That is also beside the point.&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;span class="sr-only" data-copy-status role="status" aria-live="polite"&gt;&lt;/span&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. 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;</description></item><item><title>A Cache Must Distinguish Empty from Unavailable</title><link>https://wimdy.works/thoughts/a-cache-must-distinguish-empty-from-unavailable/</link><pubDate>Mon, 01 Oct 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/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. A TTL is not invalidation with worse timing.&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://wimdy.works/thoughts/partial-work-needs-a-child-ledger/</link><pubDate>Sat, 01 Sep 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/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://wimdy.works/thoughts/retry-budgets-multiply-across-layers/</link><pubDate>Wed, 01 Aug 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/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;span class="sr-only" data-copy-status role="status" aria-live="polite"&gt;&lt;/span&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 still receives their
product. Three attempts multiplied by three attempts is nine calls, regardless
of how harmless each configuration file looks.&lt;/p&gt;</description></item><item><title>A Rate Limit Belongs to All the Workers Sharing It</title><link>https://wimdy.works/thoughts/a-rate-limit-belongs-to-all-the-workers-sharing-it/</link><pubDate>Sun, 01 Jul 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/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,
so it releases itself for fifteen seconds. During that pause, twenty other
workers poll the same provider. They have not seen the response, and 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://wimdy.works/thoughts/an-idempotency-key-is-not-an-identity/</link><pubDate>Fri, 01 Jun 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/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, and both callers receive the same
successful response. 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://wimdy.works/thoughts/a-webhook-delivery-is-not-a-unique-event/</link><pubDate>Tue, 01 May 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/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;span class="sr-only" data-copy-status role="status" aria-live="polite"&gt;&lt;/span&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 to reject it. The handler had mistaken another delivery
for another subscription, then treated the constraint as the problem.&lt;/p&gt;</description></item><item><title>Map External Data Before It Enters Your Model</title><link>https://wimdy.works/thoughts/map-external-data-before-it-enters-your-model/</link><pubDate>Sun, 01 Apr 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/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;span class="sr-only" data-copy-status role="status" aria-live="polite"&gt;&lt;/span&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. Passing the
decoded array into business code merely postpones that translation. It does not
avoid it; it just makes every later caller participate.&lt;/p&gt;</description></item><item><title>An HTTP 200 Does Not Mean the Operation Succeeded</title><link>https://wimdy.works/thoughts/an-http-200-does-not-mean-the-operation-succeeded/</link><pubDate>Mon, 01 Jan 2018 00:00:00 +0000</pubDate><guid>https://wimdy.works/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;span class="sr-only" data-copy-status role="status" aria-live="polite"&gt;&lt;/span&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;span class="sr-only" data-copy-status role="status" aria-live="polite"&gt;&lt;/span&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;Regression tests pin both behaviors. Neither can be interpreted from
the status code alone, and the empty array is worse: it is indistinguishable
from an authorized search which genuinely found nothing.&lt;/p&gt;</description></item><item><title>An Identifier Should Outlive Its Storage Engine</title><link>https://wimdy.works/thoughts/an-identifier-should-outlive-its-storage-engine/</link><pubDate>Sat, 01 Jul 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/an-identifier-should-outlive-its-storage-engine/</guid><description>&lt;p&gt;A billing export once had enough information to reconstruct a missing account,
but not enough to name it. The account still existed in an older system and its
stable identifier reached the export action. What did not exist was a matching
row in the newer application&amp;rsquo;s user table.&lt;/p&gt;
&lt;p&gt;The action worked around that absence by building a temporary object with safe
fallback details. It even gave the object a synthetic local identifier. The
field used as the export identity remained null, though, and a queued data
object later rejected the payload because that field had to be a string. The
fallback had solved the missing attributes while losing the one fact the
export actually needed: which account it was about.&lt;/p&gt;</description></item></channel></rss>