<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Experiments: ML and Data Engineering]]></title><description><![CDATA[Machine learning and Data Engineering posts, thoughts and tutorials.]]></description><link>https://blog.ismail.bio/s/ml-and-data-engineering</link><image><url>https://substackcdn.com/image/fetch/$s_!EF-d!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6df693b6-9478-43c4-91ab-762d4d455088_906x906.png</url><title>Experiments: ML and Data Engineering</title><link>https://blog.ismail.bio/s/ml-and-data-engineering</link></image><generator>Substack</generator><lastBuildDate>Wed, 28 Oct 2026 23:50:20 GMT</lastBuildDate><atom:link href="https://blog.ismail.bio/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Ismail Elouafiq]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[ismail@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[ismail@substack.com]]></itunes:email><itunes:name><![CDATA[Ismail Elouafiq]]></itunes:name></itunes:owner><itunes:author><![CDATA[Ismail Elouafiq]]></itunes:author><googleplay:owner><![CDATA[ismail@substack.com]]></googleplay:owner><googleplay:email><![CDATA[ismail@substack.com]]></googleplay:email><googleplay:author><![CDATA[Ismail Elouafiq]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[How to use formal methods in Data Systems]]></title><description><![CDATA[How to write proofs and arguments for the correctness of a data system.]]></description><link>https://blog.ismail.bio/p/how-to-use-formal-methods-in-data</link><guid isPermaLink="false">https://blog.ismail.bio/p/how-to-use-formal-methods-in-data</guid><dc:creator><![CDATA[Ismail Elouafiq]]></dc:creator><pubDate>Sat, 24 Oct 2026 12:11:34 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/a85f0eaa-29c5-47d6-873b-27c84625a8d7_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is not a call for data engineers to become mathematicians. This is a call to add back the formality that &#8220;engineering&#8221; requires. No one considers a bridge safe because it hasn&#8217;t fallen down yet. But this is more or less the standard that data pipelines and machine learning models are held to.</p><p>This article is the third and last part of a series on <mark data-color="#d9ead3" style="background-color: rgb(217, 234, 211); color: rgb(0, 0, 0);">building data systems that prevent errors by design</mark>. <a href="https://blog.ismail.bio/p/towards-a-data-discipline">Part 1 </a>argued that data systems are in an urgent need of a discipline that prevents errors by design. <a href="https://blog.ismail.bio/p/a-recipe-for-correct-data-systems">Part 2</a> turned those steps of proving, checking and monitoring into a practical recipe, with a worked example.</p><p>This part will use the same example pipeline but will focus on the &#8220;proving&#8221; step. We will go through different levels of arguing about the systems we build, from least to most complex. And how to decide which level of argument a property deserves.</p><p>We saw how writing a simple argument can rule out errors earlier. This is the good news. In my experience, the less formal levels catch most of the errors. The more formal end of the spectrum is mainly for the few properties that are both expensive to get wrong, and easy to get wrong.</p><p> This is part one of the <em>Towards a Data Discipline</em> series:</p><ul><li><p>The <a href="https://blog.ismail.bio/p/towards-a-data-discipline">first post</a> explained the main principles.</p></li><li><p>The <a href="https://blog.ismail.bio/p/a-recipe-for-correct-data-systems">second gives a step by step recipe</a> to follow.</p></li><li><p>And this last part focuses on one specific step: formal arguments to prove what we are building is correct.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">In my Experiments newsletter, I share regularly about building robust data and ML systems, subscribe here if this is your jam</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Let&#8217;s agree on what we mean by &#8220;prove&#8221;</h2><p>Recall the definition from Part 1 of this series: Given a function <em>f</em> over inputs <em>I</em>, a precondition <em>P</em> and a postcondition <em>Q</em>, a proof establishes that <em>&#8704; x &#8712; I : P(x) &#8658; Q(x, f(x))</em>.</p><p>In other words, that for every input: if this input satisfies the preconditions, the postcondition is satisfied for the input/output pair.</p><p>So a proof does not guarantee that the system will not fail. It relies on the precondition <em>P</em>, and it relies on how we model the system. Inputs in real life can violate <em>P</em>, and code can drift from the initial design. This is why we still use checking and monitoring. But a proof also forces us to reason about our design, which helps us define which assumptions we rely on and what errors we may run into and makes the checking and monitoring steps easier.</p><p>In the rest of this article I will actually use the word &#8220;<em>Argument</em>&#8221; more. This is because not all arguments are proofs in the strict sense, and when I said &#8220;prove what you can&#8221; what I really meant is to argue for these properties.</p><div class="pullquote"><h2>The worked example</h2><p>We will use the same worked example from Part 2.</p><p>As a reminder, we have a service that emits two kinds of events: <strong>orders</strong> or <strong>refunds<br></strong>Each has: id, an amount in euros and the time it happened. Events are delivered at least once, so duplicates can happen, and they can arrive late.<br>We must produce a table to show to the finance team, <code>daily_revenue</code>, with one row per day containing: gross sales, refunds.</p><ul><li><p>In our design we decided that: Every morning at 06:00 UTC, the run for day D (on D+1) recomputes the partitions for D-3 to D.</p></li><li><p>The assumptions we made along the way:<br>- A1. Events arrive within 24h.<br>- A2. Every event has a unique id (a duplicate is identical to the original).<br>- A3. Amounts are positive, in euros, in whole cents.<br>- A4. A &#8220;day&#8221; is a calendar day in UTC.</p></li></ul></div><h2>How hard should we prove it</h2><p>As we mentioned in Part 2, we asked two questions about the properties we want to prove:</p><ul><li><p>What does it cost us if this property is false?</p></li><li><p>How easy is it to get the argument wrong?</p></li></ul><p>As we did in our example, the properties end up with a proof chosen from one of the following types:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!liyi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24adfa85-3a6f-47e6-becc-0d33400e9815_2244x1350.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!liyi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24adfa85-3a6f-47e6-becc-0d33400e9815_2244x1350.png 424w, https://substackcdn.com/image/fetch/$s_!liyi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24adfa85-3a6f-47e6-becc-0d33400e9815_2244x1350.png 848w, https://substackcdn.com/image/fetch/$s_!liyi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24adfa85-3a6f-47e6-becc-0d33400e9815_2244x1350.png 1272w, https://substackcdn.com/image/fetch/$s_!liyi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24adfa85-3a6f-47e6-becc-0d33400e9815_2244x1350.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!liyi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24adfa85-3a6f-47e6-becc-0d33400e9815_2244x1350.png" width="727" height="437.39835164835165" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/24adfa85-3a6f-47e6-becc-0d33400e9815_2244x1350.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:876,&quot;width&quot;:1456,&quot;resizeWidth&quot;:727,&quot;bytes&quot;:365434,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.ismail.bio/i/215637494?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24adfa85-3a6f-47e6-becc-0d33400e9815_2244x1350.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!liyi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24adfa85-3a6f-47e6-becc-0d33400e9815_2244x1350.png 424w, https://substackcdn.com/image/fetch/$s_!liyi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24adfa85-3a6f-47e6-becc-0d33400e9815_2244x1350.png 848w, https://substackcdn.com/image/fetch/$s_!liyi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24adfa85-3a6f-47e6-becc-0d33400e9815_2244x1350.png 1272w, https://substackcdn.com/image/fetch/$s_!liyi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24adfa85-3a6f-47e6-becc-0d33400e9815_2244x1350.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">A spectrum of formal arguments. If our properties have a higher risk, we usually want to go towards a more formal argument. Most errors in my experience can be caught in the lower end of the spectrum.</figcaption></figure></div><p>Here is an example of ways to prove that go from easier to write to harder to write (which doesn&#8217;t mean better):</p><ul><li><p><strong>A written argument:</strong> written in the design document or another type of text, a reviewer can spot wrong beliefs and reasoning errors. It doesn&#8217;t guarantee there are no mistakes.</p></li><li><p><strong>Hard constraints (Construction and types): </strong>simply making the invalid states impossible to represent, so that nothing is left to prove (for example, a string variable that contains &#8216;true&#8217; or &#8216;false&#8217; is obviously better off being a boolean). This includes database constraints, parsing, validated types. In machine learning: monotonic constraints, domain-constrained outputs, equivariant architectures and other types of constraints can be added depending on the use case.</p></li><li><p><strong>A structured argument:</strong> this is a step up from the written argument. The proof techniques you may have learned and stopped using (induction over days/partitions, contradiction, contraposition etc.). This is still written text, reviewed by people, but the structure makes it less likely to fail. It requires a bit more time than a written argument.</p></li><li><p><strong>Exhaustive checking:</strong> there are tools that help you write a small model of the design and let a tool explore every state (up to a bound). Alloy for relations/data models, TLA+ for concurrency and orchestration, P for distributed state machines. There are tools for the code itself, I personally have never used any of these but there are bounded model checkers such as CBMC for C and Kani for Rust. The reason for this type of checking, as Daniel Jackson puts it, is that most design issues show up in small examples (exploring all of them is worth more than sampling large ones).</p></li><li><p><strong>Automated proof with a solver:</strong> these are only possible in specific cases, you basically state the claim as logical formulas and let an SMT solver, such as Z3, prove it for every input or give a counterexample. You can call Z3 from Python in a dozen lines, as we will. For Python functions, we have CrossHair checking contracts. For SQL, some promising tools exist such as SQLSolver and VeriEQL but they are not yet everyday tools (solvers struggle with non-linear arithmetic and complex data structures, so this level is more for small but critical claims).</p></li><li><p><strong>Machine-checked deductive proof</strong>: in these types of proofs you write the code together with its specification and prove that they match. Tools include: Dafny (can compile verified code to Python, and other languages), Why3, F*. Proof assistants such as Lean, Rocq and Isabelle. TLA+ has TLAPS. This is a very strong guarantee, but it also implies the highest cost in time and expertise.</p></li><li><p><strong>&#8220;Statistical&#8221; arguments: </strong>this is not a proof and doesn&#8217;t really fit in this list. But some properties cannot be proven that way: how long a job takes to run, how quickly an API, how accurate a model is etc. For these cases, we can still make a good case using statistics (Evaluating a model on a separate validation set is an example). Kochenderfer&#8217;s group is one of those who have thought deeply about such arguments and how they apply for airplane collision avoidance as an example. They usually name three types of tasks: <em>falsification (</em>finding any input that causes failure), <em>most-likely</em> <em>failure</em> (finding errors that are more probable), and <em>failure probability estimation</em> (finding out how often something fails). The book Algorithms for Validation includes these along with formal methods and monitoring (and is part of the inspiration to this series as well)<em>.</em></p></li></ul><p>Now let&#8217;s see where the properties we deduced in Part 2 fit here. We already argued for P1 and P2, so we will only focus on the rest. Here is the register with the type of proof we select in bold.</p><blockquote><p><strong>Proof register:</strong></p><ul><li><p>P3. daily revenue matches the finance team definition. <strong>Type guarantees</strong> and a <strong>written argument</strong>. Depends on A2, A3 and A4.</p></li><li><p>P4. daily revenue is the same whether the table was built daily or rebuilt once (incremental equals full): a <strong>structured argument</strong>, by induction. Depends on P1, P6 and P7.</p></li><li><p>P5. the job is ready by 07:00: <strong>statistical argument</strong> by running multiple times (on the edge examples) and reporting the distribution of time it takes to run.</p></li><li><p>P6. no on-time event is missed: if an event satisfies A1, it is accepted before the last run that recomputes the day of that event. <strong>Machine-checked</strong>, with SMT solver. Depends on A1 and on the scheduler.</p></li><li><p>P7. isolation: an overlapping retry never overwrites newer results. Validated through an exhaustive check of possible interleavings.</p></li></ul></blockquote><p>This is just a way of seeing these properties and where each type of argument may be more appropriate. In our example, we already went through P1-P2 through a simple written argument. This post will focus on the remaining properties and how we can argue for them.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/p/how-to-use-formal-methods-in-data?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Has this been helpful so far? share it with someone you think will benefit.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/p/how-to-use-formal-methods-in-data?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.ismail.bio/p/how-to-use-formal-methods-in-data?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><p></p><h2>How to argue for a property</h2><h3>Hard constraints: make illegal states impossible</h3><p>If an invalid state cannot be represented, it cannot be reached. For example, a string column holding &#8220;true&#8221; and &#8220;false&#8221; is better off being a boolean.</p><p>This level of argument should be used very often (and is used very often) in databases through constraints and type definitions. It also exists in machine learning in the form of monotonic constraints, outputs restricted to a valid domain, equivariant architectures etc.. A pricing model trained with a monotonic constraint on a variable does not need a proof that this variable will follow that rule. In training sets as well, point-in-time joins can be used to avoid leakage.</p><p>Now let&#8217;s look at our example <strong>P3. daily revenue matches the finance team definition. </strong>In the pseudocode below, the <em>Event</em> type can&#8217;t hold a negative amount, a naive timestamp or an event later than we allow, so nothing downstream needs to consider one (there are better types to write this code, but this is enough to show our point)</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># imports and definitions constants here...
class ContractViolation(ValueError):
    """The record goes to quarantine, after this violation"""

@dataclass(frozen=True)
class Event:
    event_id: str
    kind: Literal["order", "refund"] # "order" or "refund"
    amount: Decimal # euros, whole cents, &gt; 0 (A3)
    occurred_at: datetime  # timezone-aware
    received_at: datetime  # timezone-aware

    def __post_init__(self):
        if self.kind not in ("order", "refund"):
            raise ContractViolation(f"unknown kind: {self.kind!r}")
        if self.amount &lt;= 0 or self.amount != self.amount.quantize(CENT):
            raise ContractViolation(f"not a positive whole-cent amount: {self.amount}")
        ...
        if self.received_at - self.occurred_at &gt; MAX_DELAY:
            raise ContractViolation("arrived later than A1 allows")

    @property
    def day(self) -&gt; date:  # A4: a calendar day in UTC
        return self.occurred_at.astimezone(timezone.utc).date()

