Skip to main content
All guides

Platform modernization

Modernizing a monolith while the business keeps running

Continuous operation during a rebuild is not a promise, it is a set of engineering practices. These are the ones that decide whether the migration survives contact with real users.

Updated October 13, 2025 · 8 min read

The short answer

  • You avoid downtime by running the old and new systems side by side against a shared source of truth and moving one capability at a time, not by rehearsing a single cutover weekend.
  • Cut the monolith along business capabilities such as work orders, inventory, or invoicing. A slice shaped like a technical layer has no independent value and cannot be released on its own.
  • Every schema change during coexistence has to be backward compatible. Expand/contract is the default technique, because a one-way migration is not available to you while the old system is still reading.
  • Enforce the new architecture with tests that fail the build when a boundary is crossed. Convention alone erodes under delivery pressure and the rebuild becomes a second monolith.
  • Design cross-cutting concerns such as location-scoped permissions before capabilities start moving. Retrofitting authorization across an already-migrated system is one of the most expensive corrections available.

Every modernization proposal contains the same sentence: there will be no downtime. It is usually written before anyone has decided how two systems will share a database, or who finds out first when a migrated workflow returns the wrong totals. Continuous operation is not a commitment made at kickoff. It is a property of specific engineering practices, and when they are missing the promise breaks in month nine, in production.

Cut by business capability, not by technical layer

The first decision is where to cut, and most teams get it wrong. The instinct is to slice horizontally: extract the data access layer, then the service layer, then the UI. That produces months of work with nothing releasable, because a layer is not something a user can be switched onto, and half-migrated layers leave both systems entangled, the state you were trying to escape.

Cut vertically instead, along business capabilities. Work orders. Inventory. Customer records. Point of sale. Each slice owns its data, rules, screens, and integrations, so it can be built, tested, released to a subset of users, and rolled back without touching anything else. It also answers the question executives ask: not how much code has moved, but which parts of the business now run on the new platform.

  • A cluster of tables that only one part of the application writes to
  • A workflow with a clear start and end state that a user would name out loud
  • A capability with few inbound calls from the rest of the system, even if it makes many outbound ones
  • An area where the rules are stable and understood, so you are porting behavior rather than rediscovering it
  • Something a distinct group of users touches, which makes rollout by location straightforward

Do not start with the most tangled, highest-traffic module on the theory that the hardest thing should go first. The first slice is where you build the migration machinery: deployment path, flag mechanism, observability, rollback. Build it where a mistake can be absorbed.

Coexistence is the strategy, not a transitional phase

The old and new systems run side by side for the whole program, often a year or more, both reading a shared source of truth. Users move capability by capability, sometimes location by location. Nobody experiences a cutover, because there is none: only a long sequence of small, reversible moves.

  1. 1

    Stand up the new platform against the existing database, read-only at first, so it renders real data before it is trusted to change any.

  2. 2

    Build the first capability behind a flag that is off for everyone, and verify it against production data with internal users.

  3. 3

    Turn it on for one location or one team, and leave it there through a full business cycle including month-end.

  4. 4

    Expand the rollout in increments you can reverse in minutes.

  5. 5

    Remove the legacy path, and the flag with it, once the capability has run clean for a sustained period.

The cost of coexistence is real. A shared database means every change must stay backward compatible with the system you are retiring, so architectural progress is balanced against not breaking inherited code on every step. Vocabulary collides too, because rebuilding a capability properly means adopting clearer names than the legacy jargon. Neither is an argument for a big bang. Both are the price of the business staying open.

Schema changes that cannot break the running system

While two systems read the same data, destructive migrations are not available to you. You cannot rename a column, tighten a constraint, or split a table in one step, because the old application is still running against the old shape. Every structural change becomes a sequence of safe steps, each deployable and reversible.

TechniqueWhat it doesUse it whenFailure mode to watch
Expand/contractAdd the new structure alongside the old, migrate readers and writers across, then remove the old one later.The default for any schema change during coexistence: renames, splits, type changes, new constraints.The contract step never happens, and the schema fills with dead columns nobody dares delete.
Dual writeWrite the same information to both the old and new structures during the transition.Both systems must accept writes to the same concept at once and neither can pause.The writes are not in one transaction, so a partial failure silently diverges the copies. Reconcile, do not hope.
BackfillPopulate the new structure for existing rows in batches, outside the request path.After the new structure exists and new writes land correctly, to bring history forward.Running it as one long transaction, locking the table and causing the outage you were avoiding.
Shadow readServe the answer from the old path while computing it on the new one and comparing.Correctness matters more than the change is visible: pricing, tax, commission, availability.Logging mismatches nobody reads. Divergence needs a dashboard and a threshold.
Database migration techniques and when each applies

Two rules make this workable. Deploys and migrations are separate events, so a schema change can ship days before the code that needs it. And every migration is written to be safe if it runs twice, because one of them will.

Enforce the new architecture or you will rebuild the old one

The monolith you are replacing did not start tangled. Business rules ended up in controllers, views, and shared globals one deadline at a time. Nothing about a fresh codebase prevents that, and a rebuild that decays is worse than the original because it cost more.

The practice that holds is separating what the business does from how it is delivered (an application layer that orchestrates HTTP and holds no rules, a domain layer holding nearly all business logic, and an infrastructure layer implementing interfaces the domain defines), then enforcing those boundaries with automated architecture tests that fail the build. Not documentation. Not a diagram in the wiki. A test.

  • The domain layer may not reference the application layer or any framework HTTP type
  • Infrastructure implements domain interfaces and is never imported by the domain directly
  • Business rules live in domain services, value objects, and specifications, not in controllers
  • New screens assemble from the shared component library rather than bespoke UI

