Code3 All articles
Developer Tools & Workflow

Build Once, Rewrite Fearlessly: The Architecture Patterns That Keep You Shipping

Code3
Build Once, Rewrite Fearlessly: The Architecture Patterns That Keep You Shipping

Every engineering team eventually hits the wall. The codebase that got you to product-market fit is now the thing slowing you down. You know a rewrite is coming — maybe it's already been whispered in a few too many retros. The real question isn't whether to refactor, it's whether you can do it without stalling the entire roadmap.

Spoiler: you can. But it requires making a few deliberate architectural decisions before the pressure is on.

Why Most Rewrites Go Sideways

The classic rewrite failure story goes something like this. The team gets buy-in to rebuild a core service. A parallel branch gets created. Six months later, the new version still isn't feature-parity with the old one, the old one has been patched a dozen times, and now you've got two systems to maintain instead of one. Shipping velocity craters. Morale follows.

The root cause is almost never technical. It's structural. Teams try to do full replacements when they should be doing incremental substitutions. The architecture didn't have clean enough boundaries to allow that, so everything became a big-bang migration.

The companies that handle this well — Stripe, Shopify, GitHub — aren't doing anything mystical. They're designing systems with explicit layers and explicit contracts between those layers.

The Three Seams You Actually Need

Think of a maintainable system as having three distinct zones of concern, each with a well-defined interface to the others.

The interface layer is what your users or clients actually touch — your API surface, your UI components, your event contracts. This layer should change slowly and deliberately. Versioning lives here. When Stripe adds a new API version, old integrations don't break. That's not magic; it's a deliberate decision to absorb change at this boundary rather than push it downstream.

The domain layer is where your business logic lives. This is the stuff that makes your product your product — the pricing rules, the fulfillment logic, the recommendation engine. This layer should be almost entirely insulated from infrastructure concerns. If your domain logic has database queries mixed into it, you've already lost some flexibility.

The infrastructure layer is everything underneath: databases, queues, third-party services, caching. This is usually the first thing teams want to replace, and it should be the easiest — if the domain layer is properly isolated from it.

The real architectural work is enforcing the boundaries between these three zones. And that's harder than it sounds, because the path of least resistance in any codebase is to let things bleed together.

The Strangler Fig in Practice

One of the most practical patterns for incremental migration is the strangler fig — named after the tropical tree that grows around its host until the host is gone. You build the new system alongside the old one, route traffic incrementally, and decommission the old code as the new code proves itself.

Shopify used a version of this when migrating parts of their monolith to a service-based architecture. Rather than carving out entire domains at once, they identified high-traffic, well-understood paths first. A routing layer (sometimes called an anti-corruption layer or a facade) sat in front of both implementations, directing requests based on feature flags or percentage rollouts.

This approach has a few underrated benefits beyond the obvious risk reduction. First, it forces you to define the interface contract before you write the replacement — which is exactly when you should be making those decisions, not after. Second, it creates a natural rollback path. If the new implementation has issues, you flip the flag. Third, it generates real production traffic data on the new system before you commit to it.

Feature Flags Are Architecture, Not Just a Deployment Tool

If you're not treating feature flags as a first-class architectural concern, you're leaving a lot of flexibility on the table. Flags aren't just for A/B testing UI changes — they're the mechanism that lets you run two implementations simultaneously and gradually shift load.

Tools like LaunchDarkly, Unleash, or even a simple database-backed flag system can give you the control plane you need to run strangler fig migrations safely. The key is that flag evaluation should happen at the interface layer, not buried in domain logic. You want one place where the routing decision is made, not twenty.

Keeping the Trains Running During the Dig

The non-technical piece of this is just as important. Rewrites fail when they become invisible to the broader team — when they're treated as a separate track that the product roadmap doesn't account for.

The teams that do this well run migrations as first-class project work. Migration milestones show up in sprint planning. Progress is visible. There's a definition of done that everyone agrees on. And critically, the migration work is time-boxed so it doesn't become a permanent background task that nobody's accountable for.

Architecture that survives rewrites isn't about predicting the future. It's about building systems where the future can arrive incrementally, without requiring everything to stop so you can let it in.

Build the seams. Ship the replacement in slices. Keep the trains running.

All Articles

Related Articles

Stop Guessing, Start Measuring: A Developer's Guide to Building Smarter Feedback Loops

Stop Guessing, Start Measuring: A Developer's Guide to Building Smarter Feedback Loops

Debt Isn't a Four-Letter Word: How Smart Teams Budget for Technical Shortcuts

Debt Isn't a Four-Letter Word: How Smart Teams Budget for Technical Shortcuts

Done Is Better Than Perfect: Why Shipping Messy Code Is Actually Good Engineering

Done Is Better Than Perfect: Why Shipping Messy Code Is Actually Good Engineering