Every developer has heard (or said) the line: "this thing needs to be migrated." I thought so too. The client ran a retail chain — 40k SKUs, physical stores and online sales — on a legacy ERP that writes binary DBF files and has no API. But the entire operation trusted that system: it was fast at the counter, the team knew it inside out, and twenty years of process were shaped around it.
The pain wasn't the ERP. It was what the data trapped inside it caused: replicating stock and sales history was hard, complex reporting was impractical, and nothing reached the other systems in real time. The most expensive symptom: counter sales took too long to reflect on the online channels, listings sold products that had already left the store, stock broke and turned into cancelled orders — and cancellations on a marketplace cost reputation.
The challenge I set myself: modernize without migrating. Give the ERP what it never had — fast queries, complex reports, real-time integration — without modifying it, without stopping the operation, and without losing a single stock movement.
The ERP writes an event journal into DBF/FPT files. I built a replicator in Python that follows that journal incrementally and mirrors everything into PostgreSQL — with a watchdog to restart a stuck process and a retry queue with a dead-letter so no error is ever silently swallowed.
That layer alone changed the game: queries that choked on DBF gained modern-database performance. Reports that were impractical started coming out in seconds.
A static mirror is just a backup. The value is in reacting to every change.
On top of the mirror, the service (NestJS) installs its own triggers — 17 change-capture channels via PostgreSQL LISTEN/NOTIFY: product, price, stock, sale, customer, delivery... Every stock movement at the counter fires a notification within milliseconds.
Here lives the first important decision: no notification is processed inline. The handler only records the event in a transactional inbox. Workers claim it with FOR UPDATE SKIP LOCKED — safe for multiple instances — and process in batches, with retries and per-event status.
On the way out, HMAC-signed webhooks distribute every change to whoever wants to consume it. And that's where the project became something else: a closed system had turned into an integration platform. Today, on top of those webhooks run a CRM that carries real-time numbers to finance, the CEO and the sales team; a stockout dashboard; and the synchronization with Mercado Livre and Shopee. Orders travel the other way through a normalized outbox — a single format the ERP consumes its own way.
The most important lesson of the project fits in one sentence: real-time notification loses events. Always.
Processes restart, connections drop, the replica gets rebuilt and takes the triggers down with it. If your architecture depends on never missing a NOTIFY, it is already broken — it just hasn't told you yet.
That's why the system has four independent self-healing loops:
Plus an ordering guard: a balance is only applied if the event's sequence number is newer than the last one applied — a late event never overwrites fresher stock. A failing webhook climbs a retry ladder from 1 minute to 24 hours before landing in the dead-letter.
Speed comes from the event. Guarantee comes from reconciliation. Together they are what lets the operation sleep at night.
The client kept the ERP they trust. Counter sales reflect on the online channels in near real time — stockouts and cancellations stopped being routine. And an entire ecosystem of modern systems runs on top of a piece of '90s software, riding out replica failures, restarts and out-of-order events without losing data and without manual intervention.
Not every modernization is a migration. Sometimes the most respectful — and hardest — engineering work is building the new around what already works.