Build1 publisher3 min readPublished
The new service committed to the database while the legacy engine kept serving its in-memory copy. Redis and a full cache reload both failed the team's constraints, so the fix came out of an API the old engine already exposed.
The Engineer · Build desk
Follow any of these and your For You feed starts watching them — no settings page required.
Compiled by The EngineerSomething wrong?How this is made
The reason the lease works where the manual refresh does not comes down to scope. Both pull data from the database into the legacy engine's memory. The manual refresh reloads a large amount of it, which is why running it after every migrated request was rejected on latency grounds [12]. Releasing a lease refreshes only the cached data attached to that one resource [14]. Addressability, not novelty, was the missing property.
The second reason is participation. A lock in Redis coordinates the processes that ask it for permission, and the legacy service is described as a black box that would not consult a new lock without being modified, which the team had ruled out [11][6]. The lease is enforced by the process that owns the stale copy, because the legacy service already uses it to guard its own critical sections [14]. Coordination sits where the cache sits. It sat in the API surface of the component scheduled for deletion, an unlikely place to look for it.
The wrapper is four steps: acquire the lease for the resource, update the database, run the business logic, release the lease [15]. That is two extra calls into the service you are retiring on every migrated write [1]. For the duration of the migration the new service depends on the old one on paths that no longer need it for business logic, and that dependency only clears when the embedded cache does.
A few conditions have to hold before the pattern moves to another migration. The legacy side needs per-resource acquire and release calls rather than only the all-at-once reload [8][13]. Release has to refresh the cache, which here is an existing side effect [14], so correctness rides on the behaviour of a system nobody wants to touch. And every legacy read path that can serve the resource has to take the same lease. The writeup establishes that the lease guards the legacy service's critical sections [14]; it does not enumerate which reads sit inside them. A legacy handler that reads its cache without taking the lease keeps the original stale window open for that endpoint, and checking for one is per-endpoint work.
The failure mode also hides early. It surfaced in QA, and according to the writeup it would have become more common as more endpoints moved across [5]. Frequency scales with migration progress, because the first few migrated endpoints touch few of the cached tables, so a pilot can pass while the design is already wrong.
The available text stops short of showing the failure handling its introduction promises [16]. Lease lifetime, the behaviour when release fails after the database write has committed, and whether the refresh finishes before release returns all decide what the legacy service does after a crash: block on that resource, or go back to serving the old value. Anyone lifting the four steps is lifting the happy path.</body_markdown> </invoke>
Ranked by verification strength, evidence, and original report placement.
During an incremental system migration, the new service could successfully update the database while the legacy service continued reading stale data from its own in-memory cache.
The system has three components: a client-facing Main Service, a Legacy Service described as an older high-performance engine that loads a subset of database tables into an embedded in-memory cache, and a New Service gradually replacing the Legacy Service using the Strangler Fig pattern.
The New Service acts as a proxy between the Main Service and the Legacy Service: if an endpoint has been migrated it handles the request directly, otherwise it forwards the request to the Legacy Service.
The dangerous sequence: the Legacy Service caches status = pending; a request is migrated to the New Service; the New Service updates the database to status = completed; a later request reaches the Legacy Service, which reads status = pending from its local cache. The database is correct but the Legacy Service operates on stale state.
The issues were discovered during QA testing, and the writeup states the problem would become increasingly common as more endpoints moved to the New Service.
Constraint: the Legacy and Main Services should not be modified, because the Legacy Service is largely a black box and modifying its behaviour is risky.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
One engineer, no artifacts
Everything traces to the person who did the work, and the account is unfalsifiable as written: the legacy engine is never named and its lease APIs are described rather than shown, with no code or timing attached. What argues for it is specificity — the pending-to-completed sequence, the reason a full manual refresh was rejected, the two separate mechanisms that clean up abandoned leases — the kind of detail a hand-waved post does not bother to carry. Against it, the text stops partway through the failure diagram, so the last step of the argument is missing from what a reader can actually see.
One team's self-report
The only party using this is the author's own team, on a migration whose system, employer and traffic volume are all withheld, and the bug that prompted it was caught in QA rather than production. No other team, vendor or project in our coverage reports repurposing a legacy service's lease API as a targeted cache-refresh trigger, so there is one disclosure to count and nothing to compare it with.
Modest, with one unflagged coincidence
For the genre this is restrained: no branded pattern, no claim that the approach generalises, and an explicit preference for reusing a primitive over adding one. The small overshoot is in what goes unsaid. This works because this particular engine exposes leases whose release refreshes precisely the affected resource and whose expiry does the same, and the write-up presents that as a discovery to emulate without noting how much of the outcome depends on that coincidence.
A personal post, no product
A developer writing up their own migration on a community platform, with no employer named, no tool being promoted, and a conclusion that argues against buying anything — the point of the piece is that Redis and Kafka were avoidable. What remains is the ordinary pull of authorship: a fix you shipped yourself reads tidier in the retelling than it did in the sprint, and the messy parts are the ones a reader cannot request.
Small claims, plainly stated, unverifiable
The claims are narrow and largely self-describing, which makes them easy to read accurately even from a single telling; what cannot be done is check any of them against a second party. That ceiling, plus an ending that breaks off mid-diagram, is why our reading sits mid-range rather than higher.
build
Three services you can delete: queue, cache and search in one Postgres1 publisher
build
Four control planes, one Postgres: a team's case against polyglot persistence1 publisher
build
Two layers instead of an outbox: when the dual-write gap is cheap enough to live with1 publisher
build
A semantic cache hit saves five times what a prompt cache hit saves1 publisher
Publishers with included, body-backed reporting in this cluster.
1 article · September 7, 2026