<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tim Bright</title>
    <description>The latest articles on DEV Community by Tim Bright (@40percentironman).</description>
    <link>https://dev.to/40percentironman</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F502756%2F90e50a74-3c0b-4f05-8a5c-5cbe41da0a5e.png</url>
      <title>DEV Community: Tim Bright</title>
      <link>https://dev.to/40percentironman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/40percentironman"/>
    <language>en</language>
    <item>
      <title>DDD: Anti-Corruption Layers for Handling Vendor Data</title>
      <dc:creator>Tim Bright</dc:creator>
      <pubDate>Mon, 03 Aug 2026 17:16:41 +0000</pubDate>
      <link>https://dev.to/40percentironman/ddd-anti-corruption-layers-for-handling-vendor-data-5690</link>
      <guid>https://dev.to/40percentironman/ddd-anti-corruption-layers-for-handling-vendor-data-5690</guid>
      <description>&lt;p&gt;This is an article written by Claude but guided by me. I hate a lot of the domain-driven design articles because IMO they all talk around the issue but it's never actionable (read: helpful).&lt;/p&gt;

&lt;p&gt;Hopefully you think this is different.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Anti-Corruption Layer in Go: One Flight Model, Many Vendors
&lt;/h1&gt;

&lt;p&gt;Every airline operations system eventually integrates with something it didn't design. A legacy movement-message feed, a crew rostering package bought in 2009, a ground handler's REST API that returns &lt;code&gt;"9999-12-31"&lt;/code&gt; when it means "unknown." Each of these ships a model of the world along with its data, and that model is almost never yours.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;anti-corruption layer&lt;/strong&gt; (ACL) is the code that stops the vendor's model from leaking into your domain. It's a translation boundary: foreign representations in, your aggregates out, and nothing partial in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually get from it
&lt;/h2&gt;

&lt;p&gt;Three things, in order of importance:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One model instead of N.&lt;/strong&gt; Without an ACL, &lt;code&gt;STATUS_CD == "OUT"&lt;/code&gt; shows up in your scheduling service, your delay reporting, and your gate display logic. Now the vendor's enum &lt;em&gt;is&lt;/em&gt; your domain vocabulary, and it will diverge from what your business actually means. With an ACL, that string is resolved to a &lt;code&gt;flight.Status&lt;/code&gt; exactly once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invariants enforced at a single door.&lt;/strong&gt; Your &lt;code&gt;Flight&lt;/code&gt; aggregate has rules: no off-block time without an assigned tail, no delay over fifteen minutes without a reason code. An ACL makes translation go &lt;em&gt;through&lt;/em&gt; those rules rather than around them. Foreign data becomes a &lt;em&gt;candidate&lt;/em&gt; state that the aggregate accepts or rejects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A named place for corruption to fail.&lt;/strong&gt; Sentinel dates, empty strings meaning "unknown," a status code the vendor added last Tuesday — these become explicit translation errors at the edge, instead of a zero-valued &lt;code&gt;time.Time&lt;/code&gt; quietly propagating into a delay report.&lt;/p&gt;

&lt;h2&gt;
  
  
  The domain, expressed on its own terms
&lt;/h2&gt;

&lt;p&gt;The aggregate depends on nothing outside the standard library. That constraint is the whole design.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Scheduled&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;iota&lt;/span&gt;
    &lt;span class="n"&gt;OffBlocks&lt;/span&gt;
    &lt;span class="n"&gt;Airborne&lt;/span&gt;
    &lt;span class="n"&gt;OnBlocks&lt;/span&gt;
    &lt;span class="n"&gt;Cancelled&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Flight is the aggregate root. All fields unexported: no&lt;/span&gt;
&lt;span class="c"&gt;// caller can assemble an invalid Flight.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Flight&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="n"&gt;LegID&lt;/span&gt;
    &lt;span class="n"&gt;designator&lt;/span&gt;   &lt;span class="n"&gt;Designator&lt;/span&gt; &lt;span class="c"&gt;// e.g. UA 1174&lt;/span&gt;
    &lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt; &lt;span class="n"&gt;Station&lt;/span&gt;
    &lt;span class="n"&gt;scheduledOut&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="c"&gt;// always UTC&lt;/span&gt;
    &lt;span class="n"&gt;actualOut&lt;/span&gt;    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
    &lt;span class="n"&gt;tail&lt;/span&gt;         &lt;span class="n"&gt;Tail&lt;/span&gt; &lt;span class="c"&gt;// zero until an aircraft is assigned&lt;/span&gt;
    &lt;span class="n"&gt;delay&lt;/span&gt;        &lt;span class="n"&gt;Delay&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;       &lt;span class="n"&gt;Status&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;RecordOffBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;Scheduled&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"off-blocks from status %v: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrIllegalTransition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsZero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ErrNoAircraftAssigned&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actualOut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OffBlocks&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheduledOut&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ErrDelayReasonRequired&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The aggregate also needs a way to be reconstituted from outside state — from your own database, and from a vendor. Give it exactly one such door:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Snapshot is inbound state awaiting validation. It is not a&lt;/span&gt;
&lt;span class="c"&gt;// Flight; it is a request to become one.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Snapshot&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;                &lt;span class="n"&gt;LegID&lt;/span&gt;
    &lt;span class="n"&gt;Designator&lt;/span&gt;        &lt;span class="n"&gt;Designator&lt;/span&gt;
    &lt;span class="n"&gt;Origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dest&lt;/span&gt;      &lt;span class="n"&gt;Station&lt;/span&gt;
    &lt;span class="n"&gt;ScheduledOut&lt;/span&gt;      &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
    &lt;span class="n"&gt;ActualOut&lt;/span&gt;         &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
    &lt;span class="n"&gt;Tail&lt;/span&gt;              &lt;span class="n"&gt;Tail&lt;/span&gt;
    &lt;span class="n"&gt;Delay&lt;/span&gt;             &lt;span class="n"&gt;Delay&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;            &lt;span class="n"&gt;Status&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Rehydrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;Snapshot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ScheduledOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsZero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ScheduledOut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTC&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrScheduleTimeInvalid&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;OffBlocks&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;Cancelled&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActualOut&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrMissingActualOut&lt;/span&gt; &lt;span class="c"&gt;// "OUT" with no OUT time&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tail&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsZero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrNoAircraftAssigned&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delay&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minutes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delay&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrDelayReasonRequired&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// ... remaining checks&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;/* ... */&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Rehydrate&lt;/code&gt; is what makes this a real anti-corruption layer and not just a mapper. If the translator could fill in a &lt;code&gt;Flight&lt;/code&gt;'s fields directly, it could build a flight your rules say is impossible — one already off the gate with no aircraft assigned to it. Because all it can hand over is a &lt;code&gt;Snapshot&lt;/code&gt;, the domain gets to say no.&lt;/p&gt;

