Keeping Implementation From Drifting From Design — A Divergence Register and Three-Way Cross-Check That Protect the 'Source of Truth'
What You’ll Learn
- How to manage “design vs. implementation drift” without deleting the design
- A table format that links divergence IDs (DVG-XXX) and ticket IDs (TASK-XX)
- How to build a three-way cross-check gate between architectural / detailed / API designs that surfaces contradictions
Target Audience
- Anyone struggling with the “implementation runs ahead of design and they don’t match anymore” problem
- People stuck between the cost of constantly rewriting design and the cost of letting it rot
- Those interested in running validation commands alongside documentation operations
Environment
| Item | Content |
|---|---|
| Doc format | Markdown |
| Repo layout | e-scooter-sharing-doc managed as a separate repo |
| Validation | ripgrep (rg) |
Series article (in active development) — This article is part of Building Street-Level E-Scooter Sharing From Scratch to Understand It — Design, Implementation, and Operations Series. The project is ongoing; new articles and design notes are added continuously. The series motivation, technology stack, screen-ID glossary, and the full article index are collected at the link above.
Introduction
In the e-scooter sharing project, requirements / architectural / detailed / API designs were declared the source of truth from the start.
As implementation progressed, screen transitions, API endpoints, and status names slowly drifted. The usual responses are:
- Rewrite the design each time → the design intent fades
- Leave the design alone → design and implementation diverge and confuse new members
We took a third path.
Keep the design body as the source of truth, and track the diff against the current implementation in a “divergence register.”

The pins on the map embody “fetched from the API per design, color-coded gray / dark blue by state.” We tracked four divergences (DVG-001〜DVG-004) in the register before getting here.
Divergence Register (DVG-XXX)
Create 30_user-app/002_detailed-design/divergence-register-(S01-S13).md and manage per-screen drift in a table.
| DVG-ID | Screen | Designed (truth) | Implementation reality | Impact | Workaround | TASK-ID | Status |
|---|---|---|---|---|---|---|---|
| DVG-001 | S01 | Initial port master fetched via GET /api/v1/ports | Initial fetch implemented | First-display latency resolved | none | TASK-A4 | Resolved |
| DVG-002 | S07 | S04 guard routes to S07 when test not passed | S07 screen / route / API send implemented | Reduces operational dependence | none | TASK-B1 | Resolved |
Rules:
- Don’t erase a divergence. Only update status (Open / In progress / Resolved / Design change agreed).
- Don’t touch the design body (truth). Only the divergence register reflects facts.
- Only “Design change agreed” allows rewriting the truth side.
The payoff of this format: “the fact that implementation drifted from design” stays in the doc. Rewriting design erases history; the divergence register turns it into a record.
Implementation Diff Task List (TASK-XX)
Work to resolve divergences is managed in 30_user-app/002_detailed-design/implementation-diff-tasks-(app-api).md as tickets.
### A-1 Consolidate state guards
- Target: app
- Content: Implement RouteGuardService.determineNextRoute() and consolidate the S05->S08->S09 transition logic into one place.
- Acceptance:
- S04 "Proceed to ride setup" must always go through the shared guard.
- Order of judgement matches the design.
- Status: Resolved (2026-05-23 / app commit: 34f9009)
Sort by “Priority A (resolve soon)” / “Priority B (next phase)” / “Priority C (quality)”, with ticket IDs cross-referenced to divergence IDs.
Completion is logged as “Implementation” + “Test” + “Design update” — a three-piece set.
| Content | |
|---|---|
| Implementation | Commit ID (e.g. 34f9009) |
| Test | Evidence of acceptance condition met (test pass, screen check, etc.) |
| Design update | Record of flipping the divergence register status to “Resolved” |
Three-Way Cross-Check (Architectural / Detailed / API)
For each S01〜S13 screen, tabulate the consistency among three design artifacts.
| Screen | Architectural | Detailed | API design | Verdict |
|---|---|---|---|---|
| S05 | OTP send / verify | sendOtp(phone), verifyOtp(phone, code) | POST /auth/send-otp, /auth/verify-otp | OK |
| S10 | lock-toggle | toggleLock(rentalId, requested) | POST /rentals/{rental_id}/lock-toggle | Partial (future) |
Verdict can be “OK / Partial / Design change needed / Implementation change needed”. Anything less than full OK gets a divergence ID and goes into the divergence register.

For example, “Return here” being disabled for a full port is the picture of the architectural (“toggle button by returnability”), API (“returns available_empty_slots”), and detailed (“button enable logic in child sheet”) designs all reaching the same verdict — OK — in the three-way table after implementation.
The important thing here:
- API design is the truth at the endpoint string level
- Architectural / detailed designs are the truth at the behavioral business level
- The three-way table is a place to confirm that three axes are pointing at the same fact
If implementation renames /api/v1/foo → /api/v1/bar, you re-agree on the three-way table before rewriting the API design “truth.” That’s the heart of the no-drift mechanism.
Validation Gates and ripgrep
As the divergence register grows, manually checking “is everything resolved” becomes unrealistic. Validation gates fix this.
# Gate A: outside OLD, no main paths missing /api/v1 prefix
rg -n "`(GET|POST|PATCH|PUT|DELETE) /(auth|users|ports|scooters|rentals|payments|ekyc)" 00_common 20_API 30_user-app --glob "!**/OLD/**"
# Gate B: presence check for main "diff notation needed" words
rg -n "implementation-diff-note|not-yet|future-impl" 30_user-app
# Gate C: integrity between three-way table and task list
rg -n "S10|partial|future-impl" 30_user-app/002_detailed-design
Gate A detects old API paths remaining in body — endpoints without the /api/v1 prefix.
Gate B is the coverage check for diff annotations on not-yet-implemented items.
Gate C verifies that judgements like “partial” or “future-impl” are used with the same meaning in both the divergence register and task list.
These can go into CI or be run manually. What matters is the gate exists, and slipping on design maintenance gets detected.
OLD Directories for History
When reorganizing the design, don’t “delete” old designs — move them into an OLD/ directory.
30_user-app/
├─ 001_architectural-design/
├─ 002_detailed-design/
│ ├─ OLD/
│ │ └─ old-payment-flow-design.md
│ ├─ payment-flow-design-(truth-aligned).md
│ └─ divergence-register-(S01-S13).md
Gate A’s --glob "!**/OLD/**" excludes OLD from the search. Only living designs get validated; history stays at hand.
Lessons
- Design / implementation drift is more realistic to “manage in a visible form” than to “eliminate”
- The divergence register / task list / three-way table are three axes with different roles, and mutual references protect the truth
- Validation runs fine as a lightweight ripgrep-based gate (no CI required)
- Don’t delete designs — move them to OLD to preserve history
The design isn’t an “infallible textbook.” It’s “a tool you can place at the center of decisions by treating it as the source of truth.” The mechanism to protect that, on this project, became the divergence register and the three-way cross-check.