Safkat Nirjash
Writing/Re-architecting Legacy
Case Study7 min read

From weeks to days: re-architecting two legacy platforms without stopping delivery

Two systems, two domains, one root problem: scattered core logic with no single source of truth. A payroll tax consolidation engine and a gift card platform, both modernized with zero downtime.

Safkat Nirjash·

Two systems, the same root problem

I have re-architected legacy systems in two very different domains: enterprise payroll tax processing, and digital gift card platform operations. On the surface the two had nothing in common. One processed multi-jurisdiction tax remittances for hundreds of thousands of employees. The other handled gift card issuance and redemption for retail partners.

But when I mapped both systems against the business constraints they were creating, the root problem turned out to be identical. The core logic was scattered, and no single place owned the truth.

In the payroll system, federal and provincial tax consolidation logic was duplicated and hardcoded in several places at once. Adding a new tax code meant finding every place it touched, changing each one carefully, testing the combinations, and confirming nothing had broken in adjacent jurisdictions. That took three to four weeks per tax code, on a platform where tax code additions were a routine business requirement.

In the gift card platform, the redemption and integration logic was a monolith. Partners connected straight into the core engine through custom code paths. Every new integration was surgery. Commercial deals stalled because engineering timelines could not keep up with sales commitments.

The solutions looked different. The pattern underneath them was the same.


Story 1: re-architecting the tax consolidation engine

The system and its history

The payroll platform I worked on processed remittances for both federal and provincial tax authorities across Canada. The remittance engine handled four jobs:

  • Rolling up federal and provincial employee tax values from the payroll processing engine
  • Applying consolidation rules for the specific remittance type (standard, trust, GRS - Government Remittance Service)
  • Generating sequence-managed remittance files in the required format (DDCCPP, DDCHOLD, and related formats for TOHO processing)
  • Submitting files to the appropriate government endpoints and tracking acknowledgment

The system had grown over years by accretion. Each new tax code, jurisdiction, and government filing format had been handled by a developer who understood the immediate requirement but was working in a codebase with no extension points. What you ended up with was a consolidation engine where:

  • Consolidation logic for each tax code was spread across several handler classes, each with its own reading of the same underlying rules
  • Federal and provincial rollup calculations were duplicated, so a rule change had to be found and updated in several places
  • Adding a new tax code meant changing the core consolidation processor, the sequence number management service, the file generator, and the test suite, even when the new code was logically identical to an existing one with different rate parameters

The business impact was concrete. The commercial and compliance teams needed new tax codes faster than the engineering timeline could deliver them. Each addition took a sprint planning session, a risk assessment, a parallel testing period against the legacy code, and a deployment window. Three to four weeks, even for the straightforward ones.

The diagnosis

I started by mapping where the consolidation logic actually lived. The answer was: everywhere.

BEFORE: Scattered Consolidation Logic

ConsolidationProcessor.cs
  ├── HandleFederalBasicTaxCode()       [contains logic]
  ├── HandleFederalSupplementalCode()   [contains similar logic, slightly different]
  ├── HandleProvincialAlberta()         [contains similar logic, AB-specific]
  ├── HandleProvincialOntario()         [contains similar logic, ON-specific]
  ├── HandleProvincialQuebec()          [contains logic, QC has different rules]
  └── ... (each new code = new method = new risk surface)

RemittanceFileGenerator.cs
  ├── GenerateDDCCPP()   [calls into ConsolidationProcessor - knows about tax codes]
  └── GenerateDDCHOLD()  [calls into ConsolidationProcessor - knows about tax codes]

SequenceNumberManager.cs
  └── AssignSequence()   [tax-code-specific branching logic embedded here too]

Every tax code left fingerprints in three different classes. The redesign goal was a single authoritative consolidation path that every tax code flowed through.

The insight that made the redesign possible: the consolidation algorithm itself was the same for all tax codes. What varied was the rate and calculation parameters, the applicable jurisdictions, the file format requirements, and the sequence management rules. None of that variation needed to live inside the core algorithm. It belonged in configuration.

The new architecture: plugin-based consolidation

I rebuilt the consolidation engine around three components.

The consolidation core is a single stable algorithm that processes any registered tax code handler. It owns the rollup sequence, the parallel-run reconciliation logic, and the file generation orchestration. It knows nothing about specific tax codes.

The tax code handlers are one class per tax code, each implementing a common interface (ITaxCodeHandler). A handler owns its rate parameters, jurisdiction applicability, and format specification, and registers with the core at startup.

The tax code registry is a configuration-driven registry that maps tax code identifiers to handler implementations. Adding a new tax code now means implementing the interface, registering the handler, and writing tests for that handler in isolation.

graph TD
    subgraph CORE["Consolidation Core - stable, unchanged"]
        ROLLUP[Federal + Provincial Rollup Engine]
        SEQ[Sequence Number Manager]
        GEN[Remittance File Generator]
        ROLLUP --> SEQ
        SEQ --> GEN
    end

    subgraph REGISTRY["Tax Code Registry"]
        R[Registry - configuration-driven]
    end

    subgraph HANDLERS["Tax Code Handlers - pluggable"]
        H1[Federal Basic Handler]
        H2[Federal Supplemental Handler]
        H3[Provincial Alberta Handler]
        H4[Provincial Ontario Handler]
        H5[Provincial Quebec Handler]
        H6[NEW Handler - 2 days to add]
    end

    H1 --> R
    H2 --> R
    H3 --> R
    H4 --> R
    H5 --> R
    H6 --> R
    R --> CORE

    subgraph OUTPUT["Output"]
        DDCCPP[DDCCPP File]
        DDCHOLD[DDCHOLD File for TOHO Process]
        TRUSTGL[Trust GL Record]
        GEN --> DDCCPP
        GEN --> DDCHOLD
        GEN --> TRUSTGL
    end

