Are monoliths making a come back? Shepherd, an insurance platform, merged its polyrepo into a monorepo and collapsed its architecture into a single Next.js monolith, cutting its development cycle from nearly an hour to under 10 minutes. The company found that colocating code reduced AI context usage by up to 40% on the same task, making the reorganization economically obvious. The migration, called Monorail, involved mounting the existing backend service inside the new application rather than rewriting it. Back to the Monolith: AI Cost, Context, and our Changing Priorities Most of Shepherd's architecture existed to compensate for our coordination limits, but AI-driven development removed that need to compensate. We merged our polyrepo into a monorepo, collapsed it into a single Next.js monolith, and cut our development cycle from nearly an hour to under 10 minutes. For the last 5 years, Shepherd’s insurance platform operated on what had been industry best practice. We split our frontend and backend services into separate repositories so our teams could operate independently. Barring the occasional broken deploy, this process served us incredibly well. Shepherd is uniquely positioned: as a predominantly internal platform, we can move faster than a team shipping to millions of users. It does not let us be careless, our users are the underwriters pricing live submissions, and they notice everything. But we can take risks that allow us to invest deeply in developer efficiency. Why Monolith? What Changed? Since I joined Shepherd, our biggest problem has been keeping the contract between our frontend and backend services in sync. We rarely shipped a breaking change, but when we did, it could cost us 10 or 15 minutes of unexpected downtime that hurt both our users and our engineers’ pride. We had floated the idea of a monorepo and shared types, but our cost estimate was almost entirely in engineer hours, blocked most by coordination amongst different teams. A single engineer never had enough time to put in the work and make it happen, and we couldn’t justify handing it to more than one. And then the cost disappeared. As models improved, so did the amount of code Shepherd engineers were producing. The more code we produced, the more architectural context an agent had to carry, and the more important it became to preserve that context and minimize rot. An agent working across two repos, a GraphQL schema, and multiple service boundaries burns tokens and attention reconstructing how those services work together. With AI-assisted development, an incredibly demanding harness just takes iteration, and iteration is substantially cheaper than an engineer trying and failing to define every parameter up front. Through repeated testing and improvements to the harness, we found that colocating our code reduced context usage by up to 40% on the same task. That made reorganizing our code economically obvious, but we could take it even further with a monolith. Monorail: The Migration Before the change, Shepherd’s platform consisted of two separate applications. Goldengate was our backend Node.js server on AWS that exposed GraphQL. Brooklyn was our frontend Next.js SPA on Vercel that communicated with it. Two repos, two deployments, two sets of infrastructure, and a schema contract between them that every feature had to cross. Monorail was a single platform, a brand new third and final service bridging the gap between the two. We made an entirely new Next.js application on Vercel, backed by Neon Postgres, and started from scratch. We ran the whole thing in four beats, and the sequencing mattered more than any single decision. We would derisk with a smoke test, run in shadow mode, cut over in one move, then distribute the tail-end migration with AI-assisted tooling. The process here was just as important to us as the end result. It was essential that an engineer shouldn’t need to know about the migration until Monorail was ready for them. We started off small, without a big rewrite, and ran a single smoke test. We imported a few pieces of the legacy code into the new application and made sure deployments work and all builds were green. The strategy we settled on was to mount, not rewrite. Our Goldengate backend service got mounted inside Monorail. The entire GraphQL schema engine was served, completely unchanged, through an internal API route. Every API endpoint, every webhook, and every external service got its own Next.js route. Brooklyn’s frontend routes were programmatically imported by a script that allowed the rest of our team to keep developing, unaware of the work going on. The PR that brought Monorail to life consisted of the bare-bones skeleton of a Next.js app, 200 page files, and the script that traversed Brooklyn and generated them. Nearly every file was three lines long: // monorail/src/app/u/carriers/page.tsx 'use client'; export { default } from '@brooklyn/app/u/carriers/page'; export from '@brooklyn/app/u/carriers/page'; That’s it. Our migration has begun, and it brought us complete developer parity with a direct reference to the original source code. No code freeze needed just yet, no schema drift, no new process for our engineers. Monorail went live in shadow mode on its own subdomain alongside production, which let us migrate essential services one at a time. The pieces that couldn’t simply be mounted — authentication, navigation, our Policy and Quote flows, document generation, webhooks, external services, and background jobs — were rebuilt under a microscope. We found and squashed bugs at every layer, from CSS quirks to payload size limits to document generation failures. Our migration spent weeks living as a preview environment while we built parity with production and gained confidence. Only then did we cut over. We gave the company 10 days’ notice, planned, prepped, planned again, and made sure the to-do list was extensive. At the heart of it, the list was quite simple: - Update Vercel’s DNS to point at the new project - Migrate our RDS database into a fresh Neon database - Update environment variables As far as skeletons in the closet go, we had significantly fewer than expected: a Lambda whose database URL took longer to update than we’d care to admit, a Gmail extension that broke after its settings seemingly disappeared into the Google Console ether, and a handful of Vercel environment variables that needed tweaking to point the client back at the server correctly. We gave a heads up to the office at 2:30 PM, started at 3, and by 6:30 we were out the door to go crack open champagne at our favorite French bistro around the corner. Our users came in on Monday and were greeted with the exact same application they had been using on Friday. Once we put out the fires they found and boy do our users know how to find everything we missed , what was left was the shim. Don’t forget, we still had a GraphQL server running inside our Next.js application, mounted exactly where we had put it, and it would have kept running there as long as we let it. Being able to stop there was the biggest benefit of a migration shaped like this, because nothing else needed to happen. Even if we had stopped that Monday, we had already cut our deploy times and practically obliterated the chance that an unintentional breaking change could take down the platform, and no roadmap conversation could claw those wins back. But leaving the shim in place meant leaving the real prize on the table, and I wasn’t done. Shepherd was going to get a context-optimized monolith, and that meant addressing the long tail: ~600 GraphQL resolvers across 1,200 call sites. My part in this post-migration world was to provide the rest of the team with the tooling needed to migrate those call sites seamlessly. We ran countless iterations on our smallest routes and pages, moving them over using best-practice Next.js development, and each one was scrutinized by me, Mo our CTO , and my new best friend, 5.6 Sol. The difficulty here was controlling scope and making sure our base case had only what was needed to migrate a page off GraphQL, turning that three-line stub above into something as simple as this: js // monorail/src/app/u/carriers/page.tsx — after migration import { getCarriers } from 'monorail/server/entities/organizations/services/carriers'; import CarriersView from './CarriersView'; const CarriersPage = async = { return