def parse_event(raw: dict) -&gt; Event:
    """Once we parse here, the assumptions will hold for the rest of the code"""
    return Event(
        event_id=str(raw["id"]),
        kind=raw["kind"],
        amount=Decimal(str(raw["amount"])),
        occurred_at=datetime.fromisoformat(raw["occurred_at"]),
        received_at=datetime.fromisoformat(raw["received_at"]),
    )</code></pre></div><p>A type checker will reject code that tries to build an Event that doesn&#8217;t follow our assumptions. I talked about parsing as a &#8220;check&#8221; in part 2, and now I mentioned it as an argument. Well, it is both, it&#8217;s a check at runtime, but writing this earlier is a proof for what happens downstream. Now that we have this type guarantee, we can continue with the written argument.</p><p>Whichever the use case, this type of argument is a strong and cheap guarantee so check for what types of constraints are needed to guarantee your specific properties.</p><h3>Structured arguments</h3><p>Now let&#8217;s look at a more structured type of proof.</p><p>At the end of the day, this is still reviewed by people. But with a structure that makes it easy to spot any steps we skip, or any mistakes we make while thinking. This is where we can use arguments such as: induction, contradiction, contraposition, etc.</p><p>Here&#8217;s an example for our case: <strong>P4</strong>. <strong>incremental equals</strong> <strong>full:</strong> this property depends on a precondition: every event arrives in time for one of the runs that recompute the day of the event (either the run on the same day or in 3 days after). That precondition is P6, and we will prove P6 later so for now let&#8217;s assume it holds. It also depends on P7 because it assumes runs happen one after the other.</p><blockquote><h4><strong>Proof by induction</strong></h4><p><em><strong>Claim:</strong></em> after the run for day D, the result of every partition up to D equals the result of a full rebuild from all accepted events.<br><em>Base case</em>: before the first run, the table is empty and there is nothing to compare.<br><em><strong>Inductive step:</strong></em><strong><br></strong>Assume the claim holds after the run for day D.<br>Let us consider accepted events between the run for day D and the next on D+1.<br>- These events belong either to a day from D-2 to D+1, or to a later day, not yet published.<br>- The run for D+1 recomputes exactly partitions from D-2 to D+1, each of them equals the full rebuild (by P1).<br>- Partitions older than D&#8722;2 are not touched by this run. By P6, none of the newly accepted events belongs to them. So they are unchanged, and by the induction hypothesis they equal the full rebuild.<br>So the claim holds after running D+1</p></blockquote><p>Notice how this made use of: P1 and P6, while assuming P7. Trying to prove properties usually brings up what you depend on, and how confident you are in those dependencies.</p><h3>Exhaustive Check</h3><p>Testing tries a few cases, but an exhaustive checker tries all of them, up to a certain bound. You write a model of your design, and let a tool explore every state that can be reached. Alloy is such a tool for data models and relations. TLA+ works best for concurrency and orchestration. These tools will catch if something can go wrong within the bounds.</p><p>A small model is usually enough here, because most design errors are small. Daniel Jackson has called this the <em>small scope hypothesis</em>: if a design is flawed, a few small examples usually expose the flaw. This is also why testing as a first pass is encouraged, despite what I said earlier about testing. A test that finds an error early is good as long as it doesn&#8217;t make us overconfident.</p><p>The obvious, and heavily cited, example is that of AWS engineers who had reported that TLA+ caught design errors in their systems that they would not have found otherwise. And engineers became productive with TLA+ in two to three weeks (which is a small price to pay for a critical system, it seems that they have moved on from this recently, and use P language instead to model systems and PObserve to check the logs are following the model).</p><p>For our pipeline example, this type of argument would be overkill. But imagine a scenario where a job fails on day D, we keep retrying until D+1, while the job for D+1 itself already started. Two jobs are now writing the same three days at the same time. Which values end up in the table? Does idempotence still hold? Problems like this are what people tend to reason about worse. If you have taken a parallel programming course you may be better trained to reason in such a way. But even then, when the systems are complex enough, and critical enough, using exhaustive checks can let the machine examine what your mind is too small to hold.</p><p>So let us check it anyway for the sake of example:</p><blockquote><p><strong>P7. isolation: an overlapping retry never overwrites newer results</strong></p><p>Once again, this exhaustive check will only check for the model we defined in this code.<strong> </strong>Our actual code can still be different or drift away from this model, what we basically do here is we take our <em>data pipeline design &#8594; simplify into a model &#8594; enumerate and check interleavings</em>. </p><p>Runs can overlap, but neither run can execute its own steps out of order.<br>We will add an input variable called <code>fencing</code> , when it is true older runs are not allowed to overwrite newer runs. We will see here that when this is set to false we end up with a lot of possibilities of interleavings (in our case, more than half of the time)</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f601fbb6-8174-445d-9019-b3c43679bf7e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from itertools import combinations

def run(name, partitions):
    """This models the run as a sequence of steps, starting with 'start'
    followed by the partitions"""
    return [(name, "start")] + [(name, p) for p in partitions]

def interleavings(a, b):
    """Every order in which the steps of two runs can happen."""
    n = len(a) + len(b)
    for positions in combinations(range(n), len(a)):
        ia, ib = iter(a), iter(b)
        yield [next(ia) if i in positions else next(ib) for i in range(n)]

def execute(steps, fenced):
    """Executes the given steps with optional fencing.
    Fencing means that a partition will not be updated with an older snapshot."""
    clock, snapshot, table = 0, {}, {}
    for name, step in steps:
        # The "start" step records when the run begins
        # Each number represents a partition it writes
        if step == "start":
            clock += 1
            snapshot[name] = clock
        elif fenced and table.get(step, 0) &gt; snapshot[name]:
            continue # here we do not overwrite a partition with older data
        else:
            # here we update the partition with the current snapshot of the run
            table[step] = snapshot[name]
    return table, snapshot

fenced = False
D=10
retry = run("retry of day D", range(D - 3, D + 1))
nxt = run("run for D+1", range(D - 2, D + 2))
total = bad = 0
for steps in interleavings(retry, nxt):
    table, snapshot = execute(steps, fenced)
    # each partition must end up with the newest snapshot that wrote it
    expected = {}
    for name, partitions in (("retry of D", retry), ("run for D+1", nxt)):
        for _, p in partitions[1:]:
            expected[p] = max(expected.get(p, 0), snapshot[name])
    total += 1
    bad += table != expected

print(bad, total)  # (143, 252)
# if you set fenced = True, we get no issues</code></pre></div></blockquote><p>What we have shown here is that &#8220;<em>assuming this model holds, and fencing is enabled, we prevent older runs from overwriting newer data in all possible interleavings</em>&#8221; This doesn&#8217;t show our pipelines truly follows this model, nor that fencing is enabled. And that if fencing is not enabled, this can definitely happen.</p><p>We can make this safe in practice for example by forcing the orchestrator to never run two of these jobs at once, or to have a version of the snapshot within the run.</p><h3>Automated proof with a solver</h3><p>Now let&#8217;s look at P6 which we promised to use an SMT solver for: <strong>P6. no on-time event is missed</strong>.<br>If an event satisfies the assumption (A1, arrives within 24h) the event is received before the last run that recomputes its day.</p><p><strong>What a solver does: </strong>an SMT solver, such as Z3, takes a set of conditions on some unknowns, for example x &gt; 3 and x + y = 10. It then does one of two things.<br>- Finds values that satisfy every condition at once<br>- Or it proves that no such values exist.<br>It does not try values one by one. It reasons about the conditions in a similar way that you would solve an equation instead of trying all numbers that may or may not solve it.</p><p><strong>How that becomes a proof:</strong> in our case, we ask the solver to find a counterexample that satisfies A1 and is still missed. If it finds one, then we have an error. If it reports that no such event can exist, then P6 holds as long as the preconditions are valid.</p><p>A written argument for this simple case could be short and convincing enough. But a solver should be considered if we have the time to invest, and consider this property critical enough. Here&#8217;s an example using Z3</p><blockquote><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8e9513d7-1311-47e5-9978-fff8e55b490c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from z3 import Ints, Solver, sat, unsat
from revenue import DAYS_BACK, MAX_DELAY, RUN_START

DAY = 86_400 # in seconds

def seconds(t):
    return int(t.total_seconds())