Flags, rollout, and the gates that make a mid-migration deploy ordinary

Mid-migration you deploy frequently into a system real businesses are using, while two codebases share a database. That is only survivable if a deploy is routine rather than a scheduled risk. Feature flags separate deploying code from turning it on, so a broken slice is switched off in seconds by support instead of reverted by engineering.

Flags have a cost. Each one doubles the paths through a capability, and dozens of stale flags are harder to reason about than none. Give every flag an owner and a removal date, and delete it in the release after rollout.

GateWhat it catchesWhere it runs
Static analysis and type checkingType errors and defects before the code runs at allLocal, then blocking on merge
Unit tests over domain logicBusiness rule regressions in isolation from the frameworkBlocking on merge
Architecture testsLayer violations that would let the design decayBlocking on merge
Feature and integration testsBroken workflows across the boundary between old and newBlocking on merge
Browser-driven acceptance testsCritical user flows failing in a real browserBlocking on promotion to pre-production
Migration replay against a production-shaped databaseMigrations that lock, time out, or are unsafe to rerunBlocking on promotion to production
Gates that must exist before you deploy during a migration

Observability, so failures are visible before they are reported

During a migration your users are your integration test, and the question is whether you find out from a dashboard or from a phone call. The failure mode specific to this work is not the outage; it is the quiet divergence, where the new path produces a subtly different number and nobody notices for weeks.

  • Error rate and latency reported per capability and per flag state, so old and new paths are comparable
  • Business metrics, not just technical ones: orders created, invoices issued, jobs closed, tracked before and after each rollout step
  • Divergence counters from shadow reads, with an alert threshold rather than a log file
  • Structured logs carrying a correlation identifier that survives the hop between old system and new
  • An explicit alert when a backfill stalls, because a stalled backfill is silent by nature

Cross-cutting concerns are the ones that get expensive

Capabilities migrate independently. Concerns that touch every capability do not, and permissions are the clearest example. In multi-location operations a user is not simply an admin or a clerk: one person may hold different roles at different storefronts, and an operator may run hundreds of locations. The model that works resolves a user, an action, and a location into one allow-or-deny decision, built as a well-tested domain concern.

Settle this before the first capability moves, because the arithmetic is unforgiving. Authorization reaches into every query, every screen, and every test. Getting it right once costs a bounded amount of work. Discovering the location dimension after eight capabilities have shipped means revisiting all eight, and the bugs are the invisible kind: the wrong person sees another store's data and nothing indicates a problem.

The new platform was built alongside the old one, preserving data compatibility and shifting workflows incrementally with a fallback path at each stage. Fifteen years of legacy behavior preserved, boundaries enforced in CI, no big-bang cutovers.
Rebuilding for the next decade, an anonymized B-Team engagement

The same applies to auditing, tenancy, money and rounding, time zones, and notification routing. Decide each model once, in the domain layer, before capabilities depend on it. Finally, things not to do.

  • Do not attempt a big-bang cutover weekend for a system the business depends on daily. The rollback plan is always a fiction.
  • Do not keep the legacy path running as a hedge after a capability migrates. Two live implementations of one workflow will diverge.
  • Do not treat the rebuild as a chance to redesign every process at once. Port the behavior first, then improve it.
  • Do not measure progress in percentage of code migrated. Measure capabilities running in production and legacy paths deleted.

Common questions

Can a monolith really be modernized with zero downtime?
Zero unplanned downtime is achievable, and it comes from coexistence rather than from a careful cutover. Both systems run against a shared source of truth while capabilities move one at a time behind flags, so no single release changes what the whole business depends on. You should still expect brief planned maintenance windows for a small number of structural changes.
How long does this take compared with a rewrite and a single cutover?
Incremental migration usually takes longer in elapsed calendar time and costs less in total, because the expensive failure (a cutover that has to be reversed after a full rewrite is finished) never happens. The more useful comparison is time to first value: coexistence puts a real capability in production within months, while a big-bang rewrite delivers nothing until it delivers everything.
Should the new system have its own database from day one?
Usually not. A shared database is what makes coexistence possible, because both systems read consistent data without a synchronization layer that can drift. The cost is that every schema change has to stay backward compatible with the legacy code, which is real and constant. Separating the data store is a later step, taken per capability, once the legacy readers for that data are gone.
What actually prevents the new system from becoming a monolith too?
Automated architecture tests that fail the build when a layer boundary is crossed. Documented conventions and code review both work until a deadline, at which point business logic starts appearing in controllers again. Making the rule mechanical is the difference between a design that holds for a decade and one that decays in eighteen months.
How do we know a migrated capability is correct before we rely on it?
Run it in shadow first: serve the answer from the legacy path, compute it in parallel on the new path, and compare. Track the divergence rate as a metric with an alert threshold rather than as a log nobody reads. When divergence is at zero across a full business cycle, including month-end, the capability is ready for a real rollout.
Who should own the migration inside our organization?
One person with authority to say a slice is not ready, and enough context to weigh that against business pressure. Migrations fail when the decision to expand a rollout sits with whoever is under the most schedule pressure. The same owner should hold the flag inventory, because stale flags are a maintenance debt that accumulates silently.

Want this applied to your operation?

The first conversation is a no-cost fit discussion about the problem, its importance, and the people involved. We respond within one business day.

Start a conversation