<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Notes from the build - Medium]]></title>
        <description><![CDATA[Architecture, delivery and lessons learned, from ingesting the Twitter firehose for trading firms to white-label banking platforms and agentic engineering. - Medium]]></description>
        <link>https://medium.com/notes-from-the-build?source=rss----29aae463ef65---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Notes from the build - Medium</title>
            <link>https://medium.com/notes-from-the-build?source=rss----29aae463ef65---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 05 Nov 2026 14:10:25 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/notes-from-the-build" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[The Custom Bit Should Be a Hook, Not a Fork: Extensibility in Enterprise Products]]></title>
            <link>https://medium.com/notes-from-the-build/the-custom-bit-should-be-a-hook-not-a-fork-extensibility-in-enterprise-products-8e39476c091b?source=rss----29aae463ef65---4</link>
            <guid isPermaLink="false">https://medium.com/p/8e39476c091b</guid>
            <category><![CDATA[async]]></category>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[configuration]]></category>
            <category><![CDATA[integration-patterns]]></category>
            <dc:creator><![CDATA[Clive Shirley]]></dc:creator>
            <pubDate>Wed, 04 Nov 2026 17:22:41 GMT</pubDate>
            <atom:updated>2026-11-04T17:22:41.044Z</atom:updated>
            <content:encoded><![CDATA[<h4>Enterprise customers always need one more rule, one more integration or one more approval step. What I learned from building a mobility product is that extensibility is a product feature, not a last-minute escape hatch.</h4><p>One of the most reliable facts in enterprise software is that the standard workflow is never quite standard enough.</p><p>A customer wants one extra menu action on the client. Another wants outbound documents scrubbed before they leave the organisation. Another wants filed mail marked with an internal rule. Another needs matter or workspace rules enforced differently for a legal team. Someone else needs a different mail, fax or content provider because that is what their estate already uses, whether that means Exchange, GroupWise, Lotus Notes or something equally entrenched.</p><p>I saw this very clearly in the WorkSite mobility product I eventually sold to Interwoven. Looking back, one of the best product decisions we made was not any one feature. It was treating extensibility as part of the design, rather than as an awkward concession after the first large customer asked for changes.</p><p>This post covers what that taught me, and why <strong>the custom bit should live in a hook, not in a fork of the product</strong>.</p><h4>Enterprise products do not stay generic for long</h4><p>A team can persuade itself that version one should stay clean and opinionated, and that special cases can wait. That can work for consumer products. It rarely works for enterprise software.</p><p>Enterprise customers already have policy, tooling, compliance rules and operational habits before your product arrives. They do not replace all of that to match your neat default flow. They ask where your product can bend.</p><p>That does not mean every request is sensible, or that every request should be accepted. It means the product needs a clear answer to three questions:</p><ul><li>Which parts of the workflow are invariant?</li><li>Which parts are likely to vary by customer or deployment?</li><li>How can that variation happen without turning upgrades into a rewrite?</li></ul><p>If you cannot answer those questions, the next large deal will answer them for you, usually by asking for a branch that never rejoins the main line.</p><h4>Forks solve the short-term problem and create the long-term one</h4><p>At first glance, a customer fork looks pragmatic. You duplicate the code, make the requested changes, ship the deal and promise yourself you will clean it up later.</p><p>The trouble is that a fork moves the cost, rather than removing it. Every future fix, security patch and enhancement now has to be merged, translated or reimplemented. The more successful the product becomes, the worse the economics get.</p><p>That is why I have become suspicious of any design that depends on remembering which customer has which behaviour. If the variation matters enough to sell, support and operate, it matters enough to model explicitly.</p><p>A hook does exactly that. It says: this workflow is stable, but this step is allowed to vary. This is where you plug policy in. This is the contract you must honour. This is what the core system promises not to break casually.</p><p>That is a much healthier promise than “we can patch your copy”.</p><h4>Good extension points start with boundaries</h4><p>The useful question is not “where can we add plugins?” It is “where does local policy naturally belong?”</p><p>In the mobility product, that often meant looking for moments where the core workflow had done its job and customer-specific behaviour could safely take over. After an email had been filed into the right matter or workspace, a post-processing hook made sense. Before a document left the system, a transformation hook made sense. On the client, a menu extension made sense because it added behaviour without redefining navigation.</p><p>Those hooks worked because the surrounding workflow stayed owned by the product. Authentication, queue handling, retries, status transitions, accurate filing to the right matter or workspace, and the main integration path remained centralised. The extension saw a narrow contract, not the whole internals of the system.</p><p>That is the balance to aim for. If a hook is too narrow, nobody can do anything useful with it. If it is too wide, you have exported your internal architecture and made it someone else’s dependency.</p><h4>Configuration is part of the architecture</h4><p>One lesson I learned slowly is that extensibility is not only about code. It is also about selection.</p><p>If a system can talk to different providers, choose different handlers or enable different policies per deployment, that choice has to live somewhere durable and legible. In practice, that usually means configuration.</p><p>I do not mean magical configuration that can express an entire programme in XML and becomes impossible to reason about. I mean a simpler rule: put stable choices in configuration, and put variable business logic behind explicit interfaces.</p><p>That split matters because it changes the operational model.</p><ul><li>A new provider can be selected without editing core code.</li><li>A deployment-specific rule can be added without cloning the workflow.</li><li>An operator can inspect how a system is wired without reading the source.</li></ul><p>Configuration is often treated as secondary to architecture diagrams. In integration-heavy products, it <em>is</em> architecture. It tells you which parts are meant to move and which parts are meant to stay put.</p><h4>Extensibility needs operational discipline</h4><p>There is a trap here. Once a product has hooks, every new requirement looks like one more extension point.</p><p>That is not always right. Every hook becomes part of the support burden. You have to version it, document it, test it, and think about what happens when an extension fails in production.</p><p>So I prefer a stricter standard now:</p><ul><li>add a hook only where multiple customers are likely to vary in the same place</li><li>make the contract explicit and narrow</li><li>define what happens when the hook fails</li><li>keep extension code out of the control flow that maintains system integrity</li></ul><p>That last point matters most. If an extension decides whether the queue can advance, whether a user can recover a failed item, or whether core state remains consistent, you no longer have an extension. You have a second product living inside the first one.</p><p>Good extension models preserve the recoverability of the core system.</p><h4>The architecture benefit is not only customer flexibility</h4><p>There is another reason to invest in this early: the architecture gets cleaner for the team as well.</p><p>Once you start asking “is this core behaviour or local policy?”, design decisions become easier. Teams stop hiding customer-specific rules in random parts of the codebase. You can see which modules should be generic, which should adapt an external dependency, and which should exist only as examples or samples.</p><p>That makes testing easier too. The core workflow can be tested for invariants. Extensions can be tested against a contract. Support teams get a clearer picture of whether a fault lives in the product, the deployment, or a custom component.</p><p>It also improves product management. If a requested change can fit an existing hook, it is a deployment concern. If it needs a new hook, that is a product decision. If it needs a fork, something in the architecture has already gone wrong.</p><h4>What I would do the same now</h4><p>I would still design extension points early in any enterprise product that sits between several systems and several customers.</p><p>I would still prefer adapters and hooks to customer branches. I would still separate invariant workflow from local policy. I would still treat configuration as first-class design. And I would be even firmer about documenting failure behaviour, because extensions are easy to sell and much harder to operate.</p><p>Modern stacks change the mechanics, not the principle. Today the hook might be a webhook, an event consumer, a policy engine or a small internal service rather than a loaded assembly. The question is unchanged: can the customer-specific behaviour live behind a stable contract, while the core product remains upgradeable?</p><p>If the answer is yes, you have an extensible product. If the answer is no, you have a future maintenance bill.</p><h4>The general lesson</h4><p>Enterprise products need to bend without splitting. <strong>Put variation behind a deliberate hook, keep the main workflow invariant, and never make a fork your customisation model.</strong> The short-term convenience of a branch is rarely worth the long-term drag on every release that follows.</p><p><em>Originally published at </em><a href="https://www.shirleyconsulting.co.uk/architecture/2026/11/04/the-custom-bit-should-be-a-hook-not-a-fork.html"><em>www.shirleyconsulting.co.uk/architecture/2026/11/04/the-custom-bit-should-be-a-hook-not-a-fork.html</em></a><em> on 4 November 2026.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8e39476c091b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/notes-from-the-build/the-custom-bit-should-be-a-hook-not-a-fork-extensibility-in-enterprise-products-8e39476c091b">The Custom Bit Should Be a Hook, Not a Fork: Extensibility in Enterprise Products</a> was originally published in <a href="https://medium.com/notes-from-the-build">Notes from the build</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Mobile Was Never the Hard Part: Enterprise Mobility Was Integration]]></title>
            <link>https://medium.com/notes-from-the-build/mobile-was-never-the-hard-part-enterprise-mobility-was-integration-1cb0705086df?source=rss----29aae463ef65---4</link>
            <guid isPermaLink="false">https://medium.com/p/1cb0705086df</guid>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[async]]></category>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[mobile]]></category>
            <category><![CDATA[integration-patterns]]></category>
            <dc:creator><![CDATA[Clive Shirley]]></dc:creator>
            <pubDate>Fri, 30 Oct 2026 12:45:07 GMT</pubDate>
            <atom:updated>2026-10-30T12:45:07.641Z</atom:updated>
            <content:encoded><![CDATA[<h4>I once sold a WorkSite mobility product to Interwoven. Looking back, the interesting part was not the handset UI but the server-side integration, queuing and extension points that made mobile workflows dependable.</h4><p>When people hear “enterprise mobility” now, they tend to picture responsive web apps, iPhones, push notifications and cloud APIs. That is not the world we were building for.</p><p>The mobility product I sold to Interwoven was aimed at WorkSite users who needed documents and email workflows away from their desks. The client side mattered, but the older I get the less I think the handset was the clever bit. The hard part was making awkward enterprise systems behave well enough, and predictably enough, that a mobile user could trust them.</p><p>This post covers what that product taught me, and why <strong>mobile software often succeeds or fails on the server-side seams that users never see</strong>.</p><h4>The device was the visible part, not the difficult part</h4><p>The visible feature list sounded straightforward. Browse folders. Search for documents. Manage matters and workspaces. View profiles. Open or download files. Email, print or fax a document. File a message back into WorkSite.</p><p>On the device, those capabilities showed up as menus, forms, lists and document views. In that era, that often meant BlackBerry clients and small, constrained screens. Everything had to be explicit. You could not assume a wide display, rich browser support or a patient network connection.</p><p>It was not a document viewer with a few shortcuts bolted on. The client and server together supported proper matter and workspace workflows, including viewing documents and filing email back to the right place in WorkSite. In legal environments, it was not enough to move content around. The system had to preserve where correspondence belonged.</p><p>Those constraints forced discipline. You kept flows short, avoided unnecessary round trips, and decided quickly which operations should feel immediate and which should become background work.</p><p>Data costs mattered as well. If a device downloaded a document over the air and then sent that same document back out as an email attachment, the user had effectively paid for the transfer twice. That changed the design.</p><p>That is also why wire encryption and compression mattered so much. Encryption protected sensitive legal documents and correspondence. Compression improved speed on poor links and cut data usage at the same time. Security, performance and cost reduction were all bound up together.</p><p>The difficult part sat behind the client. Every simple user action crossed at least one boundary into a system we did not control.</p><h4>Every feature was really an integration problem</h4><p>A user saw a button labelled email or file. We saw a chain of dependencies.</p><p>To make that button work, we had to bridge a mobile client, the WorkSite server, mail systems such as Exchange, GroupWise and Lotus Notes, rendering services, print services, fax providers and a set of Windows processes that could run unattended for long periods. Each system had its own API style, its own failure modes and its own operational habits.</p><p>The real unit of work was often not a file but a matter or workspace context. A useful mobile workflow had to preserve where a document came from, where a message should be filed, and how a piece of correspondence related to a case. That is why legal mobility was harder than generic mobile content access.</p><p>That changes how you think about product design. The question stops being “can we add this feature?” and becomes “which systems need to co-operate, what can fail, and where do we recover?”</p><p>That is why I still like provider-style architectures for integration-heavy products. We hid vendor-specific detail behind internal abstractions because enterprise products live longer than any one SDK. If you tie the whole product directly to a mail API, a content API or a fax API, every external change becomes an internal rewrite.</p><p>That lesson has aged well. Today it might be SaaS APIs, webhooks and OAuth scopes instead of COM objects and Windows services, but the design pressure is the same.</p><p>A simple way to picture the product is as a small client sitting in front of a large integration surface:</p><figure><img alt="flowchart-v2 diagram" src="https://cdn-images-1.medium.com/proxy/1*N4jczOx0x1nQd8eyi5AgLw.png" /></figure><h4>Asynchronous work was product design, not plumbing</h4><p>The biggest architectural shift was realising that many actions should not be synchronous at all.</p><p>If someone wants to send a document by email, file a message into WorkSite, or route a document to print or fax, there is little value in making the handset wait for every back-end step to complete. What the user needs is confidence that the request was accepted, that it is progressing, and that failure will be visible rather than silent.</p><p>One of the clearest examples was document email from the BlackBerry client. Doing it naively would have meant downloading the document to the handset and then sending it back out again as an attachment, which doubled the mobile data cost. To avoid that, I extended BlackBerry Enterprise Server so it could connect directly to WorkSite. The client inserted a specialised mail header, and the server-side integration used that to fetch and attach the document without sending the binary over the mobile network twice.</p><p>That was not only about cost. The integration was aimed at legal correspondence, where messages needed to stay tied to the right matter or case. Server-side handling gave us a better path to accurate filing and more dependable correspondence records than a purely device-led workflow.</p><figure><img alt="Table:  · Device-led email · BES and WorkSite email" src="https://cdn-images-1.medium.com/proxy/1*L8xpGN8oUfcO25CszEN8xA.png" /></figure><p>So we leaned on queued work. That gave us room for retries, logging, status reporting and cleanup policies. It also let the front end stay small. A mobile client could submit intent and poll for outcome, rather than carrying the complexity of every downstream operation.</p><p>A document email flow looked more like this than like a direct handset-to-mail-server call:</p><figure><img alt="sequence diagram" src="https://cdn-images-1.medium.com/proxy/1*YhdSZYERpFyjkuFQsWmp9g.png" /></figure><p>This mattered more than any client-side flourish. A slightly plain interface is forgivable. A button that sometimes works, sometimes hangs and sometimes loses work is not.</p><p>I still see teams treat queues as implementation detail. In systems like this, they are part of the user experience. <strong>Reliability is a feature, even when it looks like background plumbing.</strong></p><h4>Internal tools tell the truth about a product</h4><p>Years later, one of the most interesting things in the old repositories is not the main application code. It is the test harnesses, crash tools, Exchange diagnostics and helper utilities around it.</p><p>Those tools show what the team actually struggled with: which failures were common enough to deserve their own utility, which dependencies were awkward enough to need a reproduction app, and which administrative tasks were painful enough to automate.</p><p>I have become much more respectful of that kind of code over time. In integration-heavy products, internal tools are often where operational knowledge becomes durable. If a system needs bespoke tools to observe it, debug it or recover it, that is not necessarily a sign of poor engineering. Sometimes it is a sign that the team understood the real work well enough to support it.</p><h4>What I would carry forward now</h4><p>If I were building the same kind of product today, the stack would be different. The architectural instincts would not.</p><p>I would still keep the client thin. I would still isolate external systems behind adapters and push long-running work into queues with explicit status. I would also spend even more time on observability, because complex integrations fail in ways that only become obvious once the product is in production.</p><p>What I would be less likely to do now is believe that a polished demo proves the hard part is done. In enterprise software, demos reward the visible path. Production rewards the recovery path.</p><h4>The general lesson</h4><p>When software sits between several other systems, the user interface is only the tip of the problem. <strong>The real product is the set of boundaries you make dependable.</strong> If the seams are clear, the background work is resilient and the customisation points are deliberate, the front end can stay simple. If those things are weak, no amount of mobile polish will save it.</p><p><em>Originally published at </em><a href="https://www.shirleyconsulting.co.uk/engineering/2026/10/28/mobile-was-never-the-hard-part.html"><em>www.shirleyconsulting.co.uk/engineering/2026/10/28/mobile-was-never-the-hard-part.html</em></a><em> on 28 October 2026.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1cb0705086df" width="1" height="1" alt=""><hr><p><a href="https://medium.com/notes-from-the-build/mobile-was-never-the-hard-part-enterprise-mobility-was-integration-1cb0705086df">Mobile Was Never the Hard Part: Enterprise Mobility Was Integration</a> was originally published in <a href="https://medium.com/notes-from-the-build">Notes from the build</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Platform Limits Shape Products More Than Ideas: Lessons from a Pre-AirTag Child Tracker]]></title>
            <link>https://medium.com/notes-from-the-build/platform-limits-shape-products-more-than-ideas-lessons-from-a-pre-airtag-child-tracker-1b7fd3f52724?source=rss----29aae463ef65---4</link>
            <guid isPermaLink="false">https://medium.com/p/1b7fd3f52724</guid>
            <category><![CDATA[cost]]></category>
            <category><![CDATA[mobile]]></category>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[privacy]]></category>
            <dc:creator><![CDATA[Clive Shirley]]></dc:creator>
            <pubDate>Fri, 30 Oct 2026 12:45:03 GMT</pubDate>
            <atom:updated>2026-10-30T12:45:03.587Z</atom:updated>
            <content:encoded><![CDATA[<h4>Before AirTag, we built a prototype child-tracking system around Bluetooth tokens, an iPhone app, shopping centre receivers, and a Rails backend. Looking back, the strongest lesson was that platform rules, hardware economics, and privacy constraints mattered more than the code.</h4><p>Before AirTag, before Find My accessories, and before Bluetooth Low Energy turned proximity into a mainstream product primitive, I worked on a child-tracking idea for shopping trips.</p><p>The pitch was simple enough to explain in one sentence. Put a small Bluetooth token into a pouch in a child’s shoe, link it to an iPhone app, and let parents know where their child was if they wandered off in a shopping centre.</p><p>This was not only a sketch on a whiteboard. For the proof of concept, I repurposed pre-existing Bluetooth headphone chips to build rudimentary tracking devices and tested them with my own children while we were on holiday. That was enough to validate some basics about signal, handling, and the shape of the user experience, even if it was still far from a production-ready product.</p><p>We also had iPhone code, server code, scanning tools, and conversations with shopping centres about augmenting their Wi-Fi estate with Bluetooth receivers so the system would not depend only on nearby iPhones acting as repeaters. I was also accepted into Apple’s MFi programme, which felt like a meaningful step forward at the time.</p><p><strong>The real lesson was that product viability was shaped less by code than by platform rules, hardware economics, and privacy risk.</strong></p><h4>What we actually built</h4><p>Revisiting the code now is a useful reminder that this was a real prototype with real moving parts.</p><p>The iPhone side was Objective-C. It used Core Location, local Core Data models for children and tracking devices, and Apple Push Notification support. The server side was a Rails 2.3.5 application with endpoints for mobile devices, mobile locations, and notifications. There was also Bluetooth monitoring code on macOS, plus an older iPhone codebase that experimented with device discovery and peer communication.</p><p>In broad terms, the system looked like this:</p><figure><img alt="flowchart-v2 diagram" src="https://cdn-images-1.medium.com/proxy/1*aCSu-KSS2AT1uHOIMQyFtA.png" /></figure><p>The code still shows the structure clearly:</p><ul><li>the app modelled children as local records and linked them to tracking devices</li><li>the app collected location updates and registered for push notifications</li><li>the server stored mobile devices, location sightings, and notification tokens</li><li>the monitor tools continuously scanned for Bluetooth devices</li><li>the broader architecture allowed for both handset-based detection and fixed receiver infrastructure</li></ul><p>None of that was especially elegant, but it was coherent. It was enough to prove there was a system here, not only an idea.</p><h4>How we proved the idea to ourselves</h4><p>One reason I still find this project interesting is that we did not start with custom hardware. For the proof of concept, I used pre-existing Bluetooth headphone chips to assemble rudimentary tracking devices and tried them out with my own children while we were on holiday.</p><p>That sounds slightly improvised because it was. The point was not to prove manufacturability. It was to answer earlier questions more cheaply:</p><ul><li>could we detect something reliably enough to be useful?</li><li>could a small token fit naturally into the routine?</li><li>what would a parent-facing experience need to show?</li><li>where did the idea break down in ordinary movement through public spaces?</li></ul><p>That sort of test is often enough to separate an interesting architecture problem from a plausible product. In our case, it did both. It suggested there was something there technically, and it also pushed the commercial and privacy questions much closer to the front.</p><h4>What we thought the hard part would be</h4><p>At the start, I would probably have said the hardest part was software integration.</p><p>Could the phone discover the token reliably enough? Could we keep location reasonably fresh? Could we push alerts quickly enough? Could the backend correlate a token, a child, a parent account, and a last known position?</p><p>Those were real questions, but they were not the decisive ones.</p><p>The iPhone app was old-fashioned, but workable. The Rails backend was hardly elegant, but it had the right primitives. The architecture even had a sensible next step: if handsets could not do enough on their own, fixed receivers in shopping centres could observe nearby tokens and send sightings back to our servers.</p><p>That venue-assisted version looked more like this:</p><figure><img alt="flowchart-v2 diagram" src="https://cdn-images-1.medium.com/proxy/1*GM8R_pmBSTzRjCnVml5VRg.png" /></figure><p>That was an important change in shape. We were no longer relying purely on iPhones acting as repeaters. We were trying to use the shopping centre’s own physical estate to improve coverage and reliability.</p><p>In product terms, that was the right instinct. It also exposed the real dependency more quickly.</p><h4>The real dependency was hardware economics</h4><p>One of the main tenets of the idea was that the token had to be cheap enough to disappear into the retail flow. Not cheap in the vague sense that founders often promise to optimise later. Cheap in a way that changed what category of product this was.</p><p>Our target was under $5 per token.</p><p>That figure was not arbitrary. The proof-of-concept hardware was good enough to test the idea, but the intended product was never “ship modified headphone electronics”. It was a cheap insertable token that could be bundled into children’s shoes at retail scale.</p><p>That threshold shaped almost everything:</p><ul><li>it needed to be cheap enough to bundle with shoes</li><li>cheap enough to lose without drama</li><li>cheap enough to replace without a support saga</li><li>cheap enough that a brand partner could treat it as an accessory, not a premium electronic device</li><li>cheap enough that a parent would not handle it like something precious</li></ul><p><strong>If the token could not be treated as near-disposable, the product premise started to collapse.</strong></p><p>This is where the MFi story became more interesting than the software. Getting accepted into Apple’s MFi programme felt like progress, and it was. It reduced one kind of uncertainty. But it also made another constraint impossible to ignore.</p><p>The licensed Bluetooth encryption chips we needed were cost-prohibitive for the unit economics we were aiming at. Once the accessory path required hardware with that cost profile, the rest of the model stopped fitting together commercially.</p><p>This was not a matter of shaving a few points off a margin model. It changed the category of the thing we were building.</p><p>A sub-$5 insertable token can be treated almost like packaging. A materially more expensive device becomes a gadget. The retail conversation changes. The replacement conversation changes. The support conversation changes. Parent expectations change. The whole model shifts from “simple safety add-on” to “consumer electronics programme”.</p><p>Software could not rescue that.</p><h4>Platform limits were broader than APIs</h4><p>The cost problem was the decisive one, but it was not the only platform constraint.</p><p>This was a pre-BLE world. The modern assumptions people bring to proximity products did not hold. Background behaviour was more limited, scanning and discovery were clumsier, and some of the Bluetooth hooks available to us were not on the happy path for App Store software.</p><p>That showed up in the code too. Parts of the older iPhone work experimented with private Bluetooth frameworks. Other parts used GameKit peer discovery. The server handled device registration, location updates, and push notifications, but it all sat on top of an ecosystem that had not yet settled into a clean product layer for this kind of use case.</p><p>In other words, the platform was telling us something important: this was possible to prototype, but not yet natural to ship.</p><p>That distinction matters. Engineers can often build around a gap. Product teams cannot always build a business around one.</p><h4>The privacy problems were real, and easier to understate then than now</h4><p>There is another part of this idea that deserves much more weight now than I would have given it at the time: privacy.</p><p>Tracking children in a retail environment raises hard questions even if the intent is plainly protective. In fact, clear intent can make teams too casual about the secondary risks.</p><p>The system implied a lot of sensitive joins:</p><ul><li>child identity</li><li>parent identity</li><li>token identity</li><li>venue presence</li><li>approximate or inferred location</li><li>time history</li><li>notification events</li></ul><p>Even the holiday proof-of-concept testing brought that into focus. Testing with my own children kept the exercise bounded and practical, but it also made the ethical weight of the idea harder to ignore. A system like this does not stay in the realm of clever radio engineering for long. Very quickly it becomes a question of who can observe whom, under what conditions, for how long, and with what safeguards.</p><p>A venue-assisted version made those questions even sharper. If shopping centres hosted Bluetooth receivers and fed sightings to our servers, then the data boundary was no longer only between parent and app. It potentially involved operators, partners, retention rules, access controls, and future commercial pressure to reuse the data for things it was never meant to support.</p><p>The awkward truth is that you do not have to build a sinister system for it to become a risky one. A benign “help me find my child in this shopping centre” flow can drift into something else if the data lasts too long, can be queried too freely, or gets repurposed by venue operators, commercial partners, or a future product team with weaker instincts.</p><p>If I were approaching this problem now, I would put privacy architecture much earlier in the sequence:</p><ul><li>minimise identifiers from the start</li><li>avoid collecting more history than the use case needs</li><li>separate ephemeral safety events from long-lived account data</li><li>make venue data boundaries explicit</li><li>design for deletion, not only capture</li><li>assume misuse cases will exist, not that good intentions prevent them</li></ul><p>Back then, I saw privacy as an important design consideration. Now I would treat it as a first-order architecture constraint.</p><h4>What I took from revisiting the code</h4><p>Looking back, I do not think the prototype failed because we lacked technical imagination. If anything, we had plenty. We had an app, a server, scanning tools, an MFi path, working proof-of-concept hardware, and discussions with shopping centres about supplementing Wi-Fi with Bluetooth receivers.</p><p>What we did not have was a fit between product premise and platform reality.</p><p>The premise required:</p><ul><li>phone platform support that was good enough</li><li>accessory rules that were commercially survivable</li><li>hardware cheap enough to be almost disposable</li><li>partner infrastructure that shopping centres would actually deploy</li><li>privacy posture strong enough for a sensitive use case</li></ul><p>Any one of those could have caused trouble. Together, they defined the outcome more than the quality of any individual Objective-C class or Rails controller.</p><p>That is what makes the code interesting to me now. It is not evidence that we were one sprint away from an early AirTag. It is evidence that <strong>you can be directionally right about a product and still be years too early on the surrounding ecosystem</strong>.</p><h4>The general lesson</h4><p><strong>When a product depends on platform-controlled hardware, the real architecture includes licensing, unit economics, partner incentives, and privacy boundaries, not only APIs and servers.</strong> We could make the software plausible. What we could not do was make the whole system fit the commercial and ethical constraints that actually mattered.</p><p><em>Originally published at </em><a href="https://www.shirleyconsulting.co.uk/architecture/2026/10/21/platform-limits-shape-products-more-than-ideas.html"><em>www.shirleyconsulting.co.uk/architecture/2026/10/21/platform-limits-shape-products-more-than-ideas.html</em></a><em> on 21 October 2026.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1b7fd3f52724" width="1" height="1" alt=""><hr><p><a href="https://medium.com/notes-from-the-build/platform-limits-shape-products-more-than-ideas-lessons-from-a-pre-airtag-child-tracker-1b7fd3f52724">Platform Limits Shape Products More Than Ideas: Lessons from a Pre-AirTag Child Tracker</a> was originally published in <a href="https://medium.com/notes-from-the-build">Notes from the build</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Modelling Batch Pharmacy Claims in FHIR]]></title>
            <link>https://medium.com/notes-from-the-build/modelling-batch-pharmacy-claims-in-fhir-b6a4960efdb0?source=rss----29aae463ef65---4</link>
            <guid isPermaLink="false">https://medium.com/p/b6a4960efdb0</guid>
            <category><![CDATA[fhir-hl7]]></category>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[ncpdp]]></category>
            <category><![CDATA[x12-835]]></category>
            <category><![CDATA[dynamodb]]></category>
            <dc:creator><![CDATA[Clive Shirley]]></dc:creator>
            <pubDate>Sat, 24 Oct 2026 12:19:16 GMT</pubDate>
            <atom:updated>2026-10-24T12:19:16.102Z</atom:updated>
            <content:encoded><![CDATA[<h4>FHIR has no ClaimBatch resource, and NCPDP batch files don’t care about REST. How we composed Task, List, Claim, ClaimResponse, ExplanationOfBenefit and PaymentReconciliation into a claims lifecycle with at-least-once delivery that’s safe to retry.</h4><p>US pharmacy claims are an old world meeting a new one. The pharmacy benefit manager (PBM) wants a nightly batch file in the NCPDP format, fixed-width and positional. Your platform stores everything as HL7 FHIR resources behind a REST API. Payment arrives even later, as an X12 835 remittance.</p><p>This post describes how we modelled that lifecycle natively in FHIR, and the operational store underneath that makes it safe to retry. Most of the lessons apply to any batch integration with a partner that correlates on your identifiers.</p><h4>The lifecycle</h4><figure><img alt="flowchart-v2 diagram" src="https://cdn-images-1.medium.com/proxy/1*zahmqJNWnCkrP3C5IM4kkg.png" /></figure><h4>FHIR has no batch resource, so compose one</h4><p>A nightly submission is modelled by composing standard resources, each with a single responsibility:</p><figure><img alt="Table: Concern · Resource · Role" src="https://cdn-images-1.medium.com/proxy/1*E-DY2_m6uComVcMFduTKNg.png" /></figure><p>One trap to avoid: <strong>don’t use </strong><strong>Bundle of type </strong><strong>batch as the batch tracker.</strong> That type exists for batching REST requests to a FHIR server, not for domain batches sent to a partner. A collection bundle is fine as an optional audit snapshot of the payload.</p><h4>Don’t inline the manifest</h4><p>The first draft put every claim reference on the batch Task or in one List. That breaks at scale:</p><ul><li>DynamoDB items max out at <strong>400 KB</strong>, which is roughly 2,000–4,000 embedded references.</li><li>Large FHIR List payloads hit practical server limits well before that.</li><li>Deriving a count by loading and counting every entry is slow and fragile.</li></ul><p>Instead:</p><ul><li>In the operational store, the batch header holds <strong>scalars only</strong> (claim count, receipt ID, status, file name, timestamps), with <strong>one child item per claim</strong> under the same partition, listed through an index.</li><li>In FHIR, use <strong>sharded </strong><strong>List pages</strong> and/or an <strong>inverse link</strong>: an extension on each Claim pointing to its batch Task, so “claims in batch X” is a search.</li><li>Persist the claim count as a scalar at acknowledgement time.</li><li>Remember that DynamoDB transactions accept at most <strong>25 actions</strong>, so post-acknowledgement writes for large batches become idempotent chunks: header first, then membership rows and status flips in pages.</li></ul><h4>The transaction reference is your idempotency key</h4><p>Each claim in an NCPDP file carries a numeric <strong>transaction reference</strong> (7–12 digits) that <em>you</em> generate and the PBM echoes back in the adjudication response. It’s your correlation ID and your deduplication key, so three rules apply:</p><ol><li><strong>Allocate it when the claim is created (as a draft), not at batch time.</strong> If you mint it at batch time and the batch fails before acknowledgement, a retry mints a <em>new</em> reference and breaks deduplication on the partner’s side.</li><li><strong>Never change it.</strong> The same value goes on every resubmission of that claim.</li><li><strong>Use a central, monotonic numeric allocator</strong>, such as an atomic counter or a database sequence. Don’t use random numbers, and don’t derive it from a resource ID.</li></ol><h4>At-least-once delivery, then state change</h4><p>The nightly job runs in a deliberate order:</p><ol><li><strong>Extract</strong> draft pharmacy claims through an index.</li><li><strong>Translate</strong> them into the fixed-width layout: product to NDC code, identifier to transaction reference, and so on.</li><li><strong>Transmit</strong> the file to the partner’s gateway.</li><li><strong>Only after the partner acknowledges receipt</strong>, write state: create the batch Task (receipt ID, claim count), the List pages and the inverse links, flip each Claim from draft to active, and create a queued ClaimResponse per claim linked back to the claim and the batch.</li></ol><p>If the network drops mid-flight, nothing local has changed. The retry sends the same claims with the same references, and the partner deduplicates. That’s <strong>at-least-once delivery with idempotent receipt</strong>, which is the only honest guarantee across a file-transfer boundary.</p><h4>Matching responses</h4><p>When the adjudication file arrives, the batch Task moves to adjudicating. Run <strong>integrity checks before processing any records</strong>. The key one is that the number of adjudication records <strong>must equal the claim count persisted at submission</strong>. If they differ, process nothing and investigate. Only when the counts match does each row get matched to its claim by transaction reference (through an index keyed on the reference), completing the ClaimResponse with the partner’s disposition and any structured NCPDP reject codes (such as “product not covered”).</p><p>Settlement arrives later as an 835. A PaymentReconciliation is created for the remittance, keyed on the payment trace number, with one ExplanationOfBenefit per claim pointing back to the Claim and its ClaimResponse. The batch Task reaches reconciled when every claim in it has settled.</p><p>Because everything is standard FHIR, operational dashboards are plain searches: in-flight claims (ClaimResponse?outcome=queued), an ageing report of adjudicated claims with no payout yet, and tracing the claims behind a given payment.</p><h4>Two stores, clear roles</h4><p>The <strong>operational store</strong> (single-table DynamoDB) handles the hot path: reference allocation, claim lifecycle, batch membership and response matching, with indexes for “claim by reference”, “claims in batch” and “drafts to extract”. The <strong>FHIR store</strong> is the <strong>canonical API and audit view</strong>, synchronised at defined lifecycle boundaries. Keeping the two roles explicit avoids the temptation to run a nightly batch job through a REST API one resource at a time.</p><h4>The general lessons</h4><ol><li><strong>Compose standard resources</strong> before inventing custom ones, and give each a single job.</li><li><strong>Never inline unbounded lists</strong> in a header item or resource. Use child rows and inverse links.</li><li><strong>Generate partner correlation IDs early and make them immutable.</strong> They’re your retry safety.</li><li><strong>Transmit first, then change state</strong>, and let the partner’s deduplication absorb retries.</li></ol><p><em>Originally published at </em><a href="https://www.shirleyconsulting.co.uk/architecture/2026/10/07/modelling-pharmacy-claims-in-fhir.html"><em>www.shirleyconsulting.co.uk/architecture/2026/10/07/modelling-pharmacy-claims-in-fhir.html</em></a><em> on 7 October 2026.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b6a4960efdb0" width="1" height="1" alt=""><hr><p><a href="https://medium.com/notes-from-the-build/modelling-batch-pharmacy-claims-in-fhir-b6a4960efdb0">Modelling Batch Pharmacy Claims in FHIR</a> was originally published in <a href="https://medium.com/notes-from-the-build">Notes from the build</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Skills Are Commands, Agents Are Specialists: How My Pi Day Is Organised]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/notes-from-the-build/skills-are-commands-agents-are-specialists-how-my-pi-day-is-organised-31e5ddde168e?source=rss----29aae463ef65---4"><img src="https://cdn-images-1.medium.com/max/600/1*ygwqEcclC3P4ErHWw4QvKg.png" width="600"></a></p><p class="medium-feed-link"><a href="https://medium.com/notes-from-the-build/skills-are-commands-agents-are-specialists-how-my-pi-day-is-organised-31e5ddde168e?source=rss----29aae463ef65---4">Continue reading on Notes from the build »</a></p></div>]]></description>
            <link>https://medium.com/notes-from-the-build/skills-are-commands-agents-are-specialists-how-my-pi-day-is-organised-31e5ddde168e?source=rss----29aae463ef65---4</link>
            <guid isPermaLink="false">https://medium.com/p/31e5ddde168e</guid>
            <category><![CDATA[pi]]></category>
            <category><![CDATA[developer-experience]]></category>
            <category><![CDATA[agentic-engineering]]></category>
            <category><![CDATA[tooling]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Clive Shirley]]></dc:creator>
            <pubDate>Tue, 20 Oct 2026 14:42:12 GMT</pubDate>
            <atom:updated>2026-10-20T14:42:12.630Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Right-Size Every Token: The Pi Utilities That Keep an Agent Workflow Affordable]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/notes-from-the-build/right-size-every-token-the-pi-utilities-that-keep-an-agent-workflow-affordable-5245e7eccf36?source=rss----29aae463ef65---4"><img src="https://cdn-images-1.medium.com/max/600/1*7jzQ2kkV7KTWfTbps5Wy6w.png" width="600"></a></p><p class="medium-feed-link"><a href="https://medium.com/notes-from-the-build/right-size-every-token-the-pi-utilities-that-keep-an-agent-workflow-affordable-5245e7eccf36?source=rss----29aae463ef65---4">Continue reading on Notes from the build »</a></p></div>]]></description>
            <link>https://medium.com/notes-from-the-build/right-size-every-token-the-pi-utilities-that-keep-an-agent-workflow-affordable-5245e7eccf36?source=rss----29aae463ef65---4</link>
            <guid isPermaLink="false">https://medium.com/p/5245e7eccf36</guid>
            <category><![CDATA[developer-experience]]></category>
            <category><![CDATA[agentic-engineering]]></category>
            <category><![CDATA[tooling]]></category>
            <category><![CDATA[pi]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Clive Shirley]]></dc:creator>
            <pubDate>Mon, 12 Oct 2026 11:59:08 GMT</pubDate>
            <atom:updated>2026-10-12T11:59:08.619Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Good Engineering Habits Age Better Than Technology: Rereading My Final-Year Project]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/notes-from-the-build/good-engineering-habits-age-better-than-technology-rereading-my-final-year-project-07d4e9592a47?source=rss----29aae463ef65---4"><img src="https://cdn-images-1.medium.com/max/600/1*6F38erM5JXkqyvlbq-_Myg.png" width="600"></a></p><p class="medium-feed-link"><a href="https://medium.com/notes-from-the-build/good-engineering-habits-age-better-than-technology-rereading-my-final-year-project-07d4e9592a47?source=rss----29aae463ef65---4">Continue reading on Notes from the build »</a></p></div>]]></description>
            <link>https://medium.com/notes-from-the-build/good-engineering-habits-age-better-than-technology-rereading-my-final-year-project-07d4e9592a47?source=rss----29aae463ef65---4</link>
            <guid isPermaLink="false">https://medium.com/p/07d4e9592a47</guid>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[decision-making]]></category>
            <category><![CDATA[specifications]]></category>
            <category><![CDATA[development]]></category>
            <dc:creator><![CDATA[Clive Shirley]]></dc:creator>
            <pubDate>Mon, 12 Oct 2026 11:58:52 GMT</pubDate>
            <atom:updated>2026-10-12T11:58:52.851Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[The Author Is Compromised: Independent Review and an Honest List of Gaps]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/notes-from-the-build/the-author-is-compromised-independent-review-and-an-honest-list-of-gaps-62a14ff60daf?source=rss----29aae463ef65---4"><img src="https://cdn-images-1.medium.com/max/600/1*iJQgudbFMJud7ZF35j7V5Q.png" width="600"></a></p><p class="medium-feed-link"><a href="https://medium.com/notes-from-the-build/the-author-is-compromised-independent-review-and-an-honest-list-of-gaps-62a14ff60daf?source=rss----29aae463ef65---4">Continue reading on Notes from the build »</a></p></div>]]></description>
            <link>https://medium.com/notes-from-the-build/the-author-is-compromised-independent-review-and-an-honest-list-of-gaps-62a14ff60daf?source=rss----29aae463ef65---4</link>
            <guid isPermaLink="false">https://medium.com/p/62a14ff60daf</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[pi]]></category>
            <category><![CDATA[agentic-engineering]]></category>
            <category><![CDATA[accord]]></category>
            <dc:creator><![CDATA[Clive Shirley]]></dc:creator>
            <pubDate>Wed, 07 Oct 2026 13:10:35 GMT</pubDate>
            <atom:updated>2026-10-07T13:10:35.195Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Transparent Hooks Beat Explicit Orchestration: Inside My Pi Dev Harness]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/notes-from-the-build/transparent-hooks-beat-explicit-orchestration-inside-my-pi-dev-harness-df582ea0dfbe?source=rss----29aae463ef65---4"><img src="https://cdn-images-1.medium.com/max/600/1*BHwDJ-aQL2t1GgIx0KAjag.png" width="600"></a></p><p class="medium-feed-link"><a href="https://medium.com/notes-from-the-build/transparent-hooks-beat-explicit-orchestration-inside-my-pi-dev-harness-df582ea0dfbe?source=rss----29aae463ef65---4">Continue reading on Notes from the build »</a></p></div>]]></description>
            <link>https://medium.com/notes-from-the-build/transparent-hooks-beat-explicit-orchestration-inside-my-pi-dev-harness-df582ea0dfbe?source=rss----29aae463ef65---4</link>
            <guid isPermaLink="false">https://medium.com/p/df582ea0dfbe</guid>
            <category><![CDATA[developer-experience]]></category>
            <category><![CDATA[agentic-engineering]]></category>
            <category><![CDATA[tooling]]></category>
            <category><![CDATA[pi]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Clive Shirley]]></dc:creator>
            <pubDate>Wed, 07 Oct 2026 13:10:04 GMT</pubDate>
            <atom:updated>2026-10-07T13:10:04.817Z</atom:updated>
        </item>
        <item>
            <title><![CDATA[Write the Tests Before the Code, Then Review Them Like an Adversary]]></title>
            <description><![CDATA[<div class="medium-feed-item"><p class="medium-feed-image"><a href="https://medium.com/notes-from-the-build/write-the-tests-before-the-code-then-review-them-like-an-adversary-b4822239af38?source=rss----29aae463ef65---4"><img src="https://cdn-images-1.medium.com/max/600/1*3UFVw_8lf9Xy4jp68EdUUA.png" width="600"></a></p><p class="medium-feed-link"><a href="https://medium.com/notes-from-the-build/write-the-tests-before-the-code-then-review-them-like-an-adversary-b4822239af38?source=rss----29aae463ef65---4">Continue reading on Notes from the build »</a></p></div>]]></description>
            <link>https://medium.com/notes-from-the-build/write-the-tests-before-the-code-then-review-them-like-an-adversary-b4822239af38?source=rss----29aae463ef65---4</link>
            <guid isPermaLink="false">https://medium.com/p/b4822239af38</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[pi]]></category>
            <category><![CDATA[agentic-engineering]]></category>
            <category><![CDATA[accord]]></category>
            <dc:creator><![CDATA[Clive Shirley]]></dc:creator>
            <pubDate>Tue, 06 Oct 2026 14:25:04 GMT</pubDate>
            <atom:updated>2026-10-06T14:25:03.996Z</atom:updated>
        </item>
    </channel>
</rss>