Safkat Nirjash
Writing/Re-architecting Legacy
Use Case7 min read

Legacy is a business constraint, not a technology problem: a modernization framework that ships value early

A five-phase modernization framework built on one rule: ship business value at every phase, not only at the end. Drawn from re-architecting a payroll tax engine and a gift card platform with zero downtime.

Safkat Nirjash·

The question nobody wants to answer

When a legacy system turns into a business problem, leadership tends to land on the same answer: "We need to rewrite it."

That instinct is almost always wrong. Not because rewriting is never right, but because "rewrite" is a solution chosen before anyone has understood the problem. I have watched that instinct cost companies 18 months and millions of dollars, and the new system shipped with a slightly different set of limitations than the old one.

I have also seen the opposite. A targeted modernization that delivered business value in 90 days, never stopped the existing system, cleared years of compounding technical debt, and left the team in better shape to ship new features.

The difference is not luck. It is a framework.


Why legacy systems become business constraints

Legacy systems rarely fail catastrophically. They degrade in slow, predictable ways, and each step of that degradation adds a cost.

The first cost is the extensibility tax. Adding a new capability means changing core logic that was never designed to be changed. Every change carries risk, every risk needs testing, and every test cycle is time the business does not have.

A payroll tax consolidation engine I re-architected shows the shape of this. The system pulled federal and provincial employee tax values, consolidated them into remittance files, and submitted those files to government agencies. The core consolidation logic had been built for a fixed set of tax codes. Adding a new tax code, which happens regularly in a multi-jurisdiction payroll system, meant touching the core engine, the consolidation handler, the file generation layer, and the test suite. That took three to four weeks per tax code. The business needed new jurisdictions and tax types far more often than that, and the engineering timeline had become a competitive disadvantage.

The second cost is the compliance ceiling. Systems built before a compliance requirement existed often cannot meet it without a redesign. A system with no immutable audit trail will not pass a financial regulatory audit. You cannot patch your way there; you have to change how the system records state.

The third cost is the integration wall. Modern systems talk to each other through APIs, event streams, and webhooks. A legacy system with no clean API boundary turns every new integration into a deep code change. Work that should take days takes months.

A digital gift card and loyalty platform I worked on had hit exactly that wall. Every new partner integration meant editing the redemption engine directly. Integrations that should have taken one to two weeks were taking six, because there was no stable contract between the platform's core logic and the outside world.

The last cost is the deployment blast radius. When everything is coupled, everything breaks together. A change to one part of the system forces a regression test of the whole thing. Deployment frequency drops. Features pile up in staging. The gap between what engineering builds and what users can actually use keeps widening.


The modernization framework: five phases

I use a five-phase framework across legacy modernization work. The rule that holds it together: deliver business value at every phase, not only at the end.

PHASE 1          PHASE 2          PHASE 3          PHASE 4          PHASE 5
Business         Seam             Strangler        Parallel         Cutover
Value Map        Identification   Build            Running          & Decommission

Find the         Identify where   Build new        Run old and      Migrate by
specific         clean cuts       capability       new side by      segment,
business         can be made      alongside        side, validate   decommission
constraint       in the           the old          reconciliation   progressively
the legacy       codebase         system           continuously
is creating

Phase 1: business value map

Before I write a line of new code, I map the legacy system to the specific business constraints it creates. The output is a prioritized list of capabilities that are currently blocked, degraded, or at compliance risk.

For the payroll tax engine, the map showed three problems. New tax codes could not be added within acceptable timelines, which hit revenue directly. A gap in the audit trail blocked a major financial services expansion. And the jurisdictions were coupled, so a change for one province created regression risk for all the others.

For the gift card platform, it showed a different three. Partner integration time was blocking commercial deals. Manual reconciliation was eating 15 hours of operations time a week. And the monolithic deployment made it impossible to release business domains independently.

Phase 2: seam identification

A seam is a place in the existing codebase where you can draw a clean boundary between what needs to change and what needs to stay. Good seams sit on natural domain boundaries: tax calculation versus file generation, redemption logic versus integration handling. At a good seam you can define an interface contract clearly and route traffic across it without disturbing current behavior.

Seam identification is the technical counterpart of the business value map. Once you know the business outcome you are aiming at, the seam tells you where to cut.

Phase 3: strangler fig build

The strangler fig pattern means building new capability alongside the existing system instead of replacing it. New code lives in its own module and implements the same interface the existing system exposes. The legacy system keeps running unchanged through this phase.

For the tax engine, the strangler fig was a new consolidation engine that ran alongside the old one. It used a plugin architecture where each tax code was a registered handler behind a common interface, and the core consolidation logic worked with any registered handler without knowing its details.

For the gift card platform, the strangler fig was an integration adapter layer: a thin API gateway in front of the existing redemption engine that gave partner integrations a stable contract. Partners connected to the adapter. The legacy internals stayed untouched through the transition.

Phase 4: parallel running

Before any real traffic moves, both systems process the same inputs at the same time. You compare outputs continuously, and you investigate and resolve every reconciliation failure before cutover starts.