&lt;h2&gt;
  
  
  The port belongs to the consumer
&lt;/h2&gt;

&lt;p&gt;OpsHub is the ground handler's operations platform: their system of record for flight legs, on their infrastructure, versioned on their release schedule. They ship a generated Go client for its API, and you own the adapter package that wraps that client. So when your scheduler needs one leg before reassigning an aircraft, what the vendor offers you is this — OpsHub's key in, OpsHub's record out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// package opshubsdk — the vendor's generated client, not yours&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetFlightLegV2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;legDTO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling that from the scheduler would bind the scheduler to &lt;code&gt;legDTO&lt;/code&gt; and to OpsHub's idea of a key. The fix is to put the interface where it is &lt;em&gt;used&lt;/em&gt; rather than where it is implemented — the domain declares the capability it needs, in its own terms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;

&lt;span class="c"&gt;// Named and owned here; implemented elsewhere.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;LegRepository&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;LegID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go implementations don't declare which interfaces they satisfy, so &lt;code&gt;opshub.Adapter&lt;/code&gt; can fit &lt;code&gt;LegRepository&lt;/code&gt; without the domain ever mentioning &lt;code&gt;opshub&lt;/code&gt;. Pin that in the adapter package, where the dependency is allowed to exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LegRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Adapter&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dependencies now point inward — &lt;code&gt;opshub&lt;/code&gt; imports &lt;code&gt;flight&lt;/code&gt;, never the reverse, and &lt;code&gt;flight&lt;/code&gt; imports only the standard library. That arrow is the mechanical difference between an ACL and a shared "types" package, and the compiler enforces it: an &lt;code&gt;import ".../opshub"&lt;/code&gt; inside &lt;code&gt;flight&lt;/code&gt; is a review-stopping defect. Swap ground handlers and &lt;code&gt;LegRepository&lt;/code&gt; and every caller above it are untouched; you write one new adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The foreign model, quarantined
&lt;/h2&gt;

&lt;p&gt;Everything below is code you own: the &lt;code&gt;opshub&lt;/code&gt; package &lt;em&gt;is&lt;/em&gt; the anti-corruption layer. It holds three types with three separate jobs. &lt;code&gt;legDTO&lt;/code&gt; mirrors the vendor's JSON exactly, warts included, and stays unexported so nothing outside the package can depend on its shape. &lt;code&gt;Translator&lt;/code&gt; does the mapping — one &lt;code&gt;legDTO&lt;/code&gt; in, one &lt;code&gt;flight.Snapshot&lt;/code&gt; out, no I/O anywhere in it, which is why it's trivial to test. &lt;code&gt;Adapter&lt;/code&gt; wires the two together and is the type that satisfies &lt;code&gt;flight.LegRepository&lt;/code&gt;: fetch, translate, hand to the domain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;opshub&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;legDTO&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;LegKey&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"LEG_KEY"`&lt;/span&gt;
    &lt;span class="n"&gt;FltNbr&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"FLT_NBR"`&lt;/span&gt;  &lt;span class="c"&gt;// "1174", sometimes " 174"&lt;/span&gt;
    &lt;span class="n"&gt;DepStn&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"DEP_STN"`&lt;/span&gt;
    &lt;span class="n"&gt;ArrStn&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"ARR_STN"`&lt;/span&gt;
    &lt;span class="n"&gt;SkdOut&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"SKD_OUT"`&lt;/span&gt;  &lt;span class="c"&gt;// naive station-local time&lt;/span&gt;
    &lt;span class="n"&gt;ActOut&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"ACT_OUT"`&lt;/span&gt;  &lt;span class="c"&gt;// "" or "9999-12-31 00:00"&lt;/span&gt;
    &lt;span class="n"&gt;StatusCd&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"STATUS_CD"`&lt;/span&gt;
    &lt;span class="n"&gt;AcReg&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"AC_REG"`&lt;/span&gt;
    &lt;span class="n"&gt;DlyMins&lt;/span&gt;  &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="s"&gt;`json:"DLY_MINS"`&lt;/span&gt;
    &lt;span class="n"&gt;DlyRsnCd&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"DLY_RSN_CD"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;statuses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"SKD"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scheduled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"OUT"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OffBlocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"OFF"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Airborne&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"ON"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Airborne&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// wheels-on is not a state we model&lt;/span&gt;
    &lt;span class="s"&gt;"IN"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnBlocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"CNL"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancelled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Translator&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;zones&lt;/span&gt; &lt;span class="n"&gt;ZoneResolver&lt;/span&gt; &lt;span class="c"&gt;// station -&amp;gt; IANA time zone&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="n"&gt;Translator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;toSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;legDTO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;statuses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCd&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Vendor enum drift. Refuse; do not guess.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Snapshot&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%w: STATUS_CD %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrUnmappable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewStation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DepStn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Snapshot&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DEP_STN: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;skdOut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toUTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SkdOut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Snapshot&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SKD_OUT: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;actOut&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isNullish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActOut&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// "", "9999-12-31 00:00", "0000-00-00"&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toUTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActOut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Snapshot&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ACT_OUT: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;actOut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Snapshot&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;           &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LegID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LegKey&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;Origin&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ScheduledOut&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;skdOut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ActualOut&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;actOut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Tail&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ParseTail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AcReg&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c"&gt;// zero if ""&lt;/span&gt;
        &lt;span class="n"&gt;Delay&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Minutes&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DlyMins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mapReason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DlyRsnCd&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Find is the port implementation: vendor call, translation, then the domain.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;Adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LegID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetchLeg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;translator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rehydrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// domain has final say&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what the ACL absorbed: naive local times and the station-to-time-zone lookup they require, whitespace in flight numbers, three spellings of null, a wheels-on state your business doesn't track, and vendor delay codes mapped to your own taxonomy. None of that is visible past the &lt;code&gt;Find&lt;/code&gt; method's signature.&lt;/p&gt;

