Skip to content
Work/Case Study
Distributed State · Two-Way Sync

A dirty flag beat a consensus protocol

Two systems, both able to write the same appointment, thousands of clinics apart. The overwrite race that quietly ate users' changes looked like a distributed-locking problem. It was a boolean and a rule that reads it — and I'd make that trade again.

Role
Designed and built the sync logic and dirty-flag guard; co-designed the ack loop
Problem class
Concurrent writers · unreliable partner · lost writes
Scale
3,500+ clinics · two directions · continuous
Headline result
Overwrite race gone · zero distributed locks
OwnershipDesign · HighImplementation · HighAck loop · Co-designedReal-time transport · Platform teamConfidence · Mechanism verified, magnitude estimated
01 — Background

Our platform and the clinics' external clinical systems both hold appointments, and both can change them. A front-desk staffer reschedules inside the external system; a patient books or cancels through our platform. For the product to make sense, the two worlds have to agree — in both directions, continuously, across thousands of clinics.

"Keep two systems in sync" sounds like a checkbox and is actually a swamp. The moment both sides can write, you're in distributed-systems territory whether you wanted to be or not: propagation delay, acknowledgement, retries — and the one that ruins your week, what happens when both sides touch the same record at nearly the same time.

Two writers and a race
sounds like it demands heavy machinery.

02 — Problem

The original sync was one-directional and batch-shaped — a reasonable v1. External systems periodically pulled appointments from us over HTTP and reconciled. But it was built to communicate created appointments; updates and deletes were second-class, and a periodic poll gives you neither promptness nor a clean story for changes originating on our side. As our platform became a real place to reschedule and cancel, two failure modes surfaced — worth separating, because they have different fixes.

One — changes got stuck or lost on the way out. With only a periodic pull and no acknowledgement, there was no answer to "did this change actually land over there?" A change could sit pending forever, indistinguishable from lost.

Two — the nasty one: overwrite races. A patient changes an appointment on our side. Before that reaches the external system, its periodic pull hands us its version — the stale one. Apply it naively and we overwrite our own un-synced change with old data. The user's action silently evaporates.

That second failure erodes trust one confusing incident at a time. "I definitely rescheduled that — why did it revert?" No error, no crash. Just a lost write, because two sources of truth disagreed and the wrong one won.

Fig 1 · The overwrite race, on a timeline — the bug is a specific interleaving
t0Patient reschedules on our side — record now holds the new truth
t1Change still in flight — the external system hasn't heard about it yet
t2Inbound pull arrives carrying the pre-change version
t3Naive apply → stale inbound wins → the user's change is gone

the fix criterion isn't "make it faster" or "lock something" — it's "don't let inbound stomp an un-acked local change"

Overwrite races only make sense on a timeline. Once you draw the interleaving, the deficiency is visible: the inbound path is missing one piece of knowledge — that this record is dirty on our side.
03 — Modeling the flow of truth

There's nothing to profile here. The investigation was modeling the flow of truth precisely enough to see where it broke. An outbound change and an inbound pull are two events racing; the bug is one specific interleaving. Draw it, and the fix criterion stops being about speed or locks.

That reframing is the whole ballgame. The problem isn't concurrency in the classic shared-memory sense — it's that the inbound path lacks a piece of knowledge: "this record is dirty on our side," which would let it make the right call.

Most people — me too, on a bad day — hear "two writers, same record, race" and immediately think locking or consensus. But locks across a boundary you don't control are a nightmare, and consensus is absurd overkill. The real output of the investigation was recognizing this needed a state marker, not a coordination protocol.

Fig 2 · The entire consistency model, as two states and a rule
dirty = true
Local change pending

A change originated on our side and hasn't been acknowledged yet.

inbound path → skip this record
ack received →
← local edit
dirty = false
In agreement

No pending local change; both sides believe the same thing.

inbound path → apply normally

"local change wins while it's pending; the other side retries until it catches up" — no locks, no consensus, no distributed transaction

The external system doesn't need to know the mechanism exists. From its side it just keeps trying until it gets our change — exactly what a retrying poller does anyway.

The whole consistency model fits in a sentence on purpose. I distrust elegance — but this earns it, because the elegance is the reduction: from a coordination protocol to a boolean and a rule that reads it.

04 — Alternatives considered

The instinct to reach for the sophisticated thing is exactly what I had to reject.

More frequent batch pull

Rejected

Polling harder gives no near-real-time outbound, no acknowledgement loop, and nothing against the overwrite race. It just does the inadequate thing more often.

Fire-and-forget outbound push

Rejected

No acknowledgement means no way to know it landed, no basis for retry, and still no protection against inbound clobbering. Speed without an ack loop just makes the loss faster.

Distributed locks / coordination protocol

Rejected firmly

You cannot reliably hold a lock across a boundary to an external, on-premise system that can be slow, unreachable, or down for maintenance. A held lock against an offline participant is a deadlock waiting to happen — a sledgehammer where the problem needs a light switch.

Exactly-once transactional two-way sync

Disproportionate

The heavyweight "correct" answer. Guarantees stronger than the problem needs, high cost, and it assumes a level of control over the external systems we simply don't have.

Dirty-flag guard + ack loop + pull fallback

Chosen

Track whether a record has an un-acked local change; have the inbound path respect that flag; use a real-time channel for prompt propagation and keep the pull as a reliable fallback; close the loop with an explicit acknowledgement. Light, robust, and honest that the external world is unreliable.

05 — Decision

Three cooperating mechanisms, each doing exactly one job.

1

The dirty-flag guard — the heart of it

Every appointment carries a marker: "has an un-synced local change." Set it true when a change originates on our side. The inbound path is taught one rule — skip any record whose marker is set. When the external system finally acknowledges our change, flip it back to false and inbound processing resumes.

