Reverse-Engineering a Codebase Into Docs
When you land on a large enterprise codebase you have never touched, the default path is slow: read random files, ask whoever is awake in another time zone, and hope tribal knowledge eventually sticks. I took a different path. I reverse-engineered the workflow application from source and turned it into fourteen focused architecture documents so the delivery team could reason about the whole system without living inside every pull request.
The Situation
The enterprise platform was a familiar modern stack on paper: Next.js and TypeScript on the front, Prisma talking to a relational database, dozens of API routes, background workers for long-running jobs, separate surfaces for administrators and end-user dashboards. What was missing was any single place that explained how those pieces actually cooperated. There was no architecture documentation worth the name. The team was distributed across time zones, which meant “quick questions” were rarely quick.
I needed to understand the full system fast enough to contribute safely and to help others do the same. That meant treating documentation not as prose marketing but as an engineering artifact derived from the same source of truth as production: the code itself.
The constraint was familiar to anyone who has joined mid-flight: pull requests tell you what changed recently, not why the system exists in its current shape. API reference generators list endpoints but rarely explain ordering guarantees, failure modes, or which worker actually finishes a job. I assumed nothing from folder names alone; if a directory was called services but contained orchestration plus HTTP clients plus email templating, the doc said so explicitly.
The Methodology
I worked module by module, in a repeatable loop. First, a line-by-line audit of the relevant source: route handlers, services, workers, and anything that touched persistence. I traced imports both downward into implementations and upward into callers so I did not mistake a leaf utility for an entry point. From that pass I extracted explicit data flows: who calls whom, which tables or queues are read or written, and where side effects escape the request lifecycle. Those flows became Mermaid diagrams so readers could see movement at a glance instead of reconstructing it from fifty imports.
Where branching got dense, I captured it as conditional logic tables: input, condition, outcome. I compiled file inventories with a one-line description per file so newcomers could navigate without opening every tab. For behavior that is easy to misread from static code—retries, idempotency keys, race-prone updates—I added short behavioral notes. Every document ended with cross-references to sibling modules so nobody treated a slice of the system as if it lived in isolation.
Order mattered. I started with system overview and authentication because almost every other flow assumes those invariants. Pipeline and worker docs came before dashboards so readers understood what data existed before they read how it was surfaced. Payments and notifications referenced the same job identifiers the pipeline used, which prevented duplicate definitions of “done” across the enterprise platform.
Fourteen Modules, Fourteen Documents
I split the platform into fourteen coherent slices, each with its own markdown-style living doc checked into the repository alongside the code:
- System overview and state machine — high-level lifecycle of a work item and how global states relate.
- Authentication and access control — sessions, roles, route guards, and server-side enforcement.
- Submit and upload flow — how clients send payloads, validation, storage, and enqueueing.
- Pre-QA quality gates — automated checks before deeper processing.
- Pipeline and output generation — workers, orchestration, and artifacts.
- Expert review — human-in-the-loop steps and assignment.
- Post-QA scoring — metrics, aggregation, and persistence of results.
- Recovery and self-healing — retries, dead-letter behavior, reconciliation jobs.
- Admin panel — operational UI and privileged APIs.
- Dashboard — user-facing summaries and filters.
- Notifications — email, in-app, or webhook paths and templates.
- Payments and exports — billing hooks and downloadable deliverables.
- Database schema — Prisma models, critical indexes, and migration caveats.
- Observability and shared platform — structured logging, metrics, tracing, feature flags, and internal libraries used across routes and workers.
Fourteen documents only works if they connect: the overview and schema act as spine documents, while observability explains how you prove what you think is true when something misbehaves in production. Cross-links between modules turned the set into a single navigable map instead of isolated essays.
What Each Document Contains
Every module doc followed the same skeleton so reviewers knew where to look. At the top, one or two Mermaid diagrams showing how a typical request or job traverses API routes, services, workers, and the database. Below that, conditional logic tables for the gnarliest branches—especially where business rules had evolved in code comments instead of product specs. A file inventory section listed paths under that module with a single sentence each: what the file owns and what it must not do. Behavioral notes called out non-obvious patterns: optimistic locking that can fail under concurrency, background jobs that assume at-least-once delivery, or endpoints that look idempotent but are not. Finally, explicit “see also” links pointed to authentication, notifications, or schema docs so readers did not duplicate wrong mental models.
Example: Submit and upload flow (Mermaid)
Here is a generic sketch of how a submit path might look after reading the handlers and workers. Names are illustrative, not tied to any single product:
flowchart LR
A[Client: multipart upload] --> B[API route: validate schema]
B -->|valid| C[Service: persist draft row]
B -->|invalid| Z[4xx response]
C --> D[Object storage: put blob]
D --> E[Queue: enqueue process job]
E --> F[Worker: virus scan optional]
F --> G[Worker: promote draft to active]
G --> H[(Database: update status)]
H --> I[Notification: job accepted]
The point of the diagram is not beauty; it is agreement. Once this picture existed, debates about “where does validation live?” took minutes instead of days.
Example: Conditional logic for a pre-quality gate
Automated gates are where implicit rules hide. A table forces them into the open:
| Input | Condition | Outcome |
|---|---|---|
| Artifact size | Exceeds configured maximum | Reject with user-visible error; no enqueue |
| MIME type | Not in allowlist | Reject; log security event |
| Duplicate content hash | Matches existing completed job | Short-circuit to existing result reference |
| Rate limit key | Over threshold | Return 429; worker untouched |
When product or compliance asks “what exactly happens if…?” the answer is a row, not a archaeology session in Git blame.
Example: File inventory excerpt
Inventories are boring by design. Boring is searchable:
app/api/submit/route.ts — HTTP entry; schema validation only
lib/services/upload-service.ts — Orchestrates storage + DB transaction
lib/workers/process-upload.ts — Consumes queue; calls downstream pipeline
prisma/schema.prisma (section X) — Draft vs active enums and indexes
If a file appears in the inventory, it has a stated responsibility. If responsibility drifts, the inventory is the first thing that looks wrong during review.
Verification: Three Passes
Documentation that lies is worse than none. I used three explicit passes.
First pass — write from source. I drafted each section only after reading the implementation, not from memory or old slides. If I could not point to a function or route for a claim, the claim did not ship.
Second pass — cross-check every claim. For each bullet, diagram edge, and table row, I reopened the code path and confirmed the behavior still matched. This is tedious and exactly where most informal docs fail.
Third pass — fix discrepancies. I logged mismatches in a simple checklist: statement, expected per doc, actual per code, resolution (doc update vs bug filed). We found several cases where the code had drifted from the original intent—features that had been “fixed” in production without updating comments or shared understanding. Fixing the docs surfaced those gaps for the delivery team to prioritize.
When a diagram and the implementation disagreed, I treated the code as authoritative for “what runs today” and the doc as authoritative for “what we want maintainers to believe”—then reconciled the two. That sometimes meant correcting the diagram; other times it meant opening a defect because the running behavior was unsafe or inconsistent with adjacent modules.
The checklist itself stayed lightweight—something any engineer can reuse:
- Claim quoted verbatim from the doc
- File and symbol that justify the claim
- Pass / fail
- If fail: update doc, open issue, or both
Treating verification as a repeatable procedure meant new contributors could extend the docs without inheriting my mistakes.
The Impact
The practical effect was speed with less chaos. New team members could walk the full system in hours instead of weeks because they had a guided path through fourteen bounded contexts instead of an infinite tree of repositories. Code reviews accelerated: reviewers could anchor comments in shared diagrams (“this changes the transition from state B to C”) instead of guessing side effects. Production incidents shortened because on-call engineers could identify which module owned a symptom—worker backlog versus API validation versus notification fan-out—and open the right runbook first.
None of this replaced the need to read code when changing behavior. It reduced the cost of getting to the right code the first time. For a distributed team on an enterprise platform, that is the difference between shipping and thrashing.
The workflow application kept evolving after the first publish. I encouraged the team to treat a doc change as part of the same change set when behavior moved: if you alter a state transition or a gate, you update the diagram or table in the same pull request. That habit is what keeps reverse-engineered docs from rotting into wallpaper.
Architecture documentation is not a one-time brochure. It is a maintained map. If you treat it as an output of the same discipline you use for tests and types—clear contracts, verified against source—it pays back in every timezone.