<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Data That Must Survive on Brian Faust</title><link>https://wimdy.works/series/data-that-must-survive/</link><description>Recent content in Data That Must Survive 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/series/data-that-must-survive/rss.xml" rel="self" type="application/rss+xml"/><item><title>Data Outlives the Code That Created It</title><link>https://wimdy.works/thoughts/data-outlives-the-code-that-created-it/</link><pubDate>Fri, 01 Dec 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/data-outlives-the-code-that-created-it/</guid><description>&lt;p&gt;A failed queue record kept returning. Its payload named a mail class the
running application could not resolve correctly, so retrying it could only
repeat the same failure.&lt;/p&gt;
&lt;p&gt;The workaround was blunt: once a minute, delete failed records whose
payload matched that job. It stopped an unusable delivery attempt from
disturbing the queue, but it obviously did not make the payload readable.&lt;/p&gt;
&lt;p&gt;That small incident exposes an awkward storage contract. Serializing an
executable object does not merely store its arguments. It preserves facts about
the code which created it: class name, namespace, property layout, and sometimes
framework behavior. The payload can live much longer than those facts.&lt;/p&gt;</description></item><item><title>Audit History Is Different from Application Logging</title><link>https://wimdy.works/thoughts/audit-history-is-different-from-application-logging/</link><pubDate>Wed, 01 Nov 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/audit-history-is-different-from-application-logging/</guid><description>&lt;p&gt;When a cart appears to expire, support may ask:&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;Which cart changed?
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;What was its prior state?
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;What state followed?
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Who or what caused the transition?
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Which policy allowed it?&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;An engineer investigating the same report may ask:&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;Which request handled it?
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Which worker retried?
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;How long did the database call take?
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Which exception and stack occurred?
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;What else happened on that host?&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 groups need evidence, but making one record answer both sets of questions
usually gives you a bloated audit trail and strangely incomplete logs. Audit
history explains durable business change. Application logs explain software
execution.&lt;/p&gt;</description></item><item><title>An Index Must Serve a Real Query</title><link>https://wimdy.works/thoughts/an-index-must-serve-a-real-query/</link><pubDate>Sun, 01 Oct 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/an-index-must-serve-a-real-query/</guid><description>&lt;p&gt;A production query needed a small page of recent records requiring operational
attention. The table already had an index which looked relevant. The measured
scan still discarded 84,003 historical index entries and recorded 122,936
shared-buffer reads before finding 83 eligible rows.&lt;/p&gt;
&lt;p&gt;The table did not need another vaguely relevant index. It needed an index for
this query:&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;one tenant
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;active records with zero price
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;created during the last three months
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;newest creation time first
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stable identifier as the tie-breaker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;first page only&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;That is the actual starting point: the access path, not a list of columns which
sound important in isolation.&lt;/p&gt;</description></item><item><title>Pagination Needs a Stable Order</title><link>https://wimdy.works/thoughts/pagination-needs-a-stable-order/</link><pubDate>Fri, 01 Sep 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/pagination-needs-a-stable-order/</guid><description>&lt;p&gt;A financial report encoded two sort rules as one string:&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;lowercased customer name + &amp;#34;-&amp;#34; + customer identifier&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Sharded tests with concurrently created identifiers exposed unreliable output.
The correction stopped asking string concatenation to double as a sorting
contract. It compared names directly and consulted the stable customer
identifier only when the names matched.&lt;/p&gt;
&lt;p&gt;The diff does not prove that equal names alone caused the failing
order. It does expose the rule every paginator depends on: the primary
comparison, tie-breaker, and comparison semantics must be explicit, and each
row needs exactly one reproducible position.&lt;/p&gt;</description></item><item><title>Soft Deletes Preserve Ambiguity Too</title><link>https://wimdy.works/thoughts/soft-deletes-preserve-ambiguity-too/</link><pubDate>Tue, 01 Aug 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/soft-deletes-preserve-ambiguity-too/</guid><description>&lt;p&gt;Adding &lt;code&gt;deleted_at&lt;/code&gt; to a table answers one question: when was this row marked as
deleted? It does not answer whether the row was safe to mark, who should still
see it, whether its credentials remain valid, whether it still occupies a
unique business slot, how it can be restored, or when its data should be
erased.&lt;/p&gt;
&lt;p&gt;A cart cleanup could not safely reduce those questions to one nullable
timestamp. Old carts might still have a shipment, a payment session, a booking
attempt, or item work which had crossed a meaningful boundary. The eventual
policy archives a deliberately narrow set and preserves the aggregate as
evidence. Soft deletion preserves rows, but it also preserves every ambiguity
which the lifecycle never defined.&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><item><title>Time Zones Are Part of the Data Model</title><link>https://wimdy.works/thoughts/time-zones-are-part-of-the-data-model/</link><pubDate>Thu, 01 Jun 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/time-zones-are-part-of-the-data-model/</guid><description>&lt;p&gt;One metering path assigns an event to a billing month after converting its
occurrence time into the account&amp;rsquo;s timezone. The same path derives its daily
usage key directly from the occurrence timestamp without that conversion.&lt;/p&gt;
&lt;p&gt;Near midnight, those two fields can disagree:&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;event instant: May 31, 21:30 UTC
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;local business time: June 1, 00:30
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;billing period: June
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stored usage date: May 31&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Each expression looks like an ordinary date calculation. They model different
things, and the defect stays invisible until the difference crosses a day or
month boundary.&lt;/p&gt;</description></item><item><title>Money Is Not a Floating-Point Number</title><link>https://wimdy.works/thoughts/money-is-not-a-floating-point-number/</link><pubDate>Mon, 01 May 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/money-is-not-a-floating-point-number/</guid><description>&lt;p&gt;A booking-time price arrived as four values:&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;net amount: 407
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tax rate: 25.5%
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tax amount: 104
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gross amount: 511&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 amounts are integers in the currency&amp;rsquo;s minor unit. The rate is a decimal
ratio. The contract checks two things: net plus tax equals gross, and the
declared rounding rule produces the supplied tax amount from net and rate.&lt;/p&gt;
&lt;p&gt;None of those details is bookkeeping trivia. Remove the unit, mix the rate&amp;rsquo;s
two representations, or let two services round at different stages, and the
same price can stop balancing while every individual calculation still looks
reasonable.&lt;/p&gt;</description></item><item><title>Null Is a Domain Decision</title><link>https://wimdy.works/thoughts/null-is-a-domain-decision/</link><pubDate>Sat, 01 Apr 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/null-is-a-domain-decision/</guid><description>&lt;p&gt;We added four nullable fields to preserve a pricing snapshot: net amount, tax
rate, tax amount, and gross amount. Each field could now be absent. That did
not make each field independently optional.&lt;/p&gt;
&lt;p&gt;For older records, all four nulls meant one thing: this record predates the
snapshot and does not contain captured pricing evidence. For new records, all
four values had to arrive together. A row containing gross and net but no tax
rate was not “partly known.” It was invalid.&lt;/p&gt;</description></item><item><title>A Migration Is a Production Change</title><link>https://wimdy.works/thoughts/a-migration-is-a-production-change/</link><pubDate>Wed, 01 Mar 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-migration-is-a-production-change/</guid><description>&lt;p&gt;We deployed an additive schema change which the database could execute without
rewriting the table. On paper it was cheap. In production it waited
indefinitely.&lt;/p&gt;
&lt;p&gt;The statement needed an exclusive table lock. A long-running session already
held a conflicting lock, so the migration queued. New application traffic then
queued behind the migration. The column addition was cheap after lock
acquisition; waiting to begin was the incident.&lt;/p&gt;
&lt;p&gt;Another schema change looked cheap for the opposite reason: remove fields and
tables which the application no longer read. The SQL was small, but the
decision would erase financial history from data sets measured in tens of
gigabytes. Neither change could be judged by reading the migration method
alone.&lt;/p&gt;</description></item><item><title>A Transaction Defines What Must Succeed Together</title><link>https://wimdy.works/thoughts/a-transaction-defines-what-must-succeed-together/</link><pubDate>Wed, 01 Feb 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-transaction-defines-what-must-succeed-together/</guid><description>&lt;p&gt;A booking flow reserved funds, completed the remote booking, and captured the
funds. Only then did synchronous response processing fail. The remote booking
existed, the funds were captured, and the caller received a failure.&lt;/p&gt;
&lt;p&gt;The capture was individually valid, and its own database transaction committed
cleanly. It was still too early. We had drawn the success boundary around
“remote booking returned” while the operation promised something larger: a
complete booking result which the caller could use.&lt;/p&gt;</description></item><item><title>Let the Database Reject Impossible State</title><link>https://wimdy.works/thoughts/let-the-database-reject-impossible-state/</link><pubDate>Sun, 01 Jan 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/let-the-database-reject-impossible-state/</guid><description>&lt;p&gt;Two workers asked the same reasonable question: does this owner already have an
active record? Both received the same honest answer: no. Each worker then
created one.&lt;/p&gt;
&lt;p&gt;Nothing in either request was obviously careless. The application checked
first. The insert happened inside a transaction. Tests which ran one request at
a time stayed green. The defect lived in the interval between the check and the
write, where both transactions were allowed to be correct about a state which
had already become stale.&lt;/p&gt;</description></item></channel></rss>