DEV Community

Felix Jumason
Felix Jumason

Posted on

Taped-Up DBs, But It Works

dbs

We did not integrate two databases. We copied tickets one way, on a timer, through an API that is allowed to be wrong for five minutes.

That is tape. The tape holds because we were strict about the direction.

Two products, one guest login

The new app is a field tool: photos, on-box vision models, background jobs, GPS, a workflow the legacy schema never heard of. The old app is still the system of record for work requests. Crews log into us. Closeout still has to go back through their HTTP API. Their MySQL is not our house.

So we did not share a database. We did not put a foreign data wrapper over work_requests. We did not replicate their tables into ours. Browsers talk to our web app. The web app talks to our API. Only that API may touch MySQL, and only with SELECT. The worker never sees the source database. It drains jobs that already sit in Postgres.

Image db

The copy

Sync is not a conversation. HTTP returns ticket ids. We SELECT those ids from MySQL. Non-empty MySQL fields overlay the HTTP payload: site, GPS, contact, company. The merge is upserted into Postgres tickets.raw_json. After that we work from Postgres until the next copy.

Dashboard load does it for the signed-in user. A worker repeats it every 300 seconds. A ticket-link call loads one MySQL row by owner id and request id, then writes the same Postgres row. Inspections, scores, and local status never travel the other way.

sequenceDiagram
    actor Crew
    participant HTTP as Legacy HTTP API
    participant API as Our API
    participant MySQL as Source MySQL
    participant PG as Our Postgres

    Note over Crew,PG: Copy happens on dashboard open
    Crew->>API: open dashboard
    API->>HTTP: list tickets
    HTTP-->>API: ids + payload
    API->>MySQL: SELECT work_requests WHERE id IN (...)
    alt answers within 5s connect / 10s read
        MySQL-->>API: site, GPS, contacts
    else timeout
        MySQL--xAPI: no row
        Note over API: warning only, keep HTTP payload
    end
    API->>PG: UPSERT tickets.raw_json
    Note over PG: inspections and scores stay here

    Note over Crew,PG: Closeout is a later call, not this copy
    Crew->>API: submit closeout
    API->>HTTP: closeout
    HTTP-->>API: accepted
    Note over MySQL: still no writes from us

Tuesday, 06:40. A crew opened the dashboard. Source MySQL did not answer in 5 seconds. We logged a warning, upserted the HTTP row, and the ticket rendered with a title and no pin. They started work. The pin arrived on the next 300-second poll. Dispatch asked why the map was empty for four minutes.

That is the scar. Blocking on their MySQL would have been the clean dashboard. It would also have made their outage our outage. Missing GPS for one poll is worse than a spinner. A white screen in the field is worse than both.

Why not one database

Putting our tables into their MySQL couples a new product to a schema we do not own. Using their database as our operational store mixes inspections into their domain. Joining live at query time makes every dashboard depend on a hop we cannot page.

We borrow ticket facts. We leave them there. Guest credentials use a different env prefix than their app's DB_HOST, on purpose: same server, same table, not our house.

Best practice, with the ugly bit named

The bounded part is textbook. Read-only access to a system of record, plus a local working copy, is an anti-corruption layer. Tickets we already copied keep working if source MySQL is slow. They stay unaware that inspections exist.

The taped-up part is the dual inbound path. Ticket identity still comes from HTTP. Extra columns come from MySQL. Merge rule: database wins when the value is not empty. That hides disagreement. If HTTP says approved and MySQL still says pending, we show the MySQL value and neither team would fully claim the screen. A cleaner design would pick one canonical read for details and keep HTTP only for login and closeout. We had a live API, a live table, and a field app that needed both.

Polling is tape too. Five minutes is not event-driven. For a crew already on site, it is soon enough. For a dispatcher staring at a status that changed 90 seconds ago, it is a lie we accepted.

The rule we will not break

Do not write to the source database.

The moment we UPDATE work_requests, the tape becomes a two-way cable. Then we own their schema, their transactions, and the night someone files a ticket because our inspection closed a request their finance screen still thought was open. Closeout goes through their HTTP API, which is their write path. Ours is copy-in.

Copy in. Never copy out. Live in Postgres.

What "it works" means

It works if a crew can open a ticket, see the site when MySQL answered, run the checks, and submit a closeout without us becoming a second ERP.

It works if they add a column we do not read and nothing here restarts.

It works if 06:40 looks like a ticket with no pin, not a 502 on the briefing.

Two databases. One direction. A 300-second lie we can live with.

Taped-up DBs. But it works.

Top comments (0)