&lt;p&gt;Note also what it refuses to do. An unknown &lt;code&gt;STATUS_CD&lt;/code&gt; is not defaulted to &lt;code&gt;Scheduled&lt;/code&gt;. A &lt;code&gt;"OUT"&lt;/code&gt; leg with no &lt;code&gt;ACT_OUT&lt;/code&gt; does not become an &lt;code&gt;OffBlocks&lt;/code&gt; flight with a zero timestamp — &lt;code&gt;Rehydrate&lt;/code&gt; rejects it. Translation is &lt;strong&gt;total or it fails&lt;/strong&gt;: every foreign value either maps to a domain concept or produces an error naming the field. Silent defaulting is how vendor corruption gets into a delay-cost report, and by then the trail is cold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing at the boundary
&lt;/h2&gt;

&lt;p&gt;The ACL is the cheapest thing in your system to test, because it's a pure function over bytes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Golden payloads.&lt;/strong&gt; Keep real (scrubbed) vendor responses in &lt;code&gt;testdata/&lt;/code&gt; and assert the resulting &lt;code&gt;Snapshot&lt;/code&gt;. When the vendor changes something in production, the diff shows up here first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An unknown-code case.&lt;/strong&gt; Assert that a novel &lt;code&gt;STATUS_CD&lt;/code&gt; returns &lt;code&gt;ErrUnmappable&lt;/code&gt;. This test is the one that pays for the layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain rules tested separately&lt;/strong&gt;, with hand-built &lt;code&gt;Snapshot&lt;/code&gt;s and no vendor JSON in sight. &lt;code&gt;Rehydrate&lt;/code&gt; and &lt;code&gt;RecordOffBlocks&lt;/code&gt; don't know OpsHub exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When the payload updates an aggregate you already have
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Rehydrate&lt;/code&gt; covers the read-through case: you hold no state of your own, so you build a &lt;code&gt;Flight&lt;/code&gt; out of the vendor's picture of it. Movement messages are the harder case. OpsHub pushes an update for a leg you already have, and your copy holds decisions the vendor knows nothing about — the delay reason a controller keyed in, the tail your own scheduler assigned.&lt;/p&gt;

&lt;p&gt;The tempting move is to translate the payload into a full &lt;code&gt;Snapshot&lt;/code&gt; and &lt;code&gt;Rehydrate&lt;/code&gt; over the top. Don't. That makes the vendor win every field, and its blanks quietly erase your data.&lt;/p&gt;

&lt;p&gt;Translate the payload into an &lt;em&gt;intention&lt;/em&gt; instead — a command, in domain vocabulary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;

&lt;span class="c"&gt;// A Command is a claim about what happened, not a new state.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Command&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ApplyTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OffBlocksReported&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;At&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;OffBlocksReported&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ApplyTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RecordOffBlocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;At&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// the aggregate may still refuse&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The translator's job becomes deciding &lt;em&gt;which&lt;/em&gt; domain command the payload represents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;opshub&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="n"&gt;Translator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;toCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;legDTO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCd&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"OUT"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toUTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActOut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;/* origin station */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ACT_OUT: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OffBlocksReported&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;At&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"CNL"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CancellationReported&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mapReason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DlyRsnCd&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%w: STATUS_CD %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrUnmappable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Applying it belongs in your application service, not in the ACL — load from your own store, apply, save:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ApplyLegUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LegID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;legs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// s.legs is your database, not OpsHub&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApplyTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;legs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things fall out of this that a snapshot-and-overwrite approach can't give you. Vendors send &lt;strong&gt;partial payloads&lt;/strong&gt;, and a command carries only what changed, so you're never forced to invent values for the fields the message omitted. Movement messages arrive &lt;strong&gt;late and duplicated&lt;/strong&gt;, and a replayed &lt;code&gt;"OUT"&lt;/code&gt; hits &lt;code&gt;RecordOffBlocks&lt;/code&gt; on a flight that is already off blocks and comes back as &lt;code&gt;ErrIllegalTransition&lt;/code&gt; — a rejected duplicate rather than a rewritten history. And when one payload spans &lt;strong&gt;two aggregates&lt;/strong&gt; — leg movement plus a crew change — the translator emits one command each, so the vendor's message shape doesn't get to define your transaction boundaries. Each aggregate stays its own unit of consistency, which is the whole reason you drew the boundary.&lt;/p&gt;

&lt;p&gt;That is the real job of the layer. Not moving fields between structs, but deciding what an outside system is allowed to assert about a model you own.&lt;/p&gt;

</description>
      <category>ddd</category>
      <category>go</category>
      <category>architecture</category>
    </item>
    <item>
      <title>DDD: The Repository Pattern with Rich Domain Models</title>
      <dc:creator>Tim Bright</dc:creator>
      <pubDate>Thu, 23 Jul 2026 17:11:45 +0000</pubDate>
      <link>https://dev.to/40percentironman/the-repository-pattern-with-rich-domain-models-21bh</link>
      <guid>https://dev.to/40percentironman/the-repository-pattern-with-rich-domain-models-21bh</guid>
      <description>&lt;p&gt;This is an article written by Claude but guided by me. I hate all the explanations about combining repository pattern, domain models, and domain-driven design because IMO they all talk around the issue but it's never actionable (read: helpful).&lt;/p&gt;