def find_missed_event(days_back=DAYS_BACK, run_start=RUN_START, max_delay=MAX_DELAY):
    """Find event that:
      - satisfies A1
      - and is still missed.
      Missed here means: it is received only at or after the start of the
      last run that can recompute it"""
    occurred, received = Ints("occurred received")  # defined in seconds since 1970, UTC
    day = occurred / DAY # integer division: its UTC day (A4)
    # The run for day D starts on D+1 and recomputes D-days_back up to D,
    # so day d is last recomputed by the run for d + days_back.
    last_run = (day + days_back + 1) * DAY + seconds(run_start)
    solver = Solver()
    solver.add(occurred &gt;= 0,
               received &gt;= occurred,
               received - occurred &lt;= seconds(max_delay),  # A1
               received &gt;= last_run)                       # ...and yet missed
    result = solver.check()
    if result == unsat:
        return None # proved: no such event exists
    if result == sat:
        return solver.model() # a counterexample
    raise RuntimeError("The solver was unable to decide.")</code></pre></div></blockquote><p>In simple words, the function states three facts: when the event occurred, when it was received, and that the delay between the two respects A1. Then it adds the condition that the event arrived too late. If <code>find_missed_event()</code> returns None, no event can satisfy this. This also makes things more robust to changes in the code, if someone changes the <code>DAYS_BACK</code> in the code, this property will be validated again.</p><p>A little word of honesty: as you see in the end, a solver can answer <em>sat</em>, <em>unsat</em> or <em>unknow. </em>When I first wrote this, it worked such that unknown was also considered a proof. Errors like this can happen especially for someone like myself who is very new to z3 (I&#8217;m sure experienced people would have spotted this faster). But this is just a word of caution again, that the guarantees of these arguments are great, but they are not completely free of flaws.</p><p>You can also test the SMT solver by going into a case where you know it should fail (setting days back to 0 in our example) and see that it comes up with counterexamples.<br>The solver obviously proves this for a model of the design, not for what happens when we hand it over to the orchestrator.</p><h3>Statistical arguments</h3><p>I left P5 last as it is a different kind of argument. <strong>P5. ready by 07:00</strong>: assuming the run starts at 6:00 it must finish within the hour. A proof cannot really establish this. But we can use a statistical argument.</p><blockquote><p><strong>P5. ready by 07:00 : </strong>Before we start this argument let&#8217;s make an assumption, we can add this to our assumptions register and check for it when monitoring: <em>A5. daily volume never exceeds twice the volume of the busiest day observed so far</em>.</p><p>Let&#8217;s say we now run twice the maximum volume observed 300 times on our cluster. And say the slowest run takes 19 minutes. It feels like we have enough evidence, but the evidence is weaker than it feels. 300 runs without a single overrun allow us to say, with 95% confidence, that a run overruns less than one time in a hundred. But we don&#8217;t know the tail of our model.</p></blockquote><p>Here are four directions we could go:</p><ul><li><p><strong>Accept the risk:</strong> and consider this to be enough for our case. After all, the finance team wants to guarantee this just 90% of the time.</p></li><li><p><strong>Run it more:</strong> 3000 clean runs would bring those down to one in a thousand. But this is 10x more expensive.</p></li><li><p><strong>Make an assumption about the tail distribution</strong>: and add we add that distribution as an assumption to the register. We can use it to estimate how likely a run is to go beyond an hour. But because the distribution is an assumption we may want to verify it and monitor for it depending on how critical it is.</p></li><li><p><strong>Sample the harder cases:</strong> if we are interested in the rare event, this problem is what failure probability estimation deals with. Simulating ordinary days over and over is not an efficient way to learn about the rare events. One of the methods that can be used here is <em>importance sampling</em>, one of the families of methods Kochenderfer et al. survey in <em>Algorithms for Validation</em>. This means sampling from a distribution biased towards errors (more volume, more late events etc.). We can then give each run a weight: how likely the conditions are on an ordinary day, divided by how likely we made them be in our sampling distribution. The weights correct for the bias we introduced, so the estimate still describes ordinary days, but now most of your runs are spent where the errors are most likely. However, this requires a distribution of what a &#8220;normal&#8221; day looks like. We usually don&#8217;t have this, which means that if we take this route we need to model this distribution and add that as a new assumption, by fitting it to history and then monitoring like any other assumption. Once again, <em>Algorithms for Validation</em> is the place to read about such strategies.</p></li></ul><p>Whichever way we go, what they all should have in common is a form of transparency: stating the confidence, and writing down the assumptions we are making so that we can monitor them later.</p><h3>But the code will drift away</h3><p>If you prove for your current design, the code can (will) drift away with time. So how can we keep it consistent with our proofs?</p><p>Testing can close some of the gap. For example, property-based testing states the property&#8217;s own claim, and can look for inputs that break it. This is what Hypothesis is for in Python as an example. This test is still only sampling, but it samples whether the code stays faithful to our argument.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from hypothesis import given, settings, strategies as st

# events(): a strategy for well-formed, on-time events (A1 to A3), duplicates included
@given(st.lists(events(), max_size=50))
@settings(max_examples=500)
def test_incremental_equals_full(evts):
    # replay_daily_runs: run the job every morning, on what had arrived by then
    # rebuild_from_scratch: one run at the end, over everything accepted
    assert replay_daily_runs(evts) == rebuild_from_scratch(evts)</code></pre></div><p>On our design, this would pass 500 generated histories in about two seconds. We are still just sampling but we are checking if the code still behaves in a way that the proof expects.</p><p>It&#8217;s always good practice to make sure the test can fail. Try an example where it should fail and Hypothesis should find a counterexample in a few seconds.</p><p>And of course, this is not a guarantee that the code will not drift. There are other tools that can improve this. CrossHair for example can explore Python code symbolically, using Z3 in the background. At the far end of the arguments Dafny closes it entirely (the code and the proof are tied together and the program wouldn&#8217;t compile unless the proof holds). But let&#8217;s be honest, most data teams will not go that far. But in case of very critical systems, we have to know what exists and what tradeoffs we are making.</p><h1>Conclusion</h1><p>Most of the guarantees of correctness in this series came from the simple parts: a falsifiable statement, an assumptions register, a few written arguments, type constraints, and parsing. The expensive tools (solvers, model checkers, proof assistants) only become interesting on one or two properties at most (which we may have exaggerated in this example).</p><p>And if you use language models for assistance writing code (who doesn&#8217;t at this point), just make sure you do the reasoning yourself first. Language models can assist you in reviewing it, they can assist in spotting flaws, as well as assist in writing and reviewing Z3 or Hypothesis code or similar. But if we completely rely on them, we not only risk having them solve something different from what we define as correct. I&#8217;m writing a few skills that can help in this process that I will share both here and by email as soon as they&#8217;re ready.</p><p>This whole series can be put in three lines:</p><blockquote><ul><li><p><strong>Prove what you can,</strong> so that some errors are impossible by construction.</p></li><li><p><strong>Check at the boundaries,</strong> so that the preconditions of the proofs also hold.</p></li><li><p><strong>Monitor the rest,</strong> and give every alert an owner and a checklist.</p></li></ul></blockquote><p><strong>None of this is new.</strong> What I hope this series adds is a practical way to apply it to data systems, without turning every engineer into a mathematician. If you build data or ML systems, I would genuinely like to hear if this is helpful at all.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/p/how-to-use-formal-methods-in-data/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://blog.ismail.bio/p/how-to-use-formal-methods-in-data/comments"><span>Leave a comment</span></a></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.ismail.bio/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item><item><title><![CDATA[A Recipe for “correct” data systems]]></title><description><![CDATA[This is a step-by-step recipe for building data systems that prevent errors, with a worked example. It's part 2/3 of my Towards a Data Discipline series]]></description><link>https://blog.ismail.bio/p/a-recipe-for-correct-data-systems</link><guid isPermaLink="false">https://blog.ismail.bio/p/a-recipe-for-correct-data-systems</guid><dc:creator><![CDATA[Ismail Elouafiq]]></dc:creator><pubDate>Sat, 24 Oct 2026 12:10:36 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/1bd0394c-45d7-401a-aba7-be5cefe1813a_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Data pipelines rarely fail, they usually mislead quietly.</p><p>My last post, <a href="https://blog.ismail.bio/p/towards-a-data-discipline">Towards a data discipline</a>,<strong> </strong>explained why we need a data discipline. A way of building data systems to prevent errors by design, instead of detecting them after the fact.</p><p>This post turns that into a practical step by step recipe. It follows the spirit of <a href="https://karpathy.github.io/2019/04/25/recipe/">Karpathy&#8217;s recipe for training neural networks</a>.  His observation that badly trained networks rarely announce errors, and just quietly perform worse, is shared across data systems including data pipelines. The main reason for this article is to document <mark data-color="#d9ead3" style="background-color: rgb(217, 234, 211); color: rgb(0, 0, 0);">a process for building data systems that are correct by construction</mark>. This post may be opinionated (read: biased), so any criticism is very welcome.</p><p>This is part one of the Towards a Data Discipline series:</p><ul><li><p>The <a href="https://blog.ismail.bio/p/towards-a-data-discipline">first post</a> explained the main principles.</p></li><li><p>This post gives a step by step recipe to follow.</p></li><li><p>And <a href="https://blog.ismail.bio/p/how-to-use-formal-methods-in-data">the last part </a>will focus on writing more complex formal arguments.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">if you are interested in such topics in general feel free to subscribe</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><div class="pullquote"><h4><strong>A worked example:</strong></h4><p><em>We will follow a small imaginary data pipeline in the whole article. This pipeline is deliberately simple so that we can focus on the principles.</em></p><p>A service emits two kinds of events: <strong>orders </strong><em>or</em> <em><strong>refunds</strong></em><br>Each has: id, an amount in euros and the time it happened. Events are delivered at least once, so duplicates can happen, and they can arrive late.</p><p>We must produce a table to show to the finance team, <code>daily_revenue</code>, with one row per day containing: gross sales, refunds, net revenue and the number of orders.</p></div><h2>The good news about proofs and formal methods</h2><p>I want to make this clear. Formal methods have a reputation for being complex and expensive. And some of them are. But, in my experience, <strong>most errors are caught at the less formal end</strong>: when you try to write a falsifiable statement, when you list assumptions, and when you try to explain, in simple words, why a property holds. More complex solvers and proof assistants are for the few properties where a mistake is expensive and the argument is easy to get wrong. The next post will go into more detail on proofs and writing arguments.</p><h1>The full recipe</h1><p>We build things in the wrong order today: <br><em>write ad hoc code &#8594; write tests, maybe &#8594; ship and decorate with monitoring. </em></p><p>And we end up introducing a lot of new concepts such as data contracts, SLAs, SLOs, etc. etc. I would argue that skipping explicit reasoning about correctness is a big part of why we keep reinventing ideas that other engineering fields have always been using.</p><p>This recipe changes the order in which we do things:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1DXU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F545c2578-0d89-4625-82bd-b2a822b7bdbd_1282x1002.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1DXU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F545c2578-0d89-4625-82bd-b2a822b7bdbd_1282x1002.png 424w, https://substackcdn.com/image/fetch/$s_!1DXU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F545c2578-0d89-4625-82bd-b2a822b7bdbd_1282x1002.png 848w, https://substackcdn.com/image/fetch/$s_!1DXU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F545c2578-0d89-4625-82bd-b2a822b7bdbd_1282x1002.png 1272w, https://substackcdn.com/image/fetch/$s_!1DXU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F545c2578-0d89-4625-82bd-b2a822b7bdbd_1282x1002.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!1DXU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F545c2578-0d89-4625-82bd-b2a822b7bdbd_1282x1002.png" width="1282" height="1002" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/545c2578-0d89-4625-82bd-b2a822b7bdbd_1282x1002.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1002,&quot;width&quot;:1282,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:117733,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.ismail.bio/i/215508232?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F545c2578-0d89-4625-82bd-b2a822b7bdbd_1282x1002.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1DXU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F545c2578-0d89-4625-82bd-b2a822b7bdbd_1282x1002.png 424w, https://substackcdn.com/image/fetch/$s_!1DXU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F545c2578-0d89-4625-82bd-b2a822b7bdbd_1282x1002.png 848w, https://substackcdn.com/image/fetch/$s_!1DXU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F545c2578-0d89-4625-82bd-b2a822b7bdbd_1282x1002.png 1272w, https://substackcdn.com/image/fetch/$s_!1DXU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F545c2578-0d89-4625-82bd-b2a822b7bdbd_1282x1002.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>For the sake of repetition: skipping a step is allowed but should be explicit.</p><h2>#0 - Define as you go</h2><p>As we mentioned already: you cannot define a data system you don&#8217;t understand, and you will not understand it unless you get your hands dirty with some experimentation first.</p><h3>0.1. Experiment</h3><p>Open a notebook, look at the events, do some exploratory data analysis, look for inconsistencies, duplicates, patterns, ask a domain expert etc. All of this can give you insights such as: all events &#8220;usually&#8221; arrive within a day, or that &#8220;daily&#8221; means a different thing for the finance team than it does for the logistics team etc. etc.</p><p>The goal of the experimentation step is not to be restrictive. But it has an exit condition. You are only done with experimentation when you can: state the problem properly, when you have an idea of how you can solve the problem (a rough sketch of a design you think will solve it), and you have a clear set of assumptions (such as what &#8220;daily&#8221; means etc.)</p><h3>0.2. Define what correct means</h3><p>After the experimentation, we may be able to have our definition of &#8220;correct&#8221; as something like this:</p><blockquote><p>For 3 consecutive weeks,<br>rows for day D are successfully published by 07:00 UTC on D+1 at least 90% of the time. And by 08:00 UTC at least 99% of the time.<br>And once day D is closed, from D+4 onwards, its net revenue matches the finance ledger to the cent.</p></blockquote><p>Now, if you want to evaluate your definition, here is a question to use as a test:<br>&#8220;What observation would imply your statement is incorrect?&#8221; If there is no observation that will show your statement is incorrect. Then your statement is not falsifiable.</p><p>I know that at times this can be hard to do for certain problems, but we have to start somewhere. When you approach a problem without having a proper definition, you are still having an implicit definition, so we must never use &#8220;this problem is hard to measure&#8221; as an excuse. The choice is between having this definition be implicit or having it explicitly and clearly stated.</p><p>As we previously mentioned <em>measurements do mislead</em>. Once a number becomes a target, people tend to optimise the number instead of what is meant. This is one more reason why we have to write down the assumptions.</p><h3>0.3. Keep a list of assumptions</h3><p>Write all the assumptions (and simplifications) you gather along the way in a <mark data-color="#d9ead3" style="background-color: rgb(217, 234, 211); color: rgb(0, 0, 0);">register of assumptions</mark>. This can be a markdown file near the documentation.</p><p>Every pipeline rests on a set of small beliefs and decisions. Not writing them down will almost always come back to haunt you. Writing them down can be useful for us to decide: do we want to prove this, check for it, monitor it, or accept it if it&#8217;s trivial. </p><p>Here are examples of assumptions for our finance data pipeline above:</p><ul><li><p>A1. Every event arrives within 24 hours of when it happened. (Observed maximum: 21 hours.)</p></li><li><p>A2. Every event has a unique id. Events may be delivered more than once, but a duplicate event is identical to the original.</p></li><li><p>A3. Amounts are positive, in euros, in whole cents.</p></li><li><p>A4. A &#8220;day&#8221; is a calendar day in UTC. (Confirmed with Anna from finance on the 17th of September 2025)</p></li></ul><p>What matters here is that these assumptions can be cited later, they can be checked and updated, and they are open for review.</p><blockquote><p><strong>Step #0 can be considered done when: </strong>you have a clear statement of what it means for your system to be &#8220;correct&#8221;. You have a rough sketch of how to solve it and have verified that through experimentation. You have a register of assumptions about the world and what different definitions mean.</p></blockquote><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/p/a-recipe-for-correct-data-systems?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Has this been helpful so far? share it with someone you think will benefit</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/p/a-recipe-for-correct-data-systems?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.ismail.bio/p/a-recipe-for-correct-data-systems?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><h2>#1 Prove what you can</h2><p>Because this step needs an article on its own, I will only introduce the simpler ideas here and a more detailed version is available in the <a href="https://blog.ismail.bio/publish/post/215637494">next post here</a>.</p><p>Now is the time to design and architect the data system. As we do so we will want to define &#8220;what properties must be true, and how can we convince someone else that they are?&#8221;.</p><p><strong>This forces a modular design!</strong> Dijkstra puts this more eloquently when he said that the competent programmer is &#8220;fully aware of the strictly limited size of his own skull&#8221;. A proof of an entire pipeline cannot be reviewed, if at all can be written. A proof of one critical function is usually reviewable and relatively easy to write.</p><h3>State the properties to prove</h3><p>The entire goal of this step is that you have a list of properties to prove in a <strong>proof register</strong>. In this register we want to have:</p><ul><li><p><em>what</em> we want to prove: properties that the data system should guarantee.</p></li><li><p><em>how</em> we want to prove it: we will do this in the next step, more critical properties may require more formal proofs, or other forms of validation.</p></li></ul><p>The definition step before should have given us: 1. A statement about the whole data system. 2. A list of assumptions. 3. A rough design of the system that can be improved upon. The first thing I would do in this case is select from this <em>what parts of the system are more critical in terms of correctness</em>. Before we jump into generic properties like idempotence and determinism, we first want to show the system achieves its specific goals.</p><p>In our example, we can take part of the statement: <em>&#8220;For 3 consecutive weeks, rows for day D are successfully published by 07:00 UTC on D+1 90% of the time&#8221; </em>And see what it implies for each module in our design. What each module needs to verify to achieve this, and which modules are more critical to this statement. For example, the scheduler must start early enough, and the job must finish by 07:00 UTC. Add all those properties to your proof register.</p><p><strong>On general properties to validate for data pipelines:</strong> After the above (specific) properties are added, in the case of a data pipeline we can add things that come up often. I have found the following list useful to go through, and I&#8217;m sure I have missed a few others. The reason they keep turning up is because retries, redeliveries, late data, etc. are usually part of every data pipeline. None of these are surprising to any data engineer, and they cover a big share of what goes wrong:</p><ul><li><p><strong>Determinism:</strong> the same inputs give the same output, whatever the order in which they arrive, or how many workers process them.</p></li></ul><ul><li><p><strong>Idempotence</strong>: running the job twice for the same day leaves the table exactly as running it once. This is what makes retrying your job harmless.</p></li><li><p><strong>Incremental equals full</strong>: in other words that running the job every day for a month produces the same table as rebuilding it at the end of the month in one go. Think of when a backfill produces a different result from what you have been publishing daily, and now you have to explain which one is true. </p></li><li><p><strong>Conservation</strong>: nothing is created or lost in transit. In our pipeline example, every received event ends up in exactly one place: either accepted or quarantined. Keeping the raw history immutable makes this easy to verify.</p></li><li><p><strong>Invariants</strong>: what must be true of the output at rest (independent from how the pipeline ran or how it&#8217;s designed). For example in our pipeline this can mean: exactly one row per day, or the fact that net revenue equals the gross revenue minus refunds.</p></li><li><p><strong>Isolation between runs:</strong> two overlapping runs must not overwrite each other&#8217;s results. A retry still running when the next day&#8217;s run starts could cause issues.</p></li></ul><p>I am not arguing that any of these properties must be guaranteed in every data system. These are properties I personally look at from time to time to check if they are needed in a specific scenario. <a href="https://maximebeauchemin.medium.com/functional-data-engineering-a-modern-paradigm-for-batch-data-processing-2327ec32c42a">Maxime Beauchemin&#8217;s article from 2018 on functional data engineering</a> is still a read I would recommend for reasoning about some of these. Although it focuses on Airflow, much of it is generalisable.</p><p><strong>Beyond data pipelines: </strong>the same principle applies to any data system: start from what the system is for, and ask what each module must guarantee for that to hold. The following are a few other properties from other kinds of system, each of which I have seen being repeated in enough cases to put as a list:</p><ul><li><p><strong>For training sets used in an ML model:</strong> no feature value is used before the moment it would have been known. The absence of point in time correctness will lead to leakage.</p></li><li><p><strong>A pricing model:</strong> all else equal, the predicted price never decreases as floor area grows. XGBoost and LightGBM can guarantee this by construction, through monotonic constraints.</p></li><li><p><strong>An access layer:</strong> no combination of roles can read a column tagged as personal data (AWS for example uses an SMT solver to answer such questions, we will talk more about SMT solvers in <a href="https://blog.ismail.bio/publish/post/215637494">the next post</a>).</p></li><li><p><strong>Schema migration:</strong> queries run against the old schema return the same rows against the new schema.</p></li></ul><p>Are there any similar things that you often see repeated? It would be very helpful if you let me know here in a comment.</p><h3>Decide how hard you will prove each property</h3><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2DjO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 424w, https://substackcdn.com/image/fetch/$s_!2DjO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 848w, https://substackcdn.com/image/fetch/$s_!2DjO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 1272w, https://substackcdn.com/image/fetch/$s_!2DjO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!2DjO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png" width="638" height="180.97115384615384" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:413,&quot;width&quot;:1456,&quot;resizeWidth&quot;:638,&quot;bytes&quot;:356349,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.ismail.bio/i/208690946?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!2DjO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 424w, https://substackcdn.com/image/fetch/$s_!2DjO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 848w, https://substackcdn.com/image/fetch/$s_!2DjO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 1272w, https://substackcdn.com/image/fetch/$s_!2DjO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>A proof can go from: a written argument in the design docs, to a structured argument, such as induction over partitions. Or even a machine-checked model or proof (such as using TLA+ for concurrency and orchestration)</p><p>More formal proofs are not always better. They are more expensive, but provide stronger guarantees to prevent errors. There are two questions I would ask to figure out what level to choose: &#8220;<em>what does it cost us if this is false?&#8221;</em>, and &#8220;<em>how easy is it to get the argument wrong?</em>&#8221;. I wrote more details on this in the <a href="https://blog.ismail.bio/publish/post/215637494">next post</a>.</p><p>From all the above we can complete our proof register, this list will now have what we want to prove and how we intend to prove it (including what we will not prove and why). Here&#8217;s an example for our daily revenue pipeline:</p><blockquote><p><strong>Proof register:</strong></p><ul><li><p>P1. determinism: type guarantees and a written argument. Depends on assumption A2.</p></li><li><p>P2. idempotence: written argument. Depends on P1.</p></li><li><p>P3. daily revenue matches the finance team definition. Type guarantees and a written argument. Depends on A2, A3 and A4.</p></li><li><p>P4. daily revenue is the same whether the table was built daily or rebuilt once (incremental equals full): a structured argument, by induction. Depends on P1 and P6.</p></li><li><p>P5. the job is ready by 07:00: statistical argument by running multiple times (on the edge examples) and reporting the distribution of time it takes to run.</p></li><li><p>P6. no on-time event is missed: if an event satisfies A1, it is accepted before the last run that recomputes the day of that event. Machine-checked, with SMT solver. Depends on A1 and on the scheduler.</p></li><li><p>P7. isolation: an overlapping retry never overwrites newer results. Validated through an exhaustive check of possible interleavings.</p></li><li><p>Not proven: agreement with the ledger depends on the finance system.</p></li><li><p>Not proven: the scheduler runs the job at all, which is someone else&#8217;s software. Both go to monitoring.</p></li></ul></blockquote><p>The last lines matter just as much. A reviewer can now see what we skipped and argue about it.</p><blockquote><p>As a summary, before you leave this step you should have a proof register including:</p><ul><li><p>what you will prove.</p></li><li><p>how you will prove it.</p></li><li><p>what you choose NOT to prove should be explicit.</p></li></ul><p>The next article has exact steps to help you decide this with an example.</p></blockquote><h3>Write the arguments before writing code</h3><p>Write the arguments before you write code. Quick falsification (a few tests, a small script) is still worth doing at any step, because it is the cheapest way to find out an argument is wrong, and perhaps you would have done much of this during the experimentation step but it is the argument that usually tells you what to test.</p><p>Now we have a list of things to prove and how we will prove each one.</p><p>Let&#8217;s only look at the simple arguments here, and keep the more complex ones for <a href="https://blog.ismail.bio/publish/post/215637494">the next post</a>.</p><blockquote><p><strong>P1. determinism: </strong>the function reads nothing about the environment (no clock, no environment variables, no randomness). It removes duplicates by id, which cannot depend on the order of arrival (A2). The order of summation does not matter, since amounts are Decimals, and Decimal addition is exact within the precision we need. The output therefore must depend only on the set of accepted events for that day.</p></blockquote><p>Note that, for example, the last sentence could be false if we had floats in Python<em>, </em><code>(0.1 + 0.2) + 0.3</code><em> </em>gives<em> </em><code>0.6000&#8230;.0000001</code><em> </em>and<em>  </em><code>0.1 + (0.2 + 0.3)</code><em>  </em>gives<em> </em><code>0.6</code><em>. </em>Which means that the total could depend on the order, and produce different numbers for the same events<em>.</em></p><blockquote><p><strong>P2</strong>. <strong>idempotence:</strong> running on day D replaces partitions from D-3 to D with values computed from the accepted events for those days. Replacing a value with the same value twice is the same as doing it once, and gives the same value.</p></blockquote><p>That argument would fail, if the job appends rows instead of replacing partitions, and the system would fail on a retry. It also assumes P7, that there is no issue with overlapping jobs (we will argue for P7 in part 3).</p><p>These examples are perhaps too simple, but think of how many times we actually try to argue for idempotence and determinism when we build data pipelines. Very rarely.</p><p>For the more complex end of arguments check the <a href="https://blog.ismail.bio/publish/post/215637494">next blog post on formal methods here</a>.</p><h3>Write the tests and the code</h3><p>Write the tests for the arguments above. Tests now have a precise job, they do not ask about a few examples you can think of on the fly. They ask whether the code follows the properties you care about the most.</p><p>Property-based testing fits well here and the next post will show an example of it, with a library such as Hypothesis in Python. Each test states a property from the register, and Hypothesis generates hundreds of inputs that satisfy the preconditions and tries to break the test. Because they can find counterexamples early, it is justified to use property-based tests before investing time in writing an argument.</p><p>For writing good tests I would defer to: the testing section of <em>Software Engineering at Google</em>, <em>How to Specify It!</em>  by John Hughes for property-based testing, and Google&#8217;s <em>ML Test Score for data and ML systems</em>.</p><p>Then you can write the code matching the design, and while doing so write the types and contracts that encode the assumptions, and the functions and jobs that attempt to guarantee them.</p><blockquote><p><strong>Definition of done: </strong>you are done with this step when the properties in your register are argued for, every argument names the assumptions it relies on, and every property has a test (or a machine-checked proof).</p></blockquote><h2>#2 Check at every boundary</h2><p>Now is the time where we put those assumptions to a good use.</p><p><strong>Check at the boundary: </strong>means simply execute the precondition.</p><p>In our example, we have four clear boundaries:</p><ol><li><p>We receive raw events from the checkout service that become accepted events.</p></li><li><p>Those accepted events are handed over. No check is needed here: the types and parsing should already fulfill our needs.</p></li><li><p>When writing to the table that is handed to the finance team.</p></li><li><p>The orchestrator&#8217;s config, which must run as expected at the time expected.</p></li></ol><p>When the events are received we can make use of the idea of <a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/">parse don&#8217;t validate</a> (recommended read by Alexis King) to parse the events in a way that holds the assumptions. If an event fails we can reject them and add them to a quarantine table instead of correcting them and letting an error leak into the results.</p><p>Isolating late events, for example, is not only good practice in this case, our proof for incremental-equals-full, which we will write in the <a href="https://blog.ismail.bio/publish/post/215637494">next post</a>, depends on it. Note that for our example, if we reject a late event and put it in quarantine, the results will probably disagree with the finance team&#8217;s calculations. But this will help us review our assumptions about late events, and will also be relatively easy to figure out.</p><p>When the events are written we need a contract with the reader (in our example the finance team). This should contain for example:</p><ul><li><p><em>Structure:</em> one row per UTC day, including days without sales, with the columns day, gross, refunds, net and orders.</p></li><li><p><em>Meaning:</em> amounts in euros, net equals gross minus refunds, net may be negative.</p></li><li><p><em>Delivery:</em> provisional by 07:00 UTC on D+1, and final from D+4, and a final day never changes again without an announced reopening.</p></li></ul><p>Two things about this contract. First, &#8220;net may be negative&#8221;: writing down what is allowed matters as much as writing down what is not: it is what stops a well-meaning colleague from trying to fix it. Second, &#8220;a final day never changes&#8221;: it follows from the design (each run only replaces partitions D-3 to D, see P2), since no run touches an older partition. We promise something we have argued for.</p><p>In Meyer&#8217;s terms, what we did here is: use the parser to state the preconditions, and the contract to state the postconditions to the finance team (and &#8220;one row per day&#8221; and &#8220;net equals gross minus refunds&#8221; are invariants that must hold)</p><blockquote><p><strong>Definition of done</strong> for step 2: every assumption about the world in the register is checked at the boundary where it enters, or there is a written note explaining why it cannot be; and every promise to downstream users is written down</p></blockquote><h3>#3 Monitor the rest</h3><p>Whether the world keeps behaving the way the register says it does, whether the evidence behind our statistical arguments still applies, and whether the system as a whole meets the statement from step 0. Monitoring is now a short list, because we know exactly what to watch. Every item on it points to what it is for:</p><ul><li><p><strong>A1. events arrive within 24 hours:</strong> the quarantine count and the distribution of delays. Any quarantined event alerts the pipeline owner, who reconciles with the source team and decides whether to reopen the day.</p></li><li><p><strong>P5&#8217;s evidence:</strong> the job&#8217;s runtime against the benchmark. A run still going at 06:45 is an early warning, not yet a breach.</p></li><li><p><strong>Not proven, the scheduler:</strong> if no run has started by 06:15, alert.</p></li><li><p><strong>Not proven, the ledger:</strong> from D+4, a daily reconciliation against the finance ledger. Any difference goes to the pipeline owner and to Anna.</p></li><li><p><strong>The definition itself:</strong> the weekly share of days published by 07:00, against the 90% in step 0, and by 08:00 against 99%.</p></li></ul><p>As stated in the earlier post, every monitoring artefact should have: an owner, a publishing channel, and an action (this clarifies what to do in case of an alert and who does it).</p><blockquote><p><strong>Definition of done</strong>: assumptions that cannot be proven or that must be monitored anyway have a monitor, or an explicitly written acceptance of the risk. And every monitor has an owner, a channel and an action.</p></blockquote><h3>Optimise</h3><p>Only now can you parallelise, pre-aggregate, change engines, and all that. But every change must preserve the properties. As Dijkstra wrote (in his essay <em>How do we tell truths that might hurt?</em>) &#8220;Simplicity is prerequisite for reliability,&#8221; it&#8217;s also a prerequisite for optimisation.</p><h1>Conclusion</h1><p><strong>Is this too much to ask?</strong> This is a real question, not a statement. When I started this series, I worried that some of it may be too much. But look at the simplest version of this: a falsifiable statement, a few assumptions, a few lines of argument, a parser and a contract. That &#8220;should&#8221; be a few hours of writing, which would prevent days of fixing or worse. The expensive parts (like using a proof by induction, using solvers or statistics) are reserved for the properties that deserve that time.</p><p>Now that we use assistance from LLMs to write much of our code, this discipline matters even more. The definitions, the assumptions and arguments are good things to work on without help from any AI assistant. Writing code has become faster, let&#8217;s try to keep a certain base level of understanding.</p><p>So, honestly, if you are a data or ML engineer, do you think this is too much?</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/p/a-recipe-for-correct-data-systems/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:&quot;button-wrapper&quot;}" data-component-name="ButtonCreateButton"><a class="button primary button-wrapper" href="https://blog.ismail.bio/p/a-recipe-for-correct-data-systems/comments"><span>Leave a comment</span></a></p><p><a href="https://blog.ismail.bio/publish/post/215637494">Next post</a> uses this same pipeline but will focus on the arguments themselves: how hard to argue for each property, with example proofs and using a solver and an exhaustive check.</p><p></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.ismail.bio/subscribe?"><span>Subscribe now</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Towards a data discipline]]></title><description><![CDATA[A practical manifesto to build data systems that prevent errors by design]]></description><link>https://blog.ismail.bio/p/towards-a-data-discipline</link><guid isPermaLink="false">https://blog.ismail.bio/p/towards-a-data-discipline</guid><dc:creator><![CDATA[Ismail Elouafiq]]></dc:creator><pubDate>Sat, 24 Oct 2026 11:59:39 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/dfd07db1-05c5-42d1-949b-638def48b7e3_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let me begin with a small observation,<br>and if you are involved in the data profession, you are painfully aware of this:</p><p><strong>Scenario:</strong><em> </em>You ask about the data quality and someone shows you a series of tests, monitoring dashboards, and metrics that are supposed to prove that everything works as it should.</p><p>Here&#8217;s the problem: <em>testing, at best, can reveal the presence of errors, it can never show their absence.<strong> </strong></em>Dijkstra warned against this years ago, and this same intellectual shortcut has grown tremendously in the data profession. Especially where anything close to a formal proof is hard, and sometimes impossible.</p><p>Today, we build data pipelines and machine learning models in an ad hoc experimental way. We add some garnish (infrastructure as code, basic tests and checks) and ship them to production. Detection is cheaper and more convenient than prevention, and whenever we are given the choice and are under a hard deadline, it is easy to prioritise the instruments and skip the discipline. This is not an argument against testing but rather where it belongs.</p><blockquote><p><strong>What we are really lacking is a disciplined way to</strong> <strong>prevent errors from happening in the first place: <span>a way of making certain errors impossible </span></strong><em><strong>by design.</strong></em><br><strong>Ignoring this is similar to ignoring gravity when designing an airplane.</strong></p></blockquote><p>This series is an attempt at such a discipline, to bring back the engineering in data engineering, the science in data science, and to show that data systems can be made with a prevention-first approach.</p><p>What I propose here is an approach that aims to ensure a data system is <mark data-color="#d9ead3" style="background-color: rgb(217, 234, 211); color: rgb(0, 0, 0);">correct by design</mark>. This is part one of the series:</p><ul><li><p>This part explains the main principles.</p></li><li><p>The <a href="https://blog.ismail.bio/p/a-recipe-for-correct-data-systems">second part gives a step by step recipe</a> to follow.</p></li><li><p>Finally, the <a href="https://blog.ismail.bio/p/how-to-use-formal-methods-in-data">last part</a> will focus more on: formal arguments to prove what we are building is correct.</p></li></ul><p>I expect many people will disagree with this. Your comments and challenging arguments are welcome.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Part 2 will give a step by step recipe, subscribe to my Experiments newsletter if interested.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><h2>The why and how of correctness by design</h2><p>If you are not convinced this problem is big enough, meditate on this quote: </p><p>&#8220;<em>As long as there were no machines, programming was no problem at all;</em><br><em>when we had a few weak computers, programming became a mild problem,<br>and now we have gigantic computers, <br>programming has become an equally gigantic problem</em>&#8221; <em>The Humble Programmer, </em>E. W. Dijkstra (1972)</p><p>Today this is orders of magnitude larger, and I suspect it is a matter of a few years (if not a few months) before this gets completely out of hand.</p><p>About seventeen years ago, when I took my first machine learning course, one of the notes I took reads something along the lines of: &#8220;<em>deep learning is currently of little practical use, perhaps it may be useful in the future</em>&#8221;. I mention this not to display my poor judgement, which speaks for itself, but as a reminder of the speed at which this field has grown. Problems that, back then, were not even considered possible to solve are today expected to reach production in one or two months. There is a gap that is growing even faster now with how we use language models to write code.</p><p><strong>Our capacity to build has grown dramatically, our capacity to understand what we build has stayed about the same.</strong></p><p>That is why we need a prevention-first discipline. The aim is not to guarantee every module is correct. It is to avoid as many errors as possible by construction.</p><p>The framework I will suggest here has three steps: <strong>1 Prove &#8594;</strong> <strong>2 Check &#8594;</strong> <strong>3</strong> <strong>Monitor</strong></p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BItq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faadc73fa-89e6-459f-b19d-b9bc601889be_690x420.png 424w, https://substackcdn.com/image/fetch/$s_!BItq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faadc73fa-89e6-459f-b19d-b9bc601889be_690x420.png 848w, https://substackcdn.com/image/fetch/$s_!BItq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faadc73fa-89e6-459f-b19d-b9bc601889be_690x420.png 1272w, https://substackcdn.com/image/fetch/$s_!BItq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faadc73fa-89e6-459f-b19d-b9bc601889be_690x420.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!BItq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faadc73fa-89e6-459f-b19d-b9bc601889be_690x420.png" width="280" height="170.43478260869566" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/aadc73fa-89e6-459f-b19d-b9bc601889be_690x420.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:420,&quot;width&quot;:690,&quot;resizeWidth&quot;:280,&quot;bytes&quot;:39440,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.ismail.bio/i/208690946?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faadc73fa-89e6-459f-b19d-b9bc601889be_690x420.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!BItq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faadc73fa-89e6-459f-b19d-b9bc601889be_690x420.png 424w, https://substackcdn.com/image/fetch/$s_!BItq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faadc73fa-89e6-459f-b19d-b9bc601889be_690x420.png 848w, https://substackcdn.com/image/fetch/$s_!BItq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faadc73fa-89e6-459f-b19d-b9bc601889be_690x420.png 1272w, https://substackcdn.com/image/fetch/$s_!BItq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faadc73fa-89e6-459f-b19d-b9bc601889be_690x420.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>Skipping any step in this ladder is a choice that should be explicitly made rather than being implicit.</p><p>But before we explain this, it makes no sense to talk about &#8220;<em>proving a system is correct</em>&#8221; unless we define what it means for such a system to be &#8220;<em>correct</em>&#8221; in the first place.</p><p>So all of the steps above depend on having a proper definition. How can we define what correctness means for a data system?</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RS9p!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ecd4fc8-9da5-41c9-a941-7a9ce06fff61_1038x584.png 424w, https://substackcdn.com/image/fetch/$s_!RS9p!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ecd4fc8-9da5-41c9-a941-7a9ce06fff61_1038x584.png 848w, https://substackcdn.com/image/fetch/$s_!RS9p!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ecd4fc8-9da5-41c9-a941-7a9ce06fff61_1038x584.png 1272w, https://substackcdn.com/image/fetch/$s_!RS9p!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ecd4fc8-9da5-41c9-a941-7a9ce06fff61_1038x584.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!RS9p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ecd4fc8-9da5-41c9-a941-7a9ce06fff61_1038x584.png" width="411" height="231.23699421965318" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2ecd4fc8-9da5-41c9-a941-7a9ce06fff61_1038x584.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:584,&quot;width&quot;:1038,&quot;resizeWidth&quot;:411,&quot;bytes&quot;:62262,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.ismail.bio/i/208690946?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ecd4fc8-9da5-41c9-a941-7a9ce06fff61_1038x584.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!RS9p!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ecd4fc8-9da5-41c9-a941-7a9ce06fff61_1038x584.png 424w, https://substackcdn.com/image/fetch/$s_!RS9p!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ecd4fc8-9da5-41c9-a941-7a9ce06fff61_1038x584.png 848w, https://substackcdn.com/image/fetch/$s_!RS9p!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ecd4fc8-9da5-41c9-a941-7a9ce06fff61_1038x584.png 1272w, https://substackcdn.com/image/fetch/$s_!RS9p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2ecd4fc8-9da5-41c9-a941-7a9ce06fff61_1038x584.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div></figure></div><h2>First, what does &#8220;correct&#8221; mean</h2><p>There is no reason to prove a system is correct if you cannot define what correct means. This section is about <em>defining what the system does in a way that is falsifiable</em>.</p><p>Most projects have a generalised statement of what they should achieve: &#8220;Make our data more up to date&#8221;, &#8220;Improve the data quality of X&#8221; or &#8220;Make our data products discoverable&#8221;. The issue with these generalised statements is that there is no way of ensuring we really achieved them other than a subjective feeling. </p><p>Just like saying &#8220;Cleanse the energy of the data&#8221; these generalised statements mean different things to different people. And, when we claim to have achieved the goal, what it meant for the person requesting it may be very different from what we have built. A falsifiable statement instead is clearly defined and avoids this problem. For example: <em>at least 99% of daily datasets are available by 07:00, compared with the current 92%, for 8 consecutive weeks. </em>Those proficient in the data jargon will state that this is just an SLO (service level objective), it does overlap with it but the goal here is to use this statement as the base that everything else is derived from.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-204!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60eb27ad-dee0-4bc3-88dd-393570d2b457_1634x424.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-204!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60eb27ad-dee0-4bc3-88dd-393570d2b457_1634x424.png 424w, https://substackcdn.com/image/fetch/$s_!-204!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60eb27ad-dee0-4bc3-88dd-393570d2b457_1634x424.png 848w, https://substackcdn.com/image/fetch/$s_!-204!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60eb27ad-dee0-4bc3-88dd-393570d2b457_1634x424.png 1272w, https://substackcdn.com/image/fetch/$s_!-204!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60eb27ad-dee0-4bc3-88dd-393570d2b457_1634x424.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!-204!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60eb27ad-dee0-4bc3-88dd-393570d2b457_1634x424.png" width="1456" height="378" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/60eb27ad-dee0-4bc3-88dd-393570d2b457_1634x424.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:378,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:69237,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.ismail.bio/i/208690946?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60eb27ad-dee0-4bc3-88dd-393570d2b457_1634x424.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!-204!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60eb27ad-dee0-4bc3-88dd-393570d2b457_1634x424.png 424w, https://substackcdn.com/image/fetch/$s_!-204!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60eb27ad-dee0-4bc3-88dd-393570d2b457_1634x424.png 848w, https://substackcdn.com/image/fetch/$s_!-204!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60eb27ad-dee0-4bc3-88dd-393570d2b457_1634x424.png 1272w, https://substackcdn.com/image/fetch/$s_!-204!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60eb27ad-dee0-4bc3-88dd-393570d2b457_1634x424.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>One way to define this is to think about:</p><ul><li><p>What is measured</p></li><li><p>What is the current state (the baseline we&#8217;re comparing with)</p></li><li><p>What is a measurable improvement (for the project to be called a success)</p></li><li><p>Time periods: how long until we know the measurable improvement</p></li><li><p>Important constraints and limitations</p></li></ul><p>We don&#8217;t always need all five, the main thing we need is for the statement to be, you guessed it, &#8220;falsifiable&#8221;. This gives us a clear definition of what it means for what we built to be &#8220;correct&#8221;.</p><p>Most of the time, teams resist writing such falsifiable statements because of two reasons. <em>The first is that</em> <em>they don&#8217;t have time:</em> this usually means that delivering fast is more important than delivering the right thing. Sometimes that trade-off is worth it. But it should just be clear that it is a choice, made explicitly and not just happening by default.</p><p><em>The second is that the problem is hard to understand and it is unclear what to measure:</em> this just means the problem is not understood well enough, which only means the experimentation step is not finished.</p><p><strong>You don&#8217;t need a falsifiable statement at the start. </strong>Often, you need to experiment with ad hoc code before you actually understand the problem, to go back and forth with the people involved. This step usually helps you get a true understanding of what the problem requires, how it can be solved and what can be measured. The experimentation step is necessary, because it helps us understand what is involved in the problem. The experimentation should not be restrictive, which means it allows for the kind of creativity that is needed to solve certain problems.</p><p>Sometimes measuring is hard, other times impossible, and for a few problems it can be misleading. And when a measure becomes the target, it ceases to be a good measure (Goodhart&#8217;s law, in Marilyn Strathern&#8217;s phrasing). None of it is a reason to skip the definition step. Because at the end of the day you will be making either implicit or explicit measures. It&#8217;s the choice between defining, and going with the vibe.</p><blockquote><p>In summary,<br>Whether you like it or not, <strong>you are making a hypothesis. So make sure it is clear and explicit, </strong>and define your project&#8217;s goal as a falsifiable statement.</p></blockquote><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/p/towards-a-data-discipline?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Has this been helpful so far? Share it with someone you think may benefit</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/p/towards-a-data-discipline?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.ismail.bio/p/towards-a-data-discipline?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><h2>The prevention-first discipline: prove, check, monitor</h2><p>Testing and monitoring are necessary. But given that these can only ever demonstrate the presence of errors, the better question is <strong>how can we</strong> <strong>make some errors structurally impossible</strong> before we even write code?</p><p><strong>I will argue that we need to take a more formal approach: to borrow, as far as is practical, from formal mathematical reasoning.</strong> Explicit reasoning about correctness has been almost entirely absent from mainstream data engineering (despite the fact that this field rests on formal foundations such as relational algebra and type systems).<strong> </strong>Adjacent fields like compilers, databases, and distributed systems treat formal reasoning as a first-class tool. But data and machine learning pipelines largely don&#8217;t.</p><p>There are however a few reasonable objections to this. The first is that Data and ML pipelines cannot be fully proven, some parts depend on things we don&#8217;t control: upstream sources, the stochastic nature of models, data/concept shifts and drifts to name a few. And the second is that a proof is just as good as the assumptions we make at the start. And the assumptions we make in the data field are bound to change. </p><p>So I am <strong>not</strong> proposing we formally prove the entirety of a pipeline, what I&#8217;m suggesting here is a middle way.</p><p>This middle way looks at <strong>correctness as a ladder</strong>, where we are clear about which tradeoffs we are making when we skip an important step. As we&#8217;ve defined it above, this 3-step ladder depends on the problem at hand. And once again, skipping any one step in this ladder is a choice that should be explicitly made rather than being implicit:</p><blockquote><p><strong>Prove what is provable &#8594; Check at every boundary &#8594; Monitor the rest</strong></p></blockquote><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!s31q!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da9e330-8dd6-4284-9a8a-4eb3fc90b188_2136x444.png 424w, https://substackcdn.com/image/fetch/$s_!s31q!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da9e330-8dd6-4284-9a8a-4eb3fc90b188_2136x444.png 848w, https://substackcdn.com/image/fetch/$s_!s31q!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da9e330-8dd6-4284-9a8a-4eb3fc90b188_2136x444.png 1272w, https://substackcdn.com/image/fetch/$s_!s31q!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da9e330-8dd6-4284-9a8a-4eb3fc90b188_2136x444.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!s31q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da9e330-8dd6-4284-9a8a-4eb3fc90b188_2136x444.png" width="1456" height="303" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2da9e330-8dd6-4284-9a8a-4eb3fc90b188_2136x444.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:303,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:111984,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.ismail.bio/i/208690946?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da9e330-8dd6-4284-9a8a-4eb3fc90b188_2136x444.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!s31q!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da9e330-8dd6-4284-9a8a-4eb3fc90b188_2136x444.png 424w, https://substackcdn.com/image/fetch/$s_!s31q!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da9e330-8dd6-4284-9a8a-4eb3fc90b188_2136x444.png 848w, https://substackcdn.com/image/fetch/$s_!s31q!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da9e330-8dd6-4284-9a8a-4eb3fc90b188_2136x444.png 1272w, https://substackcdn.com/image/fetch/$s_!s31q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2da9e330-8dd6-4284-9a8a-4eb3fc90b188_2136x444.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div></figure></div><h3>Step 1 - Prove what is provable</h3><p>A <em>proof is an argument that a statement is true for every possible input</em> (not just the inputs you tried). The <a href="https://blog.ismail.bio/p/a-recipe-for-correct-data-systems">next article here</a> describes what to prove and how to prove it, for now let&#8217;s just define what we mean by &#8220;prove&#8221;. </p><blockquote><p>In slightly more formal words: <br>Given a system defined by a function <em>f</em>, that accepts a defined set of inputs <em>I</em><br>- under a defined precondition <em>P </em>over its inputs, <br>- and a postcondition <em>Q</em> over the input/output pairs.<br>Proving is establishing that </p><p style="text-align: center;"><em>&#8704; x &#8712; I; P(x) =&gt; Q(x, f(x))</em></p><p>In other words, that for <strong>every possible input, if the precondition is satisfied by the input, the postcondition is satisfied (by the input/output pair)</strong>.</p></blockquote><p>Testing instead of proving is equivalent to checking <em>Q(x<sub>i</sub>, f(x<sub>i</sub>)) </em>is true for a finite set of chosen inputs <em>x<sub>i</sub></em> that satisfy <em>P</em>.</p><p>The formal method crowd will recognise Hoare&#8217;s triple here, but the goal is not to get too formal. Of all the steps, this one invites the most criticism. Engineers will usually argue that proving would take too long, whereas mathematicians and theorists will usually argue that proving is worth the tradeoff. The reason we&#8217;re taking a middle way here is that we do not want to tradeoff time either. </p><p>Let me just explain the reasoning here:<br>We have to see this as a spectrum that goes from informal types of reasoning (like an intuitive explanation in a markdown documentation) to the more rigorous formal methods (such as using machine checkable proofs).</p><p>And the good news, in my experience, is that <strong>the cheaper methods find most of the errors, in other words: just spending time reasoning about the problem and writing an informal argument usually catches the majority of potential errors</strong>. Writing down in plain words why a job should be deterministic is often enough to find out it isn&#8217;t.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2DjO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2DjO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 424w, https://substackcdn.com/image/fetch/$s_!2DjO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 848w, https://substackcdn.com/image/fetch/$s_!2DjO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 1272w, https://substackcdn.com/image/fetch/$s_!2DjO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!2DjO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png" width="1456" height="413" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:413,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:356349,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.ismail.bio/i/208690946?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2DjO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 424w, https://substackcdn.com/image/fetch/$s_!2DjO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 848w, https://substackcdn.com/image/fetch/$s_!2DjO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 1272w, https://substackcdn.com/image/fetch/$s_!2DjO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2c03e4ab-24db-4399-927f-da979d709fd7_2200x624.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">A spectrum of formal reasoning</figcaption></figure></div><p>In between those ends, we find types that make invalid states unrepresentable, arguments by induction over partitions etc. (more details in <a href="https://blog.ismail.bio/p/a-recipe-for-correct-data-systems">the next article</a>).</p><p>If we breakdown the system we are building into multiple components. We want anyone reading the design to see where every component is and why it is there.</p><p>In <a href="https://blog.ismail.bio/p/a-recipe-for-correct-data-systems">the next article</a>, I will go into details about:</p><ul><li><p>What properties you should prove (such as <em>idempotence, determinism etc.</em>).</p></li><li><p>What arguments you can use for different scenarios.</p></li></ul><p><strong>Where testing fits here:</strong> tests can help us make the argument stay more up to date with the implementation. The proof holds for the design, but the code may still drift from it. Property-based testing comes handy here, with libraries such as Hypothesis in Python. The property under test is the proof&#8217;s own statement: if P(x), then Q(x, f(x)), run on thousands of generated inputs that satisfy P. The test still only samples, but it samples something different: not the world, but whether the code faithfully follows an argument that already covers every input.</p><blockquote><p>In summary,</p><p>Before writing code, and before writing tests, we should try to argue for the most important properties, the recipe in <a href="https://blog.ismail.bio/p/a-recipe-for-correct-data-systems">the next post</a> gives an example of this, followed by the final post that goes into more details on using formal arguments.</p></blockquote><h3>Step 2 - Check at every boundary</h3><p>None of the proving we did earlier ensures that the input satisfies the precondition. That is where checking comes. The moment an input reaches our system, we must check that it satisfies the preconditions we depend on, otherwise we reject it.</p><p>A <em>boundary</em> here is anywhere data is handed over: where it arrives from an upstream source we don&#8217;t own, where it passes from one module to another, where it is written to a table other people read, where it is fed to a model, served to a user etc.</p><p><strong>A check is not a test: </strong>the two are easy to confuse. A test runs before deployment, on inputs we chose. A check runs in the deployment environment, and includes real inputs. A test asks: does my code behave on the examples I can think of? A test samples the domain. A check attempts to guard this domain instead.</p><p><strong>This is in line with designing by contract,<br></strong>a concept introduced by Bertrand Meyer in the mid-eighties. The current work on data contracts (popularised by Andrew Jones and Chad Sanderson) applies this idea to data, and is very useful. But a contract should say much more. A contract that says &#8220;quantity is an integer&#8221; is a very weak contract, it&#8217;s a simple type declaration.</p><p>A contract in Meyer&#8217;s terms binds two parties: the supplier guarantees the postcondition only if the client satisfies the precondition. A data system thus acts as a client of the upstream sources, and a supplier for the downstream users of the system. And as such it needs to define the contracts at both boundaries.</p><p>A useful contract has to define:</p><ul><li><p><em>Structure</em>: fields, types, nullability, keys.</p></li><li><p><em>Meaning</em>: ranges, units, allowed values, uniqueness, and the relationships between fields and between tables (for example: every order refers to a customer that exists, a refund never exceeds the original payment).</p></li><li><p><em>Delivery</em>: <em>when</em> the data should arrive, <em>how much</em> of it we expect, and <em>how complete</em> it should be.</p></li></ul><p>All the above should be applied to: the preconditions (write guarantees: rules that a data producer or client must satisfy before writing/streaming),<br>the postconditions (read guarantees: promises a data system or processing function makes upon completing a query or transformation, e.g., output dataset row counts match expected aggregates, null-free columns),<br>and the invariants (think here about the consistency of storage and states: rules at the level of the system itself, that must always remain true at rest or between mutations, these are things such as foreign key integrity, partition balance, chronological event ordering etc.)</p><p>Two principles complete the checking step:</p><ol><li><p><em>Parse don&#8217;t validate</em>: popularised by<strong> </strong>Alexis King in her blog post &#8220;<a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/">parse, don&#8217;t validate</a>&#8221; is about ensuring we cannot represent invalid states through parsing. Once parsed, the precondition is satisfied.</p></li><li><p><em>Reject rather than repair: </em>as much as you can, if a check fails, reject the record. Fixing silently may end up hiding errors.</p></li></ol><blockquote><p>In summary,</p><p>A proof is only as good as its preconditions, a check ensures those preconditions hold in practice. Check every boundary, reject rather than repair (not always but as much as possible), parse don&#8217;t validate.</p></blockquote><h3>Step 3 - Monitor what is outside of your control</h3><p>Some things will be outside of your control. Data drift, sources that change meaning with no notification, the stochastic nature of ML components whose behaviour is best described by black magic etc.</p><p>The proofs and checks rest on assumptions. Monitoring is simply about watching those assumptions. My criticism of monitoring is only because a lot of the time we make it the whole strategy. If you have gone through the previous steps, then you know by now what to monitor and alert for.</p><p><strong>Monitoring what you cannot prevent is a necessity.<br>Monitoring what you could prevent is laziness, manifested as a dashboard.</strong></p><p><strong>On ownership in monitoring: </strong>monitoring and alerting should always have clear ownership. We have to be clear on <em>who</em> will take action, <em>when </em>they must take action and <em>how </em>they will know (for example: an alert email or so, or if they routinely check a dashboard every Tuesday morning), and <em>what </em>the action is. Setting a dashboard that nobody looks at is as good as doing nothing.</p><p><strong>Working backwards from monitoring: </strong>when we find issues through monitoring, it is the opportunity to add checks and to revise the architecture/design of the system.</p><blockquote><p>In summary,</p><p>Derive what you must monitor from your assumptions. Give every monitoring modality or alert an owner, and treat every recurring alert as an invitation to move that error back up the ladder, to prevent it from happening in the first place.</p></blockquote><h2>None of this is new</h2><p>Nothing in this series is new. If anything, this argument has existed for decades, but the data professions seem to ignore most of this work. Proofs, contracts and checks are decades old.</p><p>So why has the data and machine learning world not taken them in (for the most part)? Not because they don&#8217;t apply, but because we are able to get away without them, until the day we&#8217;re not. Data errors rarely crash, they rather mislead, quietly. And when they do crash they create a new problem that we build another system to solve. But perhaps also because the data and machine learning fields have a lot of stochastic components built in that we use as an excuse. Add to that the speed at which we&#8217;re trying to build things today to &#8220;keep up&#8221;. That is exactly why we need the discipline more, not less. The theory is there, and so are the tools. What is missing is the habit.</p><p>I am in no way claiming:</p><ul><li><p>That every data pipeline should be formally proven.</p></li><li><p>That tests and monitoring are useless. They are just not preventive, and proofs do not replace them.</p></li><li><p>That this is faster. It costs some more design time up front. Which I believe is much cheaper in the long term, but I have no study to prove it. So do see this as a hypothesis, and if you have any evidence in either direction, I would love to hear from you.</p></li></ul><p>In <a href="https://blog.ismail.bio/p/a-recipe-for-correct-data-systems">the next article</a> we will go through a more detailed step by step recipe for building data systems that prevent errors, using a worked example.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/p/towards-a-data-discipline/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.ismail.bio/p/towards-a-data-discipline/comments"><span>Leave a comment</span></a></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.ismail.bio/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item><item><title><![CDATA[Make your data pipeline less chaotic by one versioned definition of data]]></title><description><![CDATA[Data collection at Sweden&#8217;s Television]]></description><link>https://blog.ismail.bio/p/make-your-data-pipeline-less-chaotic</link><guid isPermaLink="false">https://blog.ismail.bio/p/make-your-data-pipeline-less-chaotic</guid><dc:creator><![CDATA[Ismail Elouafiq]]></dc:creator><pubDate>Tue, 06 Jan 2026 13:56:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!TG2c!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad887d66-8128-40fd-b5f3-cb7f39146739_1024x502.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TG2c!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad887d66-8128-40fd-b5f3-cb7f39146739_1024x502.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TG2c!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad887d66-8128-40fd-b5f3-cb7f39146739_1024x502.png 424w, https://substackcdn.com/image/fetch/$s_!TG2c!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad887d66-8128-40fd-b5f3-cb7f39146739_1024x502.png 848w, https://substackcdn.com/image/fetch/$s_!TG2c!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad887d66-8128-40fd-b5f3-cb7f39146739_1024x502.png 1272w, https://substackcdn.com/image/fetch/$s_!TG2c!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad887d66-8128-40fd-b5f3-cb7f39146739_1024x502.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TG2c!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad887d66-8128-40fd-b5f3-cb7f39146739_1024x502.png" width="1024" height="502" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ad887d66-8128-40fd-b5f3-cb7f39146739_1024x502.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:502,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!TG2c!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad887d66-8128-40fd-b5f3-cb7f39146739_1024x502.png 424w, https://substackcdn.com/image/fetch/$s_!TG2c!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad887d66-8128-40fd-b5f3-cb7f39146739_1024x502.png 848w, https://substackcdn.com/image/fetch/$s_!TG2c!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad887d66-8128-40fd-b5f3-cb7f39146739_1024x502.png 1272w, https://substackcdn.com/image/fetch/$s_!TG2c!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fad887d66-8128-40fd-b5f3-cb7f39146739_1024x502.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>How we use a central protobuf schema to make our data pipeline less&nbsp;chaotic</h4><p>If I told you one of the most popular TV Shows in Sweden is about following the moose as they travel, for 21 days, you might say:<br>&#8202;&#8212;<em>&#8202;Stop it with the stereotypes! <br></em>or&nbsp;:<br>&#8212; <em>Ismail, go home and stop experimenting with psychedelics</em></p><p>I promise, this TV show is the real deal and it&#8217;s only 21 days&nbsp;long.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1gDF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddecc4e9-485d-484e-8ce6-f4703386b596_1024x359.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1gDF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddecc4e9-485d-484e-8ce6-f4703386b596_1024x359.png 424w, https://substackcdn.com/image/fetch/$s_!1gDF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddecc4e9-485d-484e-8ce6-f4703386b596_1024x359.png 848w, https://substackcdn.com/image/fetch/$s_!1gDF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddecc4e9-485d-484e-8ce6-f4703386b596_1024x359.png 1272w, https://substackcdn.com/image/fetch/$s_!1gDF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddecc4e9-485d-484e-8ce6-f4703386b596_1024x359.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1gDF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddecc4e9-485d-484e-8ce6-f4703386b596_1024x359.png" width="1024" height="359" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ddecc4e9-485d-484e-8ce6-f4703386b596_1024x359.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:359,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!1gDF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddecc4e9-485d-484e-8ce6-f4703386b596_1024x359.png 424w, https://substackcdn.com/image/fetch/$s_!1gDF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddecc4e9-485d-484e-8ce6-f4703386b596_1024x359.png 848w, https://substackcdn.com/image/fetch/$s_!1gDF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddecc4e9-485d-484e-8ce6-f4703386b596_1024x359.png 1272w, https://substackcdn.com/image/fetch/$s_!1gDF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fddecc4e9-485d-484e-8ce6-f4703386b596_1024x359.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Day 5 of the moose walk (&#8220;den stora &#228;lg vandringen&#8221;) and the moose are indeed&nbsp;walking</figcaption></figure></div><p>The TV Show &#8220;<em>den stora &#228;lg vandringen&#8221; </em>(which translates to<em> &#8220;the great moose walk&#8221;</em>)<em> </em>is a type of TV Show known as <em>slow TV. </em>This trend started in the neighboring lovely country of Norway (at&nbsp;NRK).</p><p>This TV show is 21 days long and it was streamed live on SVTs (Swedish Television&#8217;s) streaming service SVT&nbsp;Play.</p><p><em><strong>So why collect data then?<br></strong></em> It turns out filming the moose is not the only thing that SVT does and <em>SVT Play</em> is just one of the services at SVT. These services range from streaming to delivering news in addition to games and services for children and more. The point is, those apps make different types of content available (news articles, movies, TV series etc.) on different platforms including web, iOS, Android and smart&nbsp;TVs.</p><p>So a little more than two years ago, a project for data collection at SVT got started. This was an effort to aid decision making in the multiple products of SVT (that used to rely on third-party services such as google analytics).</p><p>Of course, the data is not what makes or breaks the products, nor the shows, movies or news articles. It is there to verify assumptions, to understand the users better and to support the decision&nbsp;making.</p><p>As one can see, the diversity of SVTs services and the platforms on which they run adds multiple challenges to the collection of&nbsp;data.</p><h3><strong>Too long; didn&#8217;t&nbsp;read?</strong></h3><p><em><a href="https://www.youtube.com/watch?v=ZouUbWt5tug">If you prefer watching this as a video feel free to head over&nbsp;here</a></em><a href="https://www.youtube.com/watch?v=ZouUbWt5tug">.</a></p><blockquote><p>While there are many things to talk about when it comes to the collection and analysis of data, this post will focus on: <br>&#8212; A very brief <strong>intro to how we collect data</strong> at SVT.<br>&#8212; The chaos that can result from the changes in the business rules.<br>&#8212;How we use a <strong>central schema</strong> (with Protobuf) <strong>to define the data we collect and propagate this definition</strong> in the whole pipeline (from collection to storage).<br>&#8202;&#8212;&#8202;Some of <strong>the issues we encountered</strong> with that&nbsp;choice.</p></blockquote><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption"><em>Experiments</em> is a reader-supported publication. To receive new posts and support my work, consider becoming a subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h3><strong>A Brief on how we collect data at&nbsp;SVT</strong></h3><p>Let&#8217;s say you&#8217;re a news editor, and you want to see how your articles perform. How can we collect data about&nbsp;that?</p><p>For example, you would want to know: <em>how many users read an article?</em> We cannot know for sure if a user actually reads the article, but what we can do is estimate it from what we can collect such as: a <em>mouse click</em> on the article or a <em>mouse scroll</em> inside the&nbsp;article.</p><p>These things that we can collect are what we call &#8220;<em><strong>Events&#8221;</strong>.</em> Other example events for other products can&nbsp;be:</p><ul><li><p>A <em>video start</em> or a <em>video pause </em>in the video&nbsp;player.</p></li><li><p>A <em>search query </em>in a search&nbsp;bar.</p></li><li><p>Or other events for collecting video player telemetry information.</p></li></ul><blockquote><p>An <strong>event </strong>is an action that happened on the client application side, for which the corresponding information can be collected (for example: a click or a video&nbsp;start)</p></blockquote><p>These events are just observations, and from them we should be able to either calculate or approximate our metrics (an example of a metric that cannot be calculated exactly is the <em>percentage watched of a video</em> because we simply do not know if the user is actually watching)</p><p>So we send those events. We ingest them into our pipeline (more details later). We store them in a table. We process and analyze them and then we present them to the users of our solution.</p><p>So the event will go&nbsp;through:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dM_e!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2063015-e9e6-480e-8ab7-5abcaf3b20b5_1024x280.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dM_e!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2063015-e9e6-480e-8ab7-5abcaf3b20b5_1024x280.png 424w, https://substackcdn.com/image/fetch/$s_!dM_e!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2063015-e9e6-480e-8ab7-5abcaf3b20b5_1024x280.png 848w, https://substackcdn.com/image/fetch/$s_!dM_e!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2063015-e9e6-480e-8ab7-5abcaf3b20b5_1024x280.png 1272w, https://substackcdn.com/image/fetch/$s_!dM_e!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2063015-e9e6-480e-8ab7-5abcaf3b20b5_1024x280.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!dM_e!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2063015-e9e6-480e-8ab7-5abcaf3b20b5_1024x280.png" width="1024" height="280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a2063015-e9e6-480e-8ab7-5abcaf3b20b5_1024x280.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:280,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!dM_e!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2063015-e9e6-480e-8ab7-5abcaf3b20b5_1024x280.png 424w, https://substackcdn.com/image/fetch/$s_!dM_e!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2063015-e9e6-480e-8ab7-5abcaf3b20b5_1024x280.png 848w, https://substackcdn.com/image/fetch/$s_!dM_e!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2063015-e9e6-480e-8ab7-5abcaf3b20b5_1024x280.png 1272w, https://substackcdn.com/image/fetch/$s_!dM_e!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2063015-e9e6-480e-8ab7-5abcaf3b20b5_1024x280.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ul><li><p><em><strong>collection:</strong></em> on the user-facing application side. For this part, in our case, we built SDKs (mainly for iOS, Android, Web and smart TVs) that will collect events such as clicks and send them to an API, which brings us to the next&nbsp;part.</p></li><li><p><em><strong>ingestion:</strong></em> when the API gets the previous event it publishes it to a message queue. In our case, we are currently using <a href="https://cloud.google.com/pubsub/docs/overview">Pub/Sub on the google cloud platform</a> (GCP). We have creatively named the previous API: the <em><strong>event&nbsp;API</strong></em>.</p></li><li><p><em><strong>storage:</strong></em> a service gets the previous event from Pub/Sub, checks the validity of its content, then stores it in a table (a BigQuery table in our case). We have called this service <em><strong>judge-judi</strong></em>.</p></li><li><p><em><strong>analysis:</strong></em> mostly just a bunch of&nbsp;ETL</p></li><li><p><em><strong>presentation:</strong></em> at this stage, we can show a dashboard to the right person with the right&nbsp;metric.</p></li></ul><p>For the purpose of this post, we will only focus on the <em>collect&#8202;&#8212;&#8202;ingest&#8202;&#8212;&#8202;store</em> parts of the previous pipeline.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!26Fs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F343a5f89-0b34-42f3-90d1-a8e9c89a12c4_1024x374.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!26Fs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F343a5f89-0b34-42f3-90d1-a8e9c89a12c4_1024x374.png 424w, https://substackcdn.com/image/fetch/$s_!26Fs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F343a5f89-0b34-42f3-90d1-a8e9c89a12c4_1024x374.png 848w, https://substackcdn.com/image/fetch/$s_!26Fs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F343a5f89-0b34-42f3-90d1-a8e9c89a12c4_1024x374.png 1272w, https://substackcdn.com/image/fetch/$s_!26Fs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F343a5f89-0b34-42f3-90d1-a8e9c89a12c4_1024x374.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!26Fs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F343a5f89-0b34-42f3-90d1-a8e9c89a12c4_1024x374.png" width="1024" height="374" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/343a5f89-0b34-42f3-90d1-a8e9c89a12c4_1024x374.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:374,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!26Fs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F343a5f89-0b34-42f3-90d1-a8e9c89a12c4_1024x374.png 424w, https://substackcdn.com/image/fetch/$s_!26Fs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F343a5f89-0b34-42f3-90d1-a8e9c89a12c4_1024x374.png 848w, https://substackcdn.com/image/fetch/$s_!26Fs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F343a5f89-0b34-42f3-90d1-a8e9c89a12c4_1024x374.png 1272w, https://substackcdn.com/image/fetch/$s_!26Fs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F343a5f89-0b34-42f3-90d1-a8e9c89a12c4_1024x374.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>To keep things simple, we can assume that we send one event at a time from the client application (In reality, those events are gathered by the client application and sent together in a sequence of&nbsp;events)</p><p>So, with all this in mind, where can problems&nbsp;happen?</p><h3>The need for ONE (versioned) definition of the event&nbsp;data</h3><p>From what we&#8217;ve talked about in the previous section, here&#8217;s what the life of an event looks like from collection to storage:<br>&#8202;&#8212;&#8202;First, we send the event from an app (through our SDK) to the Event API.<br>&#8202;&#8212;&#8202;The Event API then adds it to the queue for it to be processed later.<br>&#8202;&#8212;&#8202;And then on the right, is an accurate picture of <em>judge-judi</em> (the service described in the ingestion part above) performing its duty of pulling and storing valid events in the&nbsp;table.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Vgah!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd82f508f-26bd-4015-9dc5-f18cc6295ccd_1024x437.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Vgah!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd82f508f-26bd-4015-9dc5-f18cc6295ccd_1024x437.png 424w, https://substackcdn.com/image/fetch/$s_!Vgah!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd82f508f-26bd-4015-9dc5-f18cc6295ccd_1024x437.png 848w, https://substackcdn.com/image/fetch/$s_!Vgah!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd82f508f-26bd-4015-9dc5-f18cc6295ccd_1024x437.png 1272w, https://substackcdn.com/image/fetch/$s_!Vgah!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd82f508f-26bd-4015-9dc5-f18cc6295ccd_1024x437.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!Vgah!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd82f508f-26bd-4015-9dc5-f18cc6295ccd_1024x437.png" width="1024" height="437" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d82f508f-26bd-4015-9dc5-f18cc6295ccd_1024x437.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:437,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!Vgah!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd82f508f-26bd-4015-9dc5-f18cc6295ccd_1024x437.png 424w, https://substackcdn.com/image/fetch/$s_!Vgah!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd82f508f-26bd-4015-9dc5-f18cc6295ccd_1024x437.png 848w, https://substackcdn.com/image/fetch/$s_!Vgah!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd82f508f-26bd-4015-9dc5-f18cc6295ccd_1024x437.png 1272w, https://substackcdn.com/image/fetch/$s_!Vgah!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd82f508f-26bd-4015-9dc5-f18cc6295ccd_1024x437.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">on the right an accurate picture of <strong>judge-judi </strong>performing its&nbsp;duty</figcaption></figure></div><p>The first problem is that all parts of the pipeline, from collection to storage, have to agree on what the definition of an event&nbsp;is.</p><h4><strong>The first problem: consistency</strong></h4><p>Let&#8217;s assume that the event looks something like:</p><pre><code>{
    'user_id':   '00000000',
    'content_type': 'video',
    'event_type': 'video_start',    
}</code></pre><p>Given the event above. What if one of our products ends up sending an event by naming the type <em>eventType </em>instead of <em>event_type?</em></p><pre><code>{
    'user_id':   '00000000',
    'content_type': 'video',
    'eventType': 'video_start',    
}</code></pre><p>When this event reaches the validation stage at <em>judge-judi. </em>The service<em> </em>is expecting a field <em>event_type </em>it will just conclude that the field is not&nbsp;there.</p><p>So the first problem is <em><strong>having all parts of the pipeline agree on the definition of the&nbsp;data</strong></em>.</p><p>We also have a multitude of products in SVT (svtplay, news etc.) and different platforms (web, iOS, Android etc.) it would be hard to make sure that the SDKs are always consistent.</p><h4><strong>The second problem: Murphy&#8217;s law of data engineering</strong></h4><p>Which states that: &#8220;<em>If the data schema can change, the data schema will change</em>.&#8221; Now, Murphy did not necessarily say that. <br>Well, he never actually did, but I&#8217;m sure he would&nbsp;agree.</p><p>The business rules and goals change more often than teenagers&#8217; relationships. We should expect, as a result, that the definition of the event will change. And it will change often. New fields will be added, old fields will be removed, others merged together, and the list goes on and&nbsp;on.</p><p>These modifications put schemas, and therefore the definition of an event, in a state of perpetual change. Each change will require adjustment to the pipeline. <em><strong>We do not want these recurring changes in the definition of the data to break the whole (or part of) the pipeline</strong></em>. Ideally, we would want to preserve backward and forward compatibility whenever possible.</p><h4>And then problems specific to&nbsp;JSON</h4><ul><li><p>The type of a field in JSON is unclear and errors can happen. It would be preferable if we can provide guarantees of what the event structure to be expected&nbsp;is.</p></li><li><p>Because we have different platforms, we are using different programming languages. This means that the JSON body can differ depending on the programming language and libraries used.</p></li><li><p>Long serialization overhead on the Event API&#8217;s side with the use of JSON. Although I currently do not have the &#8220;data&#8221; to prove it, it is easy to see that the schema is sent with every&nbsp;event.</p></li></ul><p>But since we have control over the applications and have control over the schema definition how can we make use of&nbsp;that?</p><h3>The solution: centralizing the definition of an event with&nbsp;Protobuf</h3><p>One way of tackling the issues above is to do the following:</p><ul><li><p>One repository where the definition of an event, which we will call the <em><strong>Event Model</strong></em>, is kept (we just keep it in a repository on&nbsp;Gitlab).</p></li><li><p>This <em>event model </em>is versioned.</p></li><li><p>Every version of the <em>event model</em> is then propagated across all the elements of the pipeline. For example, the web SDK will have access to an event model library and every new version in the event model repository will correspond to a new version in the library. This version should be the same for all platforms.</p></li><li><p>We will preferably use a language-neutral definition for the <em>event model</em>&#8217;s definition that can enable us to serialize an event on the SDKs and then deserialize it on the side of <em>judge-judi</em>.</p></li><li><p>Preferably adding or removing fields from the <em>event model </em>should not break backward/forward compatibility.</p></li></ul><p>To make a choice we tried both Avro and Protobuf. While there are many choices when it comes to defining the schema, to have a definition that supports all of the languages and provides a &#8220;good enough&#8221; serialization: Protobuf was the tool of&nbsp;choice.</p><h4><em><strong>What is protobuf and how does it&nbsp;work?</strong></em></h4><p>Protocol buffers are a language-neutral, platform-neutral, extensible way of serializing structured data. At least that&#8217;s what they&#8217;re supposed to&nbsp;be.</p><p>We will not go too deeply in how protocol buffers work but here are the&nbsp;basics.</p><p>Let&#8217;s say Alice wants to send a letter message to Bob that would look like this in&nbsp;JSON:</p><pre><code>{
  'firstName': 'Alice',
  'lastName': 'Alice',
  'email': 'alice@alice.alice',
  'content': 'Hi Bob, it's Alice'
}</code></pre><p>To do so, in protobuf, Alice and Bob would have to agree on a message schema. This schema can look something like&nbsp;this:</p><pre><code>message Letter{
  required string firstName = 1;
  required string lastName = 2;
  optional string email = 3;
  required string content = 4;
}</code></pre><p>This letter schema will be stored in a&nbsp;<em>.proto</em>&nbsp;file.</p><p>Alice and Bob can then convert this message to C++ or Javascript code or whatever language they&#8217;re into using the proto compiler.</p><p>Using the generated code Alice will serialize the message. For example, if Alice is using C++ her code might look something like&nbsp;this:</p><pre><code>Letter letter;
letter.set_first_name("Alice");</code></pre><p>Alice can now send the letter to Bob. A series of bytes are sent over the network. Bob knows the Letter schema, Bob can deserialize the letter&nbsp;message.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cv-K!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2773bc1-760d-49ee-a213-38dd90577bdc_1024x296.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cv-K!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2773bc1-760d-49ee-a213-38dd90577bdc_1024x296.png 424w, https://substackcdn.com/image/fetch/$s_!cv-K!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2773bc1-760d-49ee-a213-38dd90577bdc_1024x296.png 848w, https://substackcdn.com/image/fetch/$s_!cv-K!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2773bc1-760d-49ee-a213-38dd90577bdc_1024x296.png 1272w, https://substackcdn.com/image/fetch/$s_!cv-K!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2773bc1-760d-49ee-a213-38dd90577bdc_1024x296.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!cv-K!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2773bc1-760d-49ee-a213-38dd90577bdc_1024x296.png" width="1024" height="296" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e2773bc1-760d-49ee-a213-38dd90577bdc_1024x296.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:296,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!cv-K!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2773bc1-760d-49ee-a213-38dd90577bdc_1024x296.png 424w, https://substackcdn.com/image/fetch/$s_!cv-K!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2773bc1-760d-49ee-a213-38dd90577bdc_1024x296.png 848w, https://substackcdn.com/image/fetch/$s_!cv-K!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2773bc1-760d-49ee-a213-38dd90577bdc_1024x296.png 1272w, https://substackcdn.com/image/fetch/$s_!cv-K!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe2773bc1-760d-49ee-a213-38dd90577bdc_1024x296.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>So how can we use it for the solution&nbsp;above?</h4><p>Step 1&#8202;&#8212;&#8202;In our case, the <em>event model</em> repository hosts the&nbsp;.<em>proto</em>&nbsp;file.</p><p>Step 2&#8202;&#8212;&#8202;This repository propagates code with the same for the rest of the pipeline through pushing libraries. This repository also creates the table on BigQuery (as we will mention in the next section, this part has changed).</p><p>Step 3&#8202;&#8212;&#8202;The service <em>judge-judi</em> will also fetch the latest Go version of the <em>event model </em>because it will need it later. And, just like Alice, through the SDKs we can fetch the latest version of this <em>event model </em>on the client apps to serialize the events that we will send. The app will serialize the event and send it to the&nbsp;API.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SCOs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6b766e3-aa49-4707-9af0-62dbd74278e0_1024x270.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SCOs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6b766e3-aa49-4707-9af0-62dbd74278e0_1024x270.png 424w, https://substackcdn.com/image/fetch/$s_!SCOs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6b766e3-aa49-4707-9af0-62dbd74278e0_1024x270.png 848w, https://substackcdn.com/image/fetch/$s_!SCOs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6b766e3-aa49-4707-9af0-62dbd74278e0_1024x270.png 1272w, https://substackcdn.com/image/fetch/$s_!SCOs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6b766e3-aa49-4707-9af0-62dbd74278e0_1024x270.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!SCOs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6b766e3-aa49-4707-9af0-62dbd74278e0_1024x270.png" width="1024" height="270" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c6b766e3-aa49-4707-9af0-62dbd74278e0_1024x270.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:270,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!SCOs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6b766e3-aa49-4707-9af0-62dbd74278e0_1024x270.png 424w, https://substackcdn.com/image/fetch/$s_!SCOs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6b766e3-aa49-4707-9af0-62dbd74278e0_1024x270.png 848w, https://substackcdn.com/image/fetch/$s_!SCOs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6b766e3-aa49-4707-9af0-62dbd74278e0_1024x270.png 1272w, https://substackcdn.com/image/fetch/$s_!SCOs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc6b766e3-aa49-4707-9af0-62dbd74278e0_1024x270.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Step 4&#8202;&#8212;&#8202;We collect this event as it is, without deserializing it, and we publish it to the queue on&nbsp;Pub/Sub.</p><p>Step 5&#8202;&#8212;&#8202;The service <em>judge-judi </em>pulls the events and deserializes them.</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4kjA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e56d587-31e7-4c94-9bee-8f3877851acc_1024x223.png 424w, https://substackcdn.com/image/fetch/$s_!4kjA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e56d587-31e7-4c94-9bee-8f3877851acc_1024x223.png 848w, https://substackcdn.com/image/fetch/$s_!4kjA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e56d587-31e7-4c94-9bee-8f3877851acc_1024x223.png 1272w, https://substackcdn.com/image/fetch/$s_!4kjA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e56d587-31e7-4c94-9bee-8f3877851acc_1024x223.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!4kjA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e56d587-31e7-4c94-9bee-8f3877851acc_1024x223.png" width="1024" height="223" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0e56d587-31e7-4c94-9bee-8f3877851acc_1024x223.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:223,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!4kjA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e56d587-31e7-4c94-9bee-8f3877851acc_1024x223.png 424w, https://substackcdn.com/image/fetch/$s_!4kjA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e56d587-31e7-4c94-9bee-8f3877851acc_1024x223.png 848w, https://substackcdn.com/image/fetch/$s_!4kjA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e56d587-31e7-4c94-9bee-8f3877851acc_1024x223.png 1272w, https://substackcdn.com/image/fetch/$s_!4kjA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0e56d587-31e7-4c94-9bee-8f3877851acc_1024x223.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div></div></div></div></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!evtv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c73939-48f1-43f1-b3ef-074d9822d3f8_1024x374.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!evtv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c73939-48f1-43f1-b3ef-074d9822d3f8_1024x374.png 424w, https://substackcdn.com/image/fetch/$s_!evtv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c73939-48f1-43f1-b3ef-074d9822d3f8_1024x374.png 848w, https://substackcdn.com/image/fetch/$s_!evtv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c73939-48f1-43f1-b3ef-074d9822d3f8_1024x374.png 1272w, https://substackcdn.com/image/fetch/$s_!evtv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c73939-48f1-43f1-b3ef-074d9822d3f8_1024x374.png 1456w" sizes="auto, 100vw"><img src="https://substackcdn.com/image/fetch/$s_!evtv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c73939-48f1-43f1-b3ef-074d9822d3f8_1024x374.png" width="1024" height="374" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b4c73939-48f1-43f1-b3ef-074d9822d3f8_1024x374.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:374,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!evtv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c73939-48f1-43f1-b3ef-074d9822d3f8_1024x374.png 424w, https://substackcdn.com/image/fetch/$s_!evtv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c73939-48f1-43f1-b3ef-074d9822d3f8_1024x374.png 848w, https://substackcdn.com/image/fetch/$s_!evtv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c73939-48f1-43f1-b3ef-074d9822d3f8_1024x374.png 1272w, https://substackcdn.com/image/fetch/$s_!evtv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4c73939-48f1-43f1-b3ef-074d9822d3f8_1024x374.png 1456w" sizes="auto, 100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><blockquote><p>It&#8217;s all good now, you drop the mic, walk out of the room, head high&#8230;until&#8230;stuff happens. You realize that the reality is as far from your expectations as your tongue is from touching your nose&#8230; So close, and yet so far (don&#8217;t try that, you&#8217;ll look ridiculous)</p></blockquote><h3><strong>Expectations vs.&nbsp;reality</strong></h3><h4>The myth of language agnosticism</h4><p>When the mechanism of protocol buffers is supposed to be language-agnostic and platform agnostic. In practice, this does not seem to be the&nbsp;case.</p><p>For example, some platforms had encoding that was not consistent with the deserializer. We use <a href="https://github.com/gogo/protobuf">the gogofaster</a> compiler for Go which is a third party implementation that leverages type-specific marshaling code for extra performance. But we use it mainly to provide more canonical Go structures, less typing and to not end up with the<a href="https://github.com/golang/protobuf/issues/276"> infamous </a><em><a href="https://github.com/golang/protobuf/issues/276">xxx</a></em><a href="https://github.com/golang/protobuf/issues/276">&nbsp;fields</a>.</p><h4>The myth of backward and forward compatibility</h4><p>Let&#8217;s say a field gets added, and a field gets removed from the schema. If the service <em>judge-judi</em> is using the latest version of the <em>event model</em>, it will still be able to deserialize the data with no problem even if the client application does not update to the latest version. But this is only true for simple changes. For example, this would not hold if we change the nesting of some&nbsp;fields.</p><p>Moreover, although we avoided talking about the analysis that comes after the storage part earlier, a new event model would mean that all the jobs and tables that depend on it will change. For this reason, we separated the version of the event model from the version of the event table on BigQuery so that the table would change less often. We then provide a mapping on <em>judge-judi </em>from the event model to the table schema before storing the&nbsp;events.</p><p>Thus, we identify mainly three types of changes in the <em>event model</em>&#8217;s<em> </em>schema and act accordingly:<br>&#8202;&#8212;&#8202;<em>Non-breaking changes</em>: that happen regularly. These are the changes that do not break backward compatibility. This can be the deletion or addition of fields in the protobuf schema. <br>&#8202;&#8212;&#8202;<em>Breaking changes that do not require a change of the BigQuery table</em>: that rarely happen, and should be minimized. When these happen judge-<em>judi</em> just changes the mapping between the event and the table.<em><br>&#8202;&#8212;&#8202;Breaking changes that require a change of the BigQuery table</em>: very rare and have yet to happen but they could (thus they will). Since those are rare and we cannot know what these will be like we&#8217;ll tackle them on a case by case&nbsp;basis.</p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/p/make-your-data-pipeline-less-chaotic?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">If you found this useful please give it a heart and share it. This is very easy and means a lot to me.</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/p/make-your-data-pipeline-less-chaotic?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.ismail.bio/p/make-your-data-pipeline-less-chaotic?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p></div><h3>Conclusion</h3><p>The reality of data collection and analysis is very different from the mathematical fantasies of well-defined axioms and controllable assertions.</p><p>Coming from a scientific background, we have a tendency to want to keep the pipeline clean. And that&#8217;s good. However, chaos in data engineering is not just real but probably unavoidable. And it is one of the reasons why we should try to iterate slowly, and add tests and data quality checks when possible. It is also why keeping a central definition of the data in one place can be very&nbsp;helpful.</p><p>Protocol buffers are one way of defining a data model that seems to improve serialization overhead and be almost language agnostic. If you already know what the data schema should look like, protobuf is good at managing additions and deletions of fields, although one has to be careful as always. More complex changes will require more thoughtfulness.</p><p><strong>Hope you found something useful reading this&#8230; Have a beautiful day!</strong></p><p>If you have any comments or suggestions would love to hear from&nbsp;you.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/p/make-your-data-pipeline-less-chaotic/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://blog.ismail.bio/p/make-your-data-pipeline-less-chaotic/comments"><span>Leave a comment</span></a></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://blog.ismail.bio/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption"><em>Experiments</em> is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><p><a href="https://medium.com/the-svt-tech-blog/make-your-data-pipeline-less-chaotic-by-one-versioned-definition-of-data-5ddb92c0c6f6">Make your data pipeline less chaotic by one versioned definition of data</a> was originally published in <a href="https://medium.com/the-svt-tech-blog">The SVT Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded></item></channel></rss>