2

Real-time propagation, with a pull fallback

Our-side changes push to the external systems in near-real-time. But real-time delivery is best-effort — an on-premise system can be offline — so the periodic pull remains as the reconciliation path. The fast path is never the only path. (The real-time transport itself was a platform choice; mine is the sync logic that rides on it.)

3

The acknowledgement loop — and updates/deletes done right

I rebuilt the pull so a single query returns created, modified, and deleted appointments, each tagged by the datastore with its change kind — one query, correctly typed results, instead of multiple application-side passes. A status write-back records completed or failed, so "in flight," "done," and "lost" are finally distinguishable. (The status/queue concept was co-designed with the scheduler team; I built our side.)

Ask what one side doesn't know
before asking how to lock it.

06 — Architecture: the two directions

Outbound is fast with a safety net. Inbound is safe because it reads one flag.

The dirty flag sits between the two directions as the arbiter. Everything else is transport and bookkeeping around that single piece of knowledge.

Fig 3 · Two directions, one arbiter
Our platform
patient books,
reschedules, cancels
outbound →
← inbound
the arbiter
Dirty-flag guard
outbound sets dirty=true
inbound skips if dirty
ack flips dirty=false
real-time + pull
typed pull
External system
on-premise · sometimes offline · retries the poll
near-real-time

Outbound changes push promptly; the pull is the fallback that makes the fast path safe to rely on.

one query, typed

Created / modified / deleted returned already tagged by the datastore — classification pushed down, not done in app code.

enforced everywhere

The skip-if-dirty rule is honored across every inbound path, including the per-vendor handlers — a guard in nine of ten paths is a race with better odds.

Precise boundary: the real-time transport was a platform-team choice and the ack/queue concept was shared across teams. What's mine is the sync logic that rides them and the dirty-flag consistency invariant enforced across the inbound paths.
07 — Trade-offs
Chose

Local-change-wins while pending

Not buying
Perfect conflict resolution. A small, bounded window of inbound staleness for one record. If the external side made a different legitimate change in that window — genuinely rare — we prefer ours until the dust settles. Silently losing a user's action is far worse.
Chose

Two propagation paths

Not buying
The simplicity of a single poll. Real-time-plus-fallback is more to reason about and monitor — but it's the only way to get near-real-time and reliability, where either alone gives one.
Chose

Unbounded retry, for now

Not buying
A defined permanently-unreachable case. If an external system never acknowledges, a record stays dirty and inbound keeps skipping it. In practice they come back — but the bound on that retry is exactly what I'd want answered before calling this bulletproof.

The honest framing is "local-change-wins-while-pending," not "we handle every conflict perfectly." Naming the edge is part of the trade.

08 — Outcome
mechanism code-verifiable · fleet magnitude is a dashboard I'd point at
0

Distributed locks in the design. The overwrite race — the trust-eroding one where a user's change silently reverts — is structurally prevented by the dirty-flag guard, not coordinated away.

correctness
0 of 2

Failure modes eliminated. Outbound changes reach the external systems near-real-time with a reliable pull fallback; updates and deletes are first-class, carried by a single well-formed query.

reliability
0+

Clinics kept consistent with their external systems, in both directions, continuously — without a single distributed lock. (Per-day change volume and a before/after on "stuck pending" are numbers I'd pull from monitoring, not assert from memory.)

reach
09 — Lessons learned
Reframe "concurrency" as "missing knowledge" before reaching for coordination. The race looked like a two-writer problem — the category that makes people reach for locks. The real deficiency was that the inbound path didn't know a record had a pending local change. Supply the knowledge and the fix collapses to a boolean.
You can't lock what you don't control. The external systems are on-premise, sometimes offline, outside our authority. Any design assuming you can hold a lock across that boundary is fantasy — sync with unreliable partners has to degrade gracefully, not coordinate tightly.
A fast path needs a fallback to be trustworthy. Real-time push is great until the participant is offline. Keeping the humble periodic pull as the reconciliation path is what lets you rely on the fast path — primary-plus-fallback beats either alone.
Close the loop with an explicit acknowledgement. Without a status signal, "in flight," "done," and "lost" are indistinguishable and you can't retry intelligently. An ack loop is what turns hope into a state machine.
Enforce an invariant everywhere it can break. The dirty-flag guard is only as strong as its least-guarded inbound path — including the awkward per-vendor ones. Consistency rules have to be honored at every entry point, not just the obvious spot.
10 — If I rebuilt this today

The core idea — a dirty flag and a retry instead of a lock and a consensus — I'd keep without hesitation. Three things I'd finish.

Change

Bound the retry; define the unreachable case

The one honest soft spot is what happens when a system never acknowledges. I'd add an explicit policy — alert after N failed attempts, a distinct "chronically unreachable" state, operator visibility — so the tail case is designed rather than incidental.

Change

Make the invariant structural, not disciplinary

Today "respect the dirty flag" is a rule enforced across many handlers — a new one could forget it. I'd route all inbound application through a single choke point that checks the flag once, so the guard can't be accidentally omitted. Turn a discipline into a structural guarantee.

Learned

Build the sync-health observability

Per-client, per-direction: is sync flowing? how stale is it? how many records are dirty and un-acked, and for how long? That dashboard turns "a client reports their changes aren't syncing" into "we saw it first, and here's exactly which records."

The instinct I'd want a reader to leave with isn't the flag itself — it's the reflex to ask how little coordination the problem actually requires before reaching for the sophisticated thing. The best distributed-systems solution is usually the least distributed one you can get away with.

© 2026 Sukhcharan SinghCase study · A dirty flag beat a consensus protocol