&lt;p&gt;Hopefully you think this is different.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Here's an example of the repository Pattern with rich domain models in the context of an airline's flight operations:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most Go codebases handle persistence the same way: a struct full of exported fields, a &lt;code&gt;db&lt;/code&gt; package that scans rows into it, and business rules scattered across handlers and services. It works, until the rules get interesting. Airline flight operations is a domain where the rules get interesting fast, so let's use it to show what two patterns each buy you — and how they meet in the DDD concept of an &lt;strong&gt;aggregate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The division of labor is clean and worth stating up front. A &lt;strong&gt;rich domain model&lt;/strong&gt; is about keeping the business rules &lt;em&gt;inside&lt;/em&gt; the domain model — the flight decides whether it can depart. The &lt;strong&gt;Repository pattern&lt;/strong&gt; is about how data gets &lt;em&gt;loaded into&lt;/em&gt; that model and saved back out — without the model ever knowing where it came from. One governs behavior, the other governs movement. Confuse the two and you get models that know SQL, or repositories that make business decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anemic starting point
&lt;/h2&gt;

&lt;p&gt;Here's the version you've seen a hundred times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Flight&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;          &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;      &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Crew&lt;/span&gt;        &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;CrewAssignment&lt;/span&gt;
    &lt;span class="n"&gt;DepartedAt&lt;/span&gt;  &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is exported, so anything can do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"DEPARTED"&lt;/span&gt; &lt;span class="c"&gt;// with zero crew assigned, at a gate, in the past&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The struct is a bag of data — an &lt;em&gt;anemic&lt;/em&gt; model. The rule "a flight cannot depart without a full crew complement" has to live somewhere else: a service, a handler, a stored procedure, or worst of all, three of those places with slight differences. Nothing in the type system stops a new teammate from writing the invalid assignment above. The business rules live &lt;em&gt;outside&lt;/em&gt; the model — that's the disease a rich domain model cures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rich domain models: the rules live inside
&lt;/h2&gt;

&lt;p&gt;A rich domain model inverts this. Data becomes unexported; the only way in is through methods that enforce the rules of the domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Scheduled&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;iota&lt;/span&gt;
    &lt;span class="n"&gt;Boarding&lt;/span&gt;
    &lt;span class="n"&gt;Departed&lt;/span&gt;
    &lt;span class="n"&gt;Cancelled&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Flight&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;     &lt;span class="n"&gt;ID&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;
    &lt;span class="n"&gt;crew&lt;/span&gt;   &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;CrewAssignment&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;DomainEvent&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;minCrew&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;

&lt;span class="c"&gt;// Depart is the ONLY way a flight departs.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Depart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;Boarding&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"flight %s: cannot depart from status %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;crew&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;minCrew&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ErrInsufficientCrew&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Departed&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FlightDeparted&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;FlightID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;At&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;AssignCrew&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;CrewMember&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Departed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Cancelled&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ErrFlightClosed&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;crew&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MemberID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ErrAlreadyAssigned&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;crew&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;crew&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CrewAssignment&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MemberID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this affords you: &lt;strong&gt;the business rules now live inside the model, and nowhere else&lt;/strong&gt;. Because &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;crew&lt;/code&gt; are unexported, code outside the &lt;code&gt;flight&lt;/code&gt; package &lt;em&gt;cannot&lt;/em&gt; construct or mutate a &lt;code&gt;Flight&lt;/code&gt; that violates the rules — invalid states become unrepresentable. &lt;code&gt;Depart&lt;/code&gt; isn't a setter with validation bolted on; it's the domain operation itself, encoding the legal state machine (&lt;code&gt;Scheduled → Boarding → Departed&lt;/code&gt;, with &lt;code&gt;Cancelled&lt;/code&gt; reachable until departure). Ask "can this flight depart?" and there is exactly one place the answer can come from. Go gives you this almost for free: package-level encapsulation via lowercase fields is the enforcement mechanism, no annotations or frameworks required.&lt;/p&gt;

&lt;p&gt;Note what the model deliberately knows nothing about: databases, rows, transactions. It answers &lt;em&gt;what is allowed to happen&lt;/em&gt;; it has no opinion on &lt;em&gt;how it got into memory&lt;/em&gt;. That second question belongs to a different pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  The aggregate: drawing the consistency boundary
&lt;/h2&gt;

