<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Codat Engineering Blog</title>
        <link>https://engineering.codat.io/blog</link>
        <description>Engineering stories from the team at Codat.</description>
        <lastBuildDate>Tue, 04 Aug 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[Shrinking the estate: consolidating to go faster]]></title>
            <link>https://engineering.codat.io/blog/shrinking-the-estate</link>
            <guid>https://engineering.codat.io/blog/shrinking-the-estate</guid>
            <pubDate>Tue, 04 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How we took roughly 400 deployable units down to around 160 without customers noticing, and why the number that mattered was the work it freed us to start.]]></description>
            <content:encoded><![CDATA[<p>When Azure deprecated App Service managed certificates, we had to fix it in hundreds of places. One platform change; hundreds of near-identical services, each with its own pipeline, its own secrets, its own tests to break. It cost days and delivered no new value to our customers. The day-to-day friction was real, but the deeper issue was structural: the estate had grown past the capacity we had to maintain it.</p>
<p>That constraint set the effort behind every improvement we wanted to make. Fast local development, reliable testing, a single deployment story: each had to be applied across every service, so the work scaled with the size of the estate rather than the size of the change. The improvements with the most leverage were exactly the ones the architecture made hardest to ship. It wasn't only slowing us down day to day; it was keeping the systematic, compounding improvements out of reach.</p>
<p>So over the following months we shrank it. We took roughly 400 deployable units down to around 160. No customer noticed, because the behaviour was identical on the other side. Internally, the day-to-day friction dropped and the high-leverage improvements we'd deferred became feasible. This is how we did it, and why the number that mattered wasn't the count we deleted but the work it freed us to start.</p>
<p><img decoding="async" loading="lazy" alt="Sankey diagram showing roughly 400 deployable units splitting into around 160 remaining (40%) and around 240 merged or retired (60%)" src="https://engineering.codat.io/assets/images/shrinking-the-estate-sankey-3dc2a7adcbfd3900e5ec22656c504023.png" width="2400" height="1400" class="img_SqHw"></p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_WxEf" id="how-we-got-here">How we got here<a href="https://engineering.codat.io/blog/shrinking-the-estate#how-we-got-here" class="hash-link" aria-label="Direct link to How we got here" title="Direct link to How we got here" translate="no">​</a></h2>
<p>Earlier in Codat's history, we went pretty hard on microservices, tantalised by the prospect of small, independently deployable pieces to let teams move without stepping on each other, and we got the benefits it promised: independent deployment, blast-radius isolation, clear boundaries.</p>
<p>More recently, however, it became increasingly apparent that having so many fine-grained services was slowing us down and holding us back. Every shared library bump became a dozen pull requests. Every certificate rotation became hundreds. A long tail of services that no one had touched in months still had to be patched, observed, and kept alive. We were spending way too much time Keeping The Lights On.</p>
<p>The independence was also partly an illusion. Enough of the services were tightly coupled that we had, in effect, a distributed monolith: nominally separate deployables that still had to be released in a careful, coordinated sequence. A single meaningful change often spanned several repositories and more than one team, and inconsistent deployment patterns meant no two releases felt quite the same. Shipping became an exercise in coordination rather than engineering.</p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_WxEf" id="merge-first-fix-later">Merge first, fix later<a href="https://engineering.codat.io/blog/shrinking-the-estate#merge-first-fix-later" class="hash-link" aria-label="Direct link to Merge first, fix later" title="Direct link to Merge first, fix later" translate="no">​</a></h2>
<p>The goal was simple: rationalise our service domains and drastically reduce the number of deployable units within each, with each domain owned by exactly one team.</p>
<p>The decision that made it achievable was what we chose <em>not</em> to do. Phase one merged services and nothing else. No re-architecting, no consolidating databases, no aligning every service to whatever we now consider best practice. If a service owned three databases after a merge, it kept three databases. Untangling that is a separate data-migration project for another day.</p>
<p>In hindsight, this was a prudent call. Giving in would have turned a bounded job into an open-ended rewrite we might never have finished. Drawing the line at merging the units, and leaving everything else where it was, is what kept the work finite; consistency could come later, on a smaller base.</p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_WxEf" id="approach">Approach<a href="https://engineering.codat.io/blog/shrinking-the-estate#approach" class="hash-link" aria-label="Direct link to Approach" title="Direct link to Approach" translate="no">​</a></h2>
<p>We started by mapping the whole estate to find the natural domain boundaries. This was iterative rather than a grand up-front design: propose a boundary, test it against how the services actually talked to each other, adjust.</p>
<p>Once the boundaries were set, we identified the distinct set of deployable unit types and discovered that nearly every merge reduced to one of five consolidation paths between source type and target type. Windows App Services were retired, not carried forward. WebJobs and in-process Azure Functions became isolated Functions, which run in their own process on any version of .NET and, unlike the in-process model, emit the distributed traces we need to actually see what a background job is doing.</p>
<p>Moving from Windows to Linux surfaced a handful of subtle differences between the platforms: path separators, filename casing (a single mis-cased <code>appsettings.json</code> and the app refuses to start), environment variables that Windows treats as case-insensitive and Linux does not. We catalogued these gotchas along the way to help avoid them on future merges.</p>
<p>Merging two applications also meant merging two dependency injection containers, and reconciling every registration between them. The trick we settled on was to dump both service collections just before building the container and diff them, letting the difference tell us what the combined configuration needed to be, rather than reasoning about it by hand.</p>
<p>We also decided that if a merge target was not already running in Kubernetes, we would migrate the target to k8s first rather than migrating host infrastructure mid-flight. We updated our internal routing setup to ensure deprecated connection strings now routed to the new home of the merged service.</p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_WxEf" id="leveraging-ai-to-consolidate-faster">Leveraging AI to consolidate faster<a href="https://engineering.codat.io/blog/shrinking-the-estate#leveraging-ai-to-consolidate-faster" class="hash-link" aria-label="Direct link to Leveraging AI to consolidate faster" title="Direct link to Leveraging AI to consolidate faster" translate="no">​</a></h2>
<p>What struck us early on was how mechanical most of the work turned out to be: move the code, reconcile the references, fix the build, port the tests, update the Helm charts. It was repetitive and error-prone, and we started to realise it was close to the kind of work modern coding agents are good at, which usefully coincided with us rolling out Claude Code across engineering. Claude performed the merges directly: resolving namespace collisions, untangling conflicting DI registrations, porting unit and integration tests into the target project, and updating charts and compose files.</p>
<p>But being in the driving seat guiding Claude, we were still liable to miss steps or do them in the wrong order, relearning the same lessons. So we used Claude to build a merge tool, incorporating two stages into the merge process. The first stage executed deterministic C# to perform the mechanical steps, including preserving full git history by importing the source as a stack of branches, so <code>git blame</code> still worked across the merged code, and it rewrote namespaces and using-statements to match the target's layout. The second stage generated Claude prompts for aspects of the merge that required judgement, such as consolidating two <code>appsettings.json</code> files and resolving dependency/restore failures.</p>
<p>The part we came to rely on most was that it never touched a file in the target. Any conflict was copied in beside the original under an <code>_Imported</code> or <code>_OLD</code> suffix for a person to reconcile; the build was validated before we called a merge done; and progress was saved after each stage, so an interrupted run could pick up where it stopped.</p>
<p><img decoding="async" loading="lazy" alt="Three-step diagram of AI-powered code consolidation: AI-assisted merges, an AI-built merge tool combining deterministic C# with generated Claude prompts, and conflict resolution with conflicts copied beside the originals for human review" src="https://engineering.codat.io/assets/images/shrinking-the-estate-ai-steps-116cbf46e1806f52b88ffbae2859a134.png" width="2400" height="1260" class="img_SqHw"></p>
<p>We're not going to pretend it was hands-off. It still needed direction. Occasionally it would fixate on a symptom instead of chasing the root cause, and a human had to redirect it. But the balance had tipped away from mechanical toil and toward judgement, which was what we'd been hoping for.</p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_WxEf" id="what-it-unlocks">What it unlocks<a href="https://engineering.codat.io/blog/shrinking-the-estate#what-it-unlocks" class="hash-link" aria-label="Direct link to What it unlocks" title="Direct link to What it unlocks" translate="no">​</a></h2>
<p>We removed hundreds of deployable units and consolidated our estate onto a unified host architecture. Features that used to require a coordinated march across three or four repositories now land in a single pull request. Maintenance is cheaper for the obvious reason that there are fewer moving parts to maintain; we immediately started spending less time Keeping The Lights On. But perhaps the most exciting benefit is that all the improvements we've dreamt of making to move our estate forward suddenly have become way more feasible.</p>
<p>There's some relatively low-hanging fruit such as replacing legacy custom libraries with idiomatic .NET, which we're now chipping away at. But we've immediately started work on something much bigger: we're reducing our number of environments to two, local and production. Stick around if you want to learn more!</p>]]></content:encoded>
            <category>Engineering</category>
        </item>
        <item>
            <title><![CDATA[Why we deleted our micro-frontends]]></title>
            <link>https://engineering.codat.io/blog/why-we-deleted-our-micro-frontends</link>
            <guid>https://engineering.codat.io/blog/why-we-deleted-our-micro-frontends</guid>
            <pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[We split our frontend into eight repositories for autonomy, then paid for it for years. Here's what merging it back into one app actually bought us.]]></description>
            <content:encoded><![CDATA[<p>Codat's frontend used to be a distributed system: a handful of separately built, separately deployed applications stitched together at runtime behind a shell. We've merged all of it back into one application in one repository. It took about four months. Customers never noticed.</p>
<p>Splitting it up was a defensible decision when it was made, in a company that looked different from this one. We paid for it for a long time afterwards. This is what the bill came to.</p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_WxEf" id="what-we-thought-we-were-buying">What we thought we were buying<a href="https://engineering.codat.io/blog/why-we-deleted-our-micro-frontends#what-we-thought-we-were-buying" class="hash-link" aria-label="Direct link to What we thought we were buying" title="Direct link to What we thought we were buying" translate="no">​</a></h2>
<p>The case for splitting a frontend is always an organisational one. Teams get blocked on each other, nobody ships without pain, so you draw lines through the product, hand each team a piece, and describe the result as autonomy.</p>
<p>What we got was narrower than that. Our teams could deploy without asking anyone, which is worth having. What they couldn't do was see each other. And the coordination we'd tried to remove didn't disappear, it moved somewhere more expensive. A merge conflict is cheap, because the two people who caused it fix it in ten minutes. A disagreement about the shape of an event payload, between two teams who own two repositories, takes a Slack thread, a ticket and a fortnight. We swapped a compiler error for a meeting.</p>
<p><img decoding="async" loading="lazy" alt="Seesaw diagram tipped heavily to one side: team autonomy and independent deploys outweighed by per-repo overhead, runtime contracts, hidden debt and the dependency matrix" src="https://engineering.codat.io/assets/images/microfrontends-tradeoff-04d985a292b56f68421f9aade2d1c1ce.png" width="2400" height="1260" class="img_SqHw"></p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_WxEf" id="the-bill">The bill<a href="https://engineering.codat.io/blog/why-we-deleted-our-micro-frontends#the-bill" class="hash-link" aria-label="Direct link to The bill" title="Direct link to The bill" translate="no">​</a></h2>
<p>The biggest line item was dependencies. When each application builds on its own you don't get a dependency tree, you get a matrix: two versions of the framework, four different pins of the design system. A design system that exists at four versions at once is four things sharing a name. The browser pays as well, downloading the same framework code once per slice.</p>
<p><img decoding="async" loading="lazy" alt="Before and after dependency matrix: eight repos each pinning different versions of React, TypeScript, the design system and the router, collapsing into a single repo with one version of each" src="https://engineering.codat.io/assets/images/microfrontends-dependency-matrix-c79209af84e50f9f409d917eb5f63372.png" width="2400" height="1260" class="img_SqHw"></p>
<p>You could watch the language change under you as you moved between repos: a different TypeScript in each, a router a few versions adrift, one slice still running a React that had been end-of-life for almost five years. Nobody decided any of that. It's just what a codebase does when no single place has to agree with itself. The user paid for the split too, not only us: a page load could fan out into seven separate fetches for code, and any one of them failing was a blank screen, because there was no compiler in between to have caught it first.</p>
<p><img decoding="async" loading="lazy" alt="Request waterfall of a page load fanning out into shell.js plus seven remoteEntry.js fetches with one failed, versus a single app.js bundle and a healthy page after the merge" src="https://engineering.codat.io/assets/images/microfrontends-fetch-waterfall-7954668e2db4dde565d59c33fe26d78e.png" width="2400" height="1260" class="img_SqHw"></p>
<p>One tree fixed that. An upgrade or a CVE patch is now a single PR, verified by a single CI run and applied everywhere. The volume of dependency PRs fell by roughly an order of magnitude, though the number matters less than the fact that upgrades stopped being optional. There's nowhere for a stale version to hide.</p>
<p>The second line item was debt. Splitting a system doesn't reduce technical debt, it moves it somewhere you can't see. A repo nobody reads, owned by a team that reorganised twice since, where it sits quite happily until it becomes an incident. In one repository you can grep for it. Codemods work again. So do atomic changes: you can alter a shared component and every caller of it in a single commit, with CI telling you immediately whether you were wrong, instead of publishing a version and waiting months for the last consumer to adopt it.</p>
<p>Our boundaries also got types. Custom events and conventions about what goes in the URL were contracts checked at runtime, which is a polite way of saying our users checked them for us. Merging the repositories turned them into compile-time contracts, and the first week surfaced an embarrassing number of bugs that had been sitting there for ages.</p>
<p>Then there's the overhead. Every micro-frontend carries a repository, a pipeline, a bundler config that rots, a test setup with its own flakiness, a deployment target, an observability config, a dependency tree, and a local dev story that somebody has to keep alive. Multiply that by however many boundaries you drew, and you've built a platform team's worth of undifferentiated work and handed it out, unfunded, to your product engineers. In one repo it gets done once, properly, by whoever cares most about it.</p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_WxEf" id="consistency-and-silos">Consistency, and silos<a href="https://engineering.codat.io/blog/why-we-deleted-our-micro-frontends#consistency-and-silos" class="hash-link" aria-label="Direct link to Consistency, and silos" title="Direct link to Consistency, and silos" translate="no">​</a></h2>
<p>We used to chase consistency with process: a component library, guidelines, a review forum, a fair amount of evangelism. All of that is a sign the architecture is fighting you. You only need governance when the code can't enforce the thing you want.</p>
<p>Now the consistent option is the lazy one. There's one button. You use it because it's there, and writing a second one would be more work, and someone would ask you why in review. The seams a user could see went too. No more full-page reload when you crossed an invisible internal border, no more half-second of the wrong font.</p>
<p>While each team owned a repository, the repository was the edge of what anyone was curious about. Engineers didn't read code they couldn't run, and reviews stopped at the border. We solved the same problem multiple times, without anyone noticing. Now people wander. They read adjacent code because it turns up in the same search, and they'll open a PR against something they don't own rather than file a ticket about it. Ownership is still real. It's enforced by review requirements rather than by a repository boundary nobody could see past, and engineers can move between areas of the product without hitting an onboarding cliff, which fixed a staffing problem we'd been trying to solve with org charts.</p>
<p>New starters clone one repo and run one command. It used to take them days to find everything.</p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_WxEf" id="what-we-should-have-done">What we should have done<a href="https://engineering.codat.io/blog/why-we-deleted-our-micro-frontends#what-we-should-have-done" class="hash-link" aria-label="Direct link to What we should have done" title="Direct link to What we should have done" translate="no">​</a></h2>
<p>We wanted modularity and we bought distribution, and those aren't the same purchase. Modularity is the good part: boundaries you can enforce, code that can't reach where it shouldn't, ownership that means something, changes that don't ripple outwards. All of it is available inside a single build, with tooling that's cheap and dull. Lint rules that enforce module boundaries. Tests that assert which way dependencies are allowed to point. Ownership at the folder level. Feature flags, so that releasing something and deploying it stop being the same event.</p>
<p>Distribution adds version boundaries and deployment boundaries on top of that, and unless you need them, they're cost. Deployment independence solves a deployment problem. If your deploys are slow or frightening, fix the deploys. Trunk-based development and a fast pipeline will have you shipping several times a day without splitting a single bundle.</p>
<p><img decoding="async" loading="lazy" alt="Two cards comparing modularity (enforced module boundaries, dependency direction tests, folder-level ownership, feature flags — all available inside a single build) with distribution (version boundaries, deployment boundaries, eight sets of repos and pipelines, runtime-checked contracts — cost unless you truly need it)" src="https://engineering.codat.io/assets/images/microfrontends-not-same-purchase-df12ff663b8a4f98232ce13366e7bbea.png" width="2400" height="1260" class="img_SqHw"></p>
<p>We'd still split under some circumstances. If CI can't keep up however you cache it. As a strangler pattern to get off a legacy stack, treated as a transition and not a destination. If an acquisition leaves you with two products that share a domain and nothing else. None of that was true of us, and I doubt it's true of most teams having this argument.</p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_WxEf" id="why-this-is-on-a-careers-page">Why this is on a careers page<a href="https://engineering.codat.io/blog/why-we-deleted-our-micro-frontends#why-this-is-on-a-careers-page" class="hash-link" aria-label="Direct link to Why this is on a careers page" title="Direct link to Why this is on a careers page" translate="no">​</a></h2>
<p>Taking apart an architecture you built is uncomfortable. It means writing down, where your colleagues will read it, that something you made carefully is now in the way.</p>
<p>Nobody had to defend the original decision in order to protect their standing. It was made with the information available at the time, and everyone treated it that way. The case for reversing it got written up and argued over, then tested with a spike before anyone committed. The engineers who'd built within the old architecture led the merge, partly because they knew where the bodies were buried and partly because being wrong in public isn't a career event here.</p>
<p>We build financial infrastructure. Our customers put financial products on top of our platform, and they need it dependable and boringly correct. That rewards engineers who care more about being right in eighteen months than about winning today's argument, and who'll look at their own old decisions without much sentiment and ask whether they're still earning their keep.</p>
<p>If that's how you'd want to work, you may be a good fit at Codat, and we'd love to have a chat. <a href="https://www.codat.io/careers/" target="_blank" rel="noopener noreferrer" class="">See our open engineering roles.</a></p>]]></content:encoded>
            <category>Engineering</category>
        </item>
        <item>
            <title><![CDATA[Introducing the Codat Engineering Blog]]></title>
            <link>https://engineering.codat.io/blog/introducing-the-codat-blog</link>
            <guid>https://engineering.codat.io/blog/introducing-the-codat-blog</guid>
            <pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Why we're starting a blog, and what to expect — engineering stories, life at Codat, and more.]]></description>
            <content:encoded><![CDATA[<p>Welcome to the Codat Engineering Blog. This is where we'll share what we're building, how we're building it, and what it's like to work here.</p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_WxEf" id="why-a-blog">Why a blog?<a href="https://engineering.codat.io/blog/introducing-the-codat-blog#why-a-blog" class="hash-link" aria-label="Direct link to Why a blog?" title="Direct link to Why a blog?" translate="no">​</a></h2>
<p>Codat builds the infrastructure that connects small business financial data to the products that need it. Behind that sits a lot of interesting work — distributed systems processing large volumes of financial data, integrations with dozens of accounting and banking platforms, and the day-to-day craft of shipping software in small, autonomous teams.</p>
<p>Most of that work never leaves our internal wikis. We'd like to change that.</p>
<h2 class="anchor anchorTargetHideOnScrollNavbar_WxEf" id="what-to-expect">What to expect<a href="https://engineering.codat.io/blog/introducing-the-codat-blog#what-to-expect" class="hash-link" aria-label="Direct link to What to expect" title="Direct link to What to expect" translate="no">​</a></h2>
<p>Posts here will come from people across Codat, and will broadly cover:</p>
<ul>
<li class=""><strong>Engineering deep dives</strong> — how we've solved real problems, from API design to data pipelines to developer experience.</li>
<li class=""><strong>Life at Codat</strong> — what it's like to work in our teams, how we run them, and the things we've learned along the way.</li>
<li class=""><strong>Things we've tried</strong> — experiments, conference write-ups, and honest retrospectives.</li>
</ul>
<p>If any of that sounds like an environment you'd enjoy working in, we're usually hiring — take a look at our <a href="https://www.codat.io/careers/" target="_blank" rel="noopener noreferrer" class="">careers page</a>.</p>
<p>See you in the next post.</p>]]></content:encoded>
            <category>Life at Codat</category>
            <category>Engineering</category>
        </item>
    </channel>
</rss>