The file formats, DDCCPP for standard remittances and DDCHOLD for the TOHO process, were defined in each handler's format specification. The file generator read the specification and produced the file. No hardcoded branching in the generator itself.

The parallel running period

The existing system kept processing live payroll in production throughout the redesign. The new engine ran alongside it, took the same inputs, and we compared outputs.

The discrepancies fell into two kinds. Some were true corrections: places where the legacy engine had been wrong, or inconsistently wrong across similar tax codes, and the new engine calculated correctly. The rest were configuration gaps: places where a handler's parameters had been transcribed wrong and needed fixing.

The parallel period ran for six weeks and covered two full payroll cycles across every active jurisdiction. By the end, reconciliation was 100% accurate on the true-positive cases and every configuration gap had been closed.

The result

The main business outcome was a 92% cut in the time to add a new tax code. What used to take three to four weeks, touching core logic in several classes, running a risk-review sprint, and parallel-testing the full engine, became a two or three day job: implement the handler interface, register it, write handler-level unit tests, deploy.

A few other things improved alongside it. The consolidation core became independently testable for the first time, so a change to a handler no longer forced a regression test of the whole engine. Federal and provincial rollup calculations had a single authoritative implementation, which meant a rate change was one configuration update instead of a search-and-replace across files. The core owned sequence number management instead of branching it across tax code handlers, which killed off a persistent source of hard-to-reproduce bugs. And the system was finally ready for the compliance audit trail work the business needed for its financial services expansion.


Story 2: re-architecting the gift card platform

The system and its constraints

The digital gift card and loyalty platform I joined was processing real transactions and growing. The engineering team was good. But delivery was grinding down.

The platform ran on a monolithic redemption engine. Gift card issuance, redemption, partner integrations, loyalty logic, and financial reconciliation were all coupled into one deployable unit. Every partner integration meant editing the core engine. Every deployment touched everything. The blast radius of any change was the whole platform.

The constraints came down to three. Partner integration took four to six weeks each, which blocked commercial deals the sales team was closing. Operations spent 15 hours a week on manual reconciliation, because there was no event stream to feed automated reports. And the loyalty and gift card domains were entangled, so a bug fix in one needed a regression test of both.

The new architecture: adapter layer and event bus

Instead of a rewrite, I applied two targeted interventions using the strangler fig pattern.

The first was an integration adapter layer. I built a thin adapter in front of the existing redemption engine that exposed a stable API contract for partner integrations. New partners connected to the adapter, not to the engine directly, and the adapter translated between the partner's integration format and the engine's internal interface.

That broke the direct coupling between partner integrations and the core engine. Partner integrations became bounded and replaceable. The engine's internal interface stopped being a public contract that partners depended on.

The second was an event bus for state changes. I introduced Azure Service Bus, and the redemption engine published to it on every state change: issuance, redemption, reversal, expiry. Downstream consumers, including the operations reconciliation system, the loyalty points engine, and the analytics pipeline, subscribed to the events they cared about.

That removed the manual reconciliation process entirely. The reconciliation report became a subscriber. It received every relevant event and built its own view of the data in real time.

graph TD
    subgraph BEFORE["Before: Monolithic Coupling"]
        PA[Partner A - custom code] --> ENGINE_OLD[Redemption Engine]
        PB[Partner B - custom code] --> ENGINE_OLD
        PC[Partner C - custom code] --> ENGINE_OLD
        ENGINE_OLD -->|manual export| OPS_OLD[Operations - 15 hrs/week]
        ENGINE_OLD --- LOYALTY_OLD[Loyalty Engine - tightly coupled]
    end

    subgraph AFTER["After: Adapter + Event Bus"]
        PD[Partner D] --> ADAPTER[Integration Adapter API]
        PE[Partner E] --> ADAPTER
        PF[Partner F] --> ADAPTER
        ADAPTER --> ENGINE_NEW[Redemption Engine]
        ENGINE_NEW --> BUS[Azure Service Bus]
        BUS --> RECON[Reconciliation Subscriber - automated]
        BUS --> LOYALTY_NEW[Loyalty Engine - decoupled subscriber]
        BUS --> ANALYTICS[Analytics Pipeline]
    end

The result

Partner integration time fell from four to six weeks down to eight to eleven days. With the adapter layer in place, a new integration was configuration and adapter code, not core engine surgery.

Manual reconciliation dropped to zero. The 15 hours a week the operations team had spent aggregating data went back to analysis and partner support.

Three commercial partner integrations that had been blocked for months were finished within 90 days of the adapter layer going live.


The common thread

Two systems, two domains, two different technical interventions. The same architectural move underneath: find where the core logic is scattered, pull it into one authoritative place, and make variation an extensibility concern instead of a core modification.

In the payroll system, the variation was tax codes, and the plugin registry made adding a variant a bounded, low-risk activity. In the gift card platform, the variation was partner integrations, and the adapter layer made each integration bounded and independent from the core.

The business impact landed the same way both times. A process that took weeks now takes days. The core is stable. Variation is manageable.

That is what re-architecture looks like when it is aimed at business outcomes instead of technical aesthetics.

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