&lt;p&gt;Notice that &lt;code&gt;Flight&lt;/code&gt; owns its &lt;code&gt;[]CrewAssignment&lt;/code&gt;. That's deliberate, and it's what DDD calls an &lt;strong&gt;aggregate&lt;/strong&gt;: a cluster of objects treated as a single unit for data changes, with one entry point — the &lt;strong&gt;aggregate root&lt;/strong&gt; (&lt;code&gt;Flight&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The boundary is chosen by asking one question: &lt;strong&gt;what has to be true at save time?&lt;/strong&gt; Whatever must hold the instant a transaction commits goes inside the aggregate; everything else stays out. "Minimum four crew before departure" must be true the moment a departed flight hits the database — there is no acceptable window where a departed flight has three crew. So the flight and its crew list must change together, and crew assignments live inside the aggregate.&lt;/p&gt;

&lt;p&gt;By contrast, "a crew member can't work two flights the same day" spans &lt;em&gt;many&lt;/em&gt; flights. Pulling every flight a crew member touches into one aggregate would mean one giant lock over half the schedule. So that rule is allowed to be true &lt;em&gt;eventually&lt;/em&gt;: a domain service or an event handler detects the conflict after commit and triggers a reassignment. &lt;code&gt;CrewMember&lt;/code&gt; itself is a separate aggregate that &lt;code&gt;Flight&lt;/code&gt; references only by ID.&lt;/p&gt;

&lt;p&gt;This is the real payoff of the aggregate concept: the boundary is your consistency dial. Inside the aggregate, invariants are &lt;strong&gt;strongly consistent&lt;/strong&gt; — enforced in one transaction, never observable in a broken state. Across aggregates, consistency is &lt;strong&gt;eventual&lt;/strong&gt; — reconciled by events, sagas, or background checks. Draw the boundary too wide and you've serialized your writes; too narrow and rules that must hold at commit time can't be enforced at all.&lt;/p&gt;

&lt;p&gt;Two working rules fall out of this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Outside code holds a reference only to the root. Nobody reaches into a flight and edits &lt;code&gt;crew[2]&lt;/code&gt; directly.&lt;/li&gt;
&lt;li&gt;The aggregate is the unit of loading and saving — which brings us to repositories.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Repository: how aggregates get in and out of memory
&lt;/h2&gt;

&lt;p&gt;If the rich model owns the rules, the repository owns the &lt;em&gt;movement&lt;/em&gt;: it is the mechanism by which data is loaded from storage into a domain model, and by which a changed model gets back out. It presents aggregates as if they were an in-memory collection — you &lt;code&gt;Get&lt;/code&gt; a flight, you &lt;code&gt;Save&lt;/code&gt; a flight, and the fact that rows, documents, or a map were involved is invisible. The interface lives in the &lt;strong&gt;domain package&lt;/strong&gt;, expressed purely in domain terms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Repository&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;DepartingBetween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="n"&gt;TimeRange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;*sql.DB&lt;/code&gt;, no column names, no ORM types. What this affords you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dependency inversion.&lt;/strong&gt; The domain defines the interface; the &lt;code&gt;postgres&lt;/code&gt; package implements it. Your business logic imports nothing from your storage layer — the arrow points the other way. This is idiomatic Go anyway: interfaces are defined by the consumer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One repository per aggregate, operating on whole aggregates.&lt;/strong&gt; &lt;code&gt;Save&lt;/code&gt; persists the flight &lt;em&gt;and&lt;/em&gt; its crew assignments in one transaction — the repository is what makes "true at save time" literally true, because the aggregate's transactional boundary becomes the database's. There is no &lt;code&gt;CrewAssignmentRepository&lt;/code&gt;: crew assignments aren't independently addressable, and giving them their own save path would let callers commit a partial aggregate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testability.&lt;/strong&gt; A &lt;code&gt;memoryRepository&lt;/code&gt; backed by a map makes domain and application tests trivial, no database container needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Equally important is what the repository must &lt;em&gt;not&lt;/em&gt; do: decide anything. It never checks crew counts or flips statuses — it faithfully moves state between storage and model. All judgment stays inside the aggregate.&lt;/p&gt;

&lt;p&gt;The application service ties it together, and stays thin because loading is the repository's job and deciding is the model's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OpsService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;DepartFlight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flights&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Depart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="c"&gt;// domain said no&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flights&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repository loads, model decides, repository saves. Every use case reads like this, and each line stays in its lane. If you find business logic creeping into the service — or worse, into a repository implementation — it belongs on the aggregate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rehydration wrinkle
&lt;/h2&gt;

&lt;p&gt;There's one honest tension in Go: if fields are unexported, how does the Postgres implementation rebuild a &lt;code&gt;Flight&lt;/code&gt; from rows? Don't weaken encapsulation with setters. Instead, export a single reconstitution function from the domain package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Rehydrate rebuilds a Flight from persisted state.&lt;/span&gt;
&lt;span class="c"&gt;// For repository use only; it bypasses creation rules&lt;/span&gt;
&lt;span class="c"&gt;// because the state was already validated when written.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Rehydrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;crew&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;CrewAssignment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Flight&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;crew&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;crew&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a trust boundary, documented as such — a small price for keeping every &lt;em&gt;mutation&lt;/em&gt; path guarded.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the three ideas lock together
&lt;/h2&gt;

&lt;p&gt;Each concept answers a different question, and each depends on the next. The &lt;strong&gt;rich domain model&lt;/strong&gt; answers &lt;em&gt;where do the business rules live?&lt;/em&gt; — inside the model, enforced by encapsulation. The &lt;strong&gt;aggregate&lt;/strong&gt; answers &lt;em&gt;what has to be true at save time?&lt;/em&gt; — it draws the line between the strongly consistent (inside, one transaction) and the eventually consistent (across boundaries, reconciled later), and names a root to guard it. The &lt;strong&gt;repository&lt;/strong&gt; answers &lt;em&gt;how does data get loaded into that model and saved back out?&lt;/em&gt; — atomically, wholesale, behind a domain-owned interface.&lt;/p&gt;

&lt;p&gt;Skip any one and the others sag. A repository over an anemic model is just a fancy DAO — the rules still leak. A rich model without an aggregate boundary can't tell you what a transaction should contain. An aggregate without a repository gets persisted piecemeal by whatever SQL each caller writes, and the boundary evaporates at the database.&lt;/p&gt;

&lt;p&gt;Together, in a domain like flight ops, they give you the thing that actually matters at 4 a.m. during irregular operations: a codebase where an illegal flight state is not a bug you catch in review, but a program that doesn't compile — or an error the domain hands back before anything touches the database.&lt;/p&gt;

</description>
      <category>ddd</category>
      <category>designpatterns</category>
      <category>go</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Frontend Code from a Backend Perspective</title>
      <dc:creator>Tim Bright</dc:creator>
      <pubDate>Tue, 26 Nov 2024 03:03:52 +0000</pubDate>
      <link>https://dev.to/40percentironman/frontend-code-from-a-backend-perspective-47i7</link>
      <guid>https://dev.to/40percentironman/frontend-code-from-a-backend-perspective-47i7</guid>
      <description>&lt;p&gt;I don't call myself a frontend developer.&lt;/p&gt;

&lt;p&gt;To be clear, I know how to do frontend work. I'm trying to keep myself up-to-date with all the newest ways to try to more easily put content on the browser in a performant way. I build web applications, so you have to know how to build a user-facing client to call your backend.&lt;/p&gt;

&lt;p&gt;I would call myself a backend developer.&lt;/p&gt;

&lt;p&gt;But I definitely wouldn't call myself a frontend developer.&lt;/p&gt;

&lt;p&gt;People say the frontend is the "easy" work. I don't. For me, it's a special kind of hell where you have every tool available but none of the right ones. It always seemed to me that it was inevitable that frontend code always ends up in spaghetti and you have to scrap and redo it periodically.&lt;/p&gt;

&lt;p&gt;Recently, I found a paradigm that resonates with me and I thought I'd share my thinking. It came to me from a more backend-focused perspective, but the more I think about it, I see it's just good programming practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Architecture: The Lost Years"
&lt;/h2&gt;

&lt;p&gt;One of the talks that influenced me the most was one Robert "Uncle Bob" Martin has that's titled &lt;em&gt;"Architecture: The Lost Years"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/WpkDN78P884"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In it, he laments how software development evolved over the last few decades:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I call this talk 'The Lost Years' [...] because from 1993 until now we have forgotten this thing we were on the verge of learning...and do you know what drove it out of our heads? The Web. The Web was so all-consuming [...] and it drove all those ideas out of our brains for [years] and they're just now starting to come back..."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, I will say Uncle Bob has some hot takes. There are a lot of people that disagree with him on his version of what "clean code" is (and I'm one of them). I try not to blindly parrot his or anyone's takes, or follow them without a deeper understanding of what they're trying to say. I can't speak to this "Middle Ages of Software Architecture" that he seems to be referring to. Back in 1991 when the Web was new, I think I was just starting to write my first "Hello world" applications in C++. Compared to him and lot of others, I'm a relative n00b.&lt;/p&gt;

&lt;p&gt;However, I have noticed that there seems to be a rift between what people say we &lt;em&gt;should&lt;/em&gt; do, and what we &lt;em&gt;actually&lt;/em&gt; do in practice. For example: &lt;strong&gt;why do most of the web app test suites I've ever seen require the web server to be running in order to run the tests?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unless you're testing for the actual user responses being received (which can be tested independent of the business logic), &lt;em&gt;there's absolutely no reason for the web server to be running.&lt;/em&gt; Whenever I've seen it, it's an artifact of treating the web server as the central abstraction of the application. The application then becomes a web server spidered with business logic. In order to test the code, you need to run the web server for the tests and do mock HTTP calls to it. This makes your testing unnecessarily slow because you have the web server in the middle of it.&lt;/p&gt;

&lt;p&gt;I've seen the same thing with frontend code. In a lot of cases, you're not able to test the business logic independent of the UI. &lt;em&gt;Why should I have to render the React code to test fetching data from the backend?&lt;/em&gt;  React is a UI framework; it should never be the central abstraction for my frontend code. However, that's what it seems most frontend clients are: "React apps" instead of applications using React. It also seems that most tutorials (and even some tools!) direct you to "Reactify" all your code, further feeding into this pattern of making React the center of your application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmj9zjc2aeoxiqnrfl3gy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmj9zjc2aeoxiqnrfl3gy.png" alt="Thanks to Uncle Bob for this image: https://www.youtube.com/watch?v=o_TH-Y78tt4&amp;amp;t=2581s" width="800" height="614"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Ports and Adapters" Pattern
&lt;/h2&gt;

&lt;p&gt;So how can we avoid this?&lt;/p&gt;

&lt;p&gt;Uncle Bob's architectural recommendation in that talk is remarkably similar to the "ports and adapters" pattern for hexagonal architecture. What Uncle Bob calls the &lt;em&gt;interactor&lt;/em&gt; object is a part of the application. It implements a specific interface that defines the functions of how the application object should be interacted with. To use the "ports and adapters" language, the interface is a port, and the interactor is an adapter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface Application {
  doAppFunction(): void;
}