Most teams want to skip this phase because it feels slow. It is also the phase that makes a zero-downtime migration possible. For a payroll system it is not optional. A reconciliation failure in production means employees get paid incorrectly.

Phase 5: cutover and decommission

Cutover happens gradually, one jurisdiction or partner or product type at a time. You validate each segment migration before starting the next. The legacy system stays available as a fallback until every segment has moved and a stability period has passed.


The two patterns that keep recurring

Across these engagements, two architectural patterns have proven their worth more often than any others.

The first is centralized extensibility through a plugin or strategy architecture.

When a legacy system has hardcoded logic that needs to handle variations, whether those are tax codes, partner integrations, or jurisdictions, the modernization that works extracts the common algorithm and separates it from the variable parts. The core does not change when you add a new variant. A new variant is a new plugin, not a new version of the core.

This pattern is what reshaped the payroll tax engine. We extracted and stabilized the core consolidation algorithm. Tax code handlers became plugins. Adding a new tax code came down to implementing one interface and registering it. The timeline per tax code dropped from three to four weeks down to two or three days.

graph TD
    subgraph BEFORE["Before: Hardcoded Consolidation"]
        TC1[Tax Code A - hardcoded]
        TC2[Tax Code B - hardcoded]
        TC3[Tax Code C - hardcoded]
        TC1 --> CE_OLD[Consolidation Engine - monolithic]
        TC2 --> CE_OLD
        TC3 --> CE_OLD
        CE_OLD --> FILE_OLD[Remittance File Generator]
    end

    subgraph AFTER["After: Plugin Architecture"]
        CE_NEW[Consolidation Engine - stable core]
        REGISTRY[Tax Code Registry]
        P1[Tax Code A Handler - plugin]
        P2[Tax Code B Handler - plugin]
        P3[Tax Code C Handler - plugin]
        P4[NEW Tax Code D Handler - plugin - 2 days to add]
        REGISTRY --> CE_NEW
        P1 --> REGISTRY
        P2 --> REGISTRY
        P3 --> REGISTRY
        P4 --> REGISTRY
        CE_NEW --> FILE_NEW[Remittance File Generator]
    end

The second pattern is an event-driven integration boundary.

When a legacy system needs to expose its state changes to downstream systems, such as partners, reporting tools, and reconciliation engines, the modernization that works publishes state change events to a bus. Downstream systems subscribe to the events they care about. The core does not need to know any of them exist.

This is what broke through the integration wall on the gift card platform. Every redemption, issuance, and reconciliation event went onto an event bus. Partners connected to the bus. The operations reconciliation report became a subscription. Manual reconciliation, all 15 hours a week of it, went to zero.

graph TD
    subgraph BEFORE["Before: Direct Coupling"]
        RE[Redemption Engine]
        PartnerA -->|Custom code| RE
        PartnerB -->|Custom code| RE
        PartnerC -->|Custom code| RE
        RE -->|Manual export| OPS[Operations - 15 hrs/week reconciliation]
    end

    subgraph AFTER["After: Event-Driven Boundary"]
        RE2[Redemption Engine]
        ADAPTER[Integration Adapter - stable API]
        BUS[Event Bus]
        PartnerD --> ADAPTER
        PartnerE --> ADAPTER
        PartnerF --> ADAPTER
        ADAPTER --> RE2
        RE2 --> BUS
        BUS --> REPORT[Auto Reconciliation Report]
        BUS --> ANALYTICS[Partner Analytics]
        BUS --> OPS2[Operations Dashboard - 0 hrs manual]
    end

The modernization maturity model

Not every legacy system needs the same intervention. I use a five-level maturity model to calibrate how deep the modernization has to go.

LevelStatePrimary ConstraintRecommended Intervention
1Monolith - no internal boundariesDeployment blast radiusIntroduce bounded contexts within existing codebase
2Bounded contexts - tightly coupled dataData coupling creates cross-domain failuresSeparate data stores per domain
3Service boundaries - but synchronousLatency cascades, no resilienceIntroduce async communication and event bus
4Event-driven - but hardcoded handlersCannot extend without core changesImplement plugin/strategy pattern for variable logic
5Plugin-based, event-drivenMature - focus on observability and performanceInvest in instrumentation and optimization

Most legacy systems that show up as business constraints sit at Level 1 or Level 2. Getting to Level 3 or Level 4 is usually enough to unlock the business outcome that triggered the modernization in the first place.


What this framework is not

This framework does not claim a rewrite is never the right answer. Sometimes a system is so deeply coupled, so poorly tested, and so far from supporting the business outcomes you need that incremental modernization costs more than replacement.

But that conclusion should come out of the business value map and seam identification work, not go in as the starting assumption. Teams that start with "we need to rewrite" are usually solving the wrong problem. Teams that start by asking which specific business constraint the legacy system is creating almost always find a faster, more targeted path.

Work with me

Ready to discuss your architecture?

I work with founders and engineering leaders as a Fractional CTO to translate business goals into technical strategy - and execute on them. Free 30-minute Technical Health Check to start.

Book a call