class Interactor extends Application {
  constructor(...) { }

  public doAppFunction() {
    ...execute some application code
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This adapter can be plugged into another bit of code that uses the port, perhaps as an argument to a function or it can be dependency injected at creation time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class UI {
   constructor(app: Application) { this.app = app; }

   someUIFunction() {
      ...

      this.app.doAppFunction();

      ... do other UI code
   }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, the UI can do application-specific functions via the &lt;code&gt;Application&lt;/code&gt; interface but the UI code does not contain any of that code. This allows you to test the &lt;code&gt;Interactor&lt;/code&gt; object independently from the UI, meaning you don't have to run the UI and also you don't have to run the app to do UI unit tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Working with React
&lt;/h3&gt;

&lt;p&gt;So how would this work with React?&lt;/p&gt;

&lt;p&gt;One way to do this is to make a React &lt;code&gt;Context&lt;/code&gt; that holds an &lt;code&gt;Application&lt;/code&gt; object and then just fetch the object with &lt;code&gt;useContext&lt;/code&gt; whenever you need to interact with something outside of React:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const AppContext = createContext&amp;lt;Application&amp;gt;(undefined);

export const useApplication = () =&amp;gt; {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error("useApplication must be used within a ApplicationProvider");
  }
  return context;
};

export const ApplicationProvider = (props) =&amp;gt; {
  const { children, value } = props;
  return &amp;lt;AppContext.Provider value={value}&amp;gt;{children}&amp;lt;/AppContext.Provider&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can create the &lt;code&gt;Interactor&lt;/code&gt; object just before you render your React application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const application = new Interactor();

export const renderApp = () =&amp;gt; {
  const root = document.getElementById("root");
  ReactDOM.createRoot(root).render(
    &amp;lt;React.StrictMode&amp;gt;
      &amp;lt;ApplicationProvider value={application}&amp;gt;
        &amp;lt;MyReactApp /&amp;gt;
      &amp;lt;/ApplicationProvider&amp;gt;
    &amp;lt;/React.StrictMode&amp;gt;,
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can use the hook in the children to get access to the &lt;code&gt;Application&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const MyReactApp = () =&amp;gt; {
  const app = useApplication();
  app.doAppFunction();

  return &amp;lt;&amp;gt; ... &amp;lt;/&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern allows you to separate your UI from what essentially are your models and controllers. You can now work with app state and not have to bind it with React's UI state, which gives you a lot of control. However, you can still use tools like &lt;code&gt;@tanstack/query&lt;/code&gt; to control overzealous calls to your application object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Mileage May Vary
&lt;/h2&gt;

&lt;p&gt;I know I'm going against the grain on this one.&lt;/p&gt;

&lt;p&gt;I'm probably going to  get a lot of comments like, &lt;em&gt;"You shouldn't do that because _&lt;/em&gt;__ and _____&lt;em&gt;, you n00b!"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And that's okay.&lt;/p&gt;

&lt;p&gt;I don't claim to be a frontend developer.&lt;/p&gt;

&lt;p&gt;But I will say my frontend code became a lot more simple once I started using this pattern. This same thing actually happened when I started doing this with my backend code as well. I would inject my app dependencies into my app object, and then I would inject my app object as a dependency to my REST/GraphQL/gRPC code. It abstracted a lot of the unnecessary details away and kept my concerns separate.&lt;/p&gt;

&lt;p&gt;Try it out. You might like it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Distribute your Go CLI tools with GoReleaser and Homebrew</title>
      <dc:creator>Tim Bright</dc:creator>
      <pubDate>Fri, 01 Sep 2023 22:01:08 +0000</pubDate>
      <link>https://dev.to/40percentironman/distribute-your-go-cli-tools-with-goreleaser-and-homebrew-4jd8</link>
      <guid>https://dev.to/40percentironman/distribute-your-go-cli-tools-with-goreleaser-and-homebrew-4jd8</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/spf13/cobra" rel="noopener noreferrer"&gt;Cobra is awesome.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At Duro, my team and I made a sweet CLI tool that allows us to create microservices for our platform quickly and easily. The tool sets up a standard service configuration for TypeScript, linting, integration with our shared library, and specific Github Actions for pushing the service to the cloud. It's been really nice to have.&lt;/p&gt;

&lt;p&gt;We're constantly tweaking things with it, fixing little things and changing our configurations. We decided to use &lt;a href="https://goreleaser.com/" rel="noopener noreferrer"&gt;GoReleaser&lt;/a&gt; to help us version the tool and create the binaries and publish them to Github. It's been awesome.&lt;/p&gt;

&lt;p&gt;Until we had to distribute it.&lt;/p&gt;

&lt;p&gt;Revisions started increasing and it was a hassle trying to make sure that you have the latest version of the tool. In the spirit of "get things done", we just had everyone download it and delete it when they were done. Not a great solution, but hey, we had more things to worry about.&lt;/p&gt;

&lt;p&gt;Lately, I decided to take it upon myself to show this tool some love (and curry favor with my fellow devs) to try to manage versions of this tool via Homebrew. I didn't know the internals of how Homebrew worked, so I decided to dig in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Homebrew Basics
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiptxn9pc8qnwyflaqmpm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiptxn9pc8qnwyflaqmpm.png" alt="Homebrew logo image" width="800" height="1204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Homebrew is a package manager that allows users to install packages through a simple interface. If you need &lt;code&gt;curl&lt;/code&gt; and you don't have it, you can simply run &lt;code&gt;brew install curl&lt;/code&gt; and Homebrew will fetch it. You can even specify specific versions of a package if you need.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does Homebrew know where to get things from?
&lt;/h3&gt;

&lt;p&gt;Homebrew has a list of core packages &lt;a href="https://formulae.brew.sh/formula/" rel="noopener noreferrer"&gt;you can browse here&lt;/a&gt; and those are available when you type in &lt;code&gt;brew install&lt;/code&gt;. Each of these packages has a &lt;strong&gt;formula&lt;/strong&gt;, which is just a Ruby file that shows what the latest SHA is of a package and allows Homebrew to select the right binary based on your operating system. Here's an example of a formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Wget &amp;lt; Formula
  homepage "https://www.gnu.org/software/wget/"
  url "https://ftp.gnu.org/gnu/wget/wget-1.15.tar.gz"
  sha256 "52126be8cf1bddd7536886e74c053ad7d0ed2aa89b4b630f76785bac21695fcd"

  def install
    system "./configure", "--prefix=#{prefix}"
    system "make", "install"
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository that this formula is stored in is called a &lt;strong&gt;tap&lt;/strong&gt;. In fact, the list of core package are stored in the &lt;a href="https://github.com/Homebrew/homebrew-core" rel="noopener noreferrer"&gt;core tap&lt;/a&gt;. This is not only tap you can have; you can create your own tap.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I create a tap and what happens when I do?
&lt;/h3&gt;

&lt;p&gt;There's standard documentation on how to &lt;a href="https://docs.brew.sh/Taps" rel="noopener noreferrer"&gt;create a tap&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The short version is that when you type in &lt;code&gt;brew tap &amp;lt;tap_name&amp;gt; &amp;lt;URL&amp;gt;&lt;/code&gt;, it will go to that URL and clone the repository there and save it in &lt;code&gt;$(brew --repository)/Library/Taps&lt;/code&gt;. Then, when you type in &lt;code&gt;brew install &amp;lt;package&amp;gt;&lt;/code&gt;, it will search the core tap first and then any additional taps you've created. It will then look for the formula Ruby file and grab the latest package (or the version you specify) from the specified location in the formula and put it in Homebrew's path so you can access it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I create a formula?
&lt;/h3&gt;

&lt;p&gt;You can create one by hand, but &lt;a href="https://goreleaser.com/customization/homebrew/" rel="noopener noreferrer"&gt;GoReleaser can do it for you&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Here's the basic config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brews:
  -
    # Name of the recipe
    #
    # Default: ProjectName
    # Templates: allowed
    name: some_app

    # Folder inside the repository to put the formula.
    folder: Formula

    # Your app's description.
    #
    # Templates: allowed
    description: "Put in description here."

    # Repository to push the generated files to.
    repository:
      owner: example
      name: some_app
      branch: brew-releases/{{ .Version }}
      token: "{{ .Env.GITHUB_TOKEN }}"
      pull_request:
        enabled: true
        base:
          owner: example
          name: some_app
          branch: master

    # NOTE: make sure the url_template, the token and given repo (github or
    # gitlab) owner and name are from the same kind.
    # We will probably unify this in the next major version like it is
    # done with scoop.

    # URL which is determined by the given Token (github, gitlab or gitea).
    #
    # Default depends on the client.
    # Templates: allowed
    url_template: "https://github.com/example/some_app/releases/download/{{ .Tag }}/{{ .ArtifactName }}"

    # Allows you to set a custom download strategy. Note that you'll need
    # to implement the strategy and add it to your tap repository.
    # Example: https://docs.brew.sh/Formula-Cookbook#specifying-the-download-strategy-explicitly
    download_strategy: GitHubPrivateRepositoryReleaseDownloadStrategy

    custom_require: './custom_release_strategy'

    # Git author used to commit to the repository.
    commit_author:
      name: goreleaserbot
      email: goreleaserbot@example.com

    # The project name and current git tag are used in the format string.
    #
    # Templates: allowed
    commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, GoReleaser will generate the Ruby formula file for you and try to commit it into the &lt;code&gt;Formula&lt;/code&gt; folder in this repository. In my case, we made our main branch protected so we had to specify the section that auto-creates a PR with the new formula.&lt;/p&gt;

&lt;p&gt;We also have a Github Action that uses &lt;a href="https://github.com/goreleaser/goreleaser-action" rel="noopener noreferrer"&gt;GoReleaser's custom action&lt;/a&gt; to trigger a release whenever a new tag is created in our repo. Now all one of us has to do is create a release with a new tag and the GitHub Action will prepare a release with the binaries and then auto-create the PR with the updated formula. Once it's merged in, Homebrew should take care of updating the revisions.&lt;/p&gt;

&lt;p&gt;Then it's just the &lt;code&gt;brew tap&lt;/code&gt; and &lt;code&gt;brew install&lt;/code&gt; combo FTW!(You might need to have a GitHub personal access token in your &lt;code&gt;HOMEBREW_GITHUB_API_TOKEN&lt;/code&gt; env variable).&lt;/p&gt;

&lt;h2&gt;
  
  
  That doesn't sound so bad! Why were you complaining about it being a pain?
&lt;/h2&gt;

&lt;p&gt;Ah, that's right. The gotcha.&lt;/p&gt;

&lt;p&gt;Homebrew uses several different methods to download/clone repos, such as &lt;code&gt;CurlDownloadStrategy&lt;/code&gt; (&lt;a href="https://github.com/Homebrew/brew/blob/master/Library/Homebrew/download_strategy.rb#L380" rel="noopener noreferrer"&gt;which does exactly what it says&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;They used to support one called &lt;code&gt;GitHubPrivateRepositoryReleaseDownloadStrategy&lt;/code&gt;. It's exactly what the name implies: it downloads releases from GitHub private repositories. At the time I was doing my research into doing this myself, there was a good amount of documentation floating around that says to use that strategy for fun and profit.&lt;/p&gt;

&lt;p&gt;Problem is, &lt;a href="https://github.com/Homebrew/brew/pull/5112" rel="noopener noreferrer"&gt;Homebrew no longer supports that strategy&lt;/a&gt;. Then &lt;a href="https://github.com/goreleaser/goreleaser/pull/1304" rel="noopener noreferrer"&gt;GoReleaser stopped supporting it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmr318dlojpni7vftm0dp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmr318dlojpni7vftm0dp.png" alt="Morpheus what if I told you" width="375" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Screwed? Not really, thanks to some help from the community and open source software!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/goreleaser/goreleaser/issues/967#issuecomment-467354058" rel="noopener noreferrer"&gt;This comment&lt;/a&gt; in a GoReleaser issue showed me I can copy the old code from older versions of Homebrew and include them into my formula with GoReleaser.&lt;/p&gt;

&lt;p&gt;I think in the end we used &lt;a href="https://github.com/Homebrew/brew/issues/15169#issuecomment-1503860402" rel="noopener noreferrer"&gt;the recommendation made here&lt;/a&gt; with the code contained &lt;a href="https://gist.github.com/minamijoyo/3d8aa79085369efb79964ba45e24bb0e" rel="noopener noreferrer"&gt;in this Gist link&lt;/a&gt;. We put that code into the same &lt;code&gt;Formula&lt;/code&gt; folder in the repo and named it &lt;code&gt;custom_release_strategy.rb&lt;/code&gt;. That file is pointed to in the &lt;code&gt;custom_require&lt;/code&gt; field in the &lt;code&gt;brews&lt;/code&gt; section and automatically includes it in the formula file so Homebrew knows how to download the packages.&lt;/p&gt;

&lt;p&gt;Hope that helps anyone who wants to build their own CLI tool and ship it via Homebrew!&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Helpful Links:
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://hackernoon.com/building-homebrew-taps-for-private-github-repos" rel="noopener noreferrer"&gt;https://hackernoon.com/building-homebrew-taps-for-private-github-repos&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/jhot/homebrew-and-private-github-repositories-1dfh"&gt;https://dev.to/jhot/homebrew-and-private-github-repositories-1dfh&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>homebrew</category>
      <category>cli</category>
      <category>goreleaser</category>
    </item>
    <item>
      <title>React Context with React Router v6.4</title>
      <dc:creator>Tim Bright</dc:creator>
      <pubDate>Sun, 27 Nov 2022 21:16:51 +0000</pubDate>
      <link>https://dev.to/40percentironman/react-context-with-react-router-v64-44je</link>
      <guid>https://dev.to/40percentironman/react-context-with-react-router-v64-44je</guid>
      <description>&lt;p&gt;I searched for a while to find how to use the Context API with &lt;code&gt;createBrowserRouter&lt;/code&gt;. What I found is that you can use &lt;a href="https://reactrouter.com/en/main/start/concepts#layout-routes" rel="noopener noreferrer"&gt;layout routes&lt;/a&gt; to wrap your components with the provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Outlet } from 'react-router-dom';
export default function ContextLayout() {
  return (
    &amp;lt;ContextProvider&amp;gt;
      &amp;lt;Outlet /&amp;gt;
    &amp;lt;/ContextProvider&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, you can create a &lt;code&gt;RouteObject&lt;/code&gt; without a path that renders that layout route with the children as the components you want to wrap in the provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
  {
    element: &amp;lt;ContextLayout /&amp;gt;,
    children: [
      {
        path: '/some/path',
        element: &amp;lt;SomeContextConsumer /&amp;gt;
      }
    ]
  },
  ...
]);

export default () =&amp;gt; {
  const rootElement = document.getElementById('root');
  const root = createRoot(rootElement!);
  root.render(
    &amp;lt;RouterProvider router={router} /&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! Hopefully this saves someone the time it took me to figure this out.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
