On a phone? for plain text and static diagrams.
Understanding a Codebase
Before you change the code, understand the system.
The system
The feature
The change
Acceleration
The system
What does the system do, and for whom?
Business Context
- Business Context: What does the system do, and for whom?
- Repository Structure: Where does everything live?
- Architecture: Which components talk to each other?
- Runtime Flow: What happens when it starts?
- Feature Flow: Where does this feature travel?
- Business Logic: Where are the decisions made?
- Data Flow: What shape is the data at each step?
- Impact Analysis: What could my change affect?
- Codebase Clearance: Can I explain it to another developer?
- AI-Assisted Discovery: How do I go faster without guessing?
The first question is not “which file?”
Tickets arrive small. Add a new status. Change this API. Add a new button. Fix this calculation.
The common move is to search for the file whose name looks closest to the ticket, open it, and start typing. Sometimes that works. More often the change lands in the layer you happened to open instead of the layer that owns the behaviour, and the rest of the system finds out in UAT.
The real first question is: how does this part of the system actually work?
- UI
- Frontend state
- API
- Backend
- Business logic
- Database
- Integrations
- Tests
- Reports
- Notifications
Add a new status. crosses 10 of 10 layers. A status is read everywhere: badges, filters, rules, emails, monthly numbers.
Answering it means building a codebase mental model: a picture in your head of who uses the system, where its code lives, which components talk, how one feature travels through them, where decisions are made, how data changes shape, and what a change would ripple into. This episode builds that model in nine gates, on one realistic system, and only then shows where AI makes it faster.
What does “understanding a codebase” mean?
You progressively move from what does the system do? to where does this feature live? to what could my change affect? The journey at the top of the page is the whole method. By the end, you should be able to answer these thirteen questions about any system you join:
- 1What problem does this system solve?
- 2Who uses it?
- 3What are its major business workflows?
- 4How is the repository structured?
- 5What are the major architectural components?
- 6How does the application start?
- 7How does a feature travel through the system?
- 8Where does business logic live?
- 9How does data move through the system?
- 10What could be affected by a change?
- 11What should be investigated before coding?
- 12How can AI accelerate this investigation?
- 13How do we validate AI's findings?
The outcome: you can explain the system before you change the system.
The lab system: a Warranty Claim Application
Customers submit warranty claims for products that failed. Administrators review each claim and approve or reject it. Every gate below investigates this same system, so by the end you will know it the way you would know a codebase after your first two weeks.
You will trace one existing feature, Approve Warranty Claim, and then analyse one change: add REJECTED to the claim status.
- Next.js
- React
- TypeScript
- NestJS
- Prisma
- PostgreSQL
- REST API
- Vitest
- Playwright
The technologies are only the laboratory. The subject is how an engineer builds a mental model.
1warranty-platform/2├── apps/3│ ├── web/4│ │ ├── app/5│ │ ├── components/6│ │ ├── features/7│ │ └── lib/8│ └── api/9│ └── src/10│ ├── modules/11│ ├── common/12│ └── main.ts13├── packages/14│ ├── ui/15│ ├── types/16│ └── validation/17├── prisma/18│ └── schema.prisma19├── tests/20│ ├── e2e/21│ └── fixtures/22├── package.json23├── pnpm-workspace.yaml24└── turbo.json
Gate 01
Business Context
“What does this system do, and for whom?”
Concept
Before you open the code, understand the business. Code is a translation of business decisions; if you do not know the original, you cannot tell a rule from an accident.
Why it matters
Skip it and every name in the code is a guess. You will read ClaimStatus.PENDING without knowing who is waiting, for what, and what happens if they wait too long.
Actor
Customer
Bought a product that failed within warranty.
Action
Submits a warranty claim
Product, serial number, purchase date, receipt photo, description of the fault.
Process
Claim created
Status PENDING. The customer receives a reference number.
Actor
Administrator reviews
Checks the warranty period, the serial number and the receipt.
Decision
Valid and within warranty?
The only decision point in the workflow.
Outcome
Approved
Repair or replacement arranged; the customer is notified.
What to inspect
- Product or onboarding documentation
- The epic and its acceptance criteria
- The running app, used once as each role
- Seed data in prisma/seed.ts
- A 15 minute conversation with the product owner
Hands-on task
Use the staging app as a customer, then as an administrator. Submit one claim and decide it. Write down every actor, action and outcome you saw, without opening the repository.
Clearance questionCan I describe the claim workflow to a new teammate without mentioning a single file?
Gate 02
Repository Structure
“Where does everything live?”
Concept
Now enter the codebase, but do not open random files. Identify the boundaries first: which folders are applications, which are shared, where the schema and tests are, and which files configure the whole workspace.
Why it matters
Skip it and you will edit apps/web/components/StatusBadge.tsx without noticing that packages/ui exports the StatusBadge everyone else imports. Repository structure is not the architecture. It is only the first map.
- warranty-platform/
Web
Next.js app for customers and administrators.
Open first
- apps/web/app/(admin)/claims/[id]/page.tsx
- apps/web/features/claims/
- apps/web/lib/api/claims.ts
- Web (apps/web): Next.js app for customers and administrators. Open first: app/(admin)/claims/[id]/page.tsx, features/claims/, lib/api/claims.ts
- API (apps/api): NestJS REST API. Owns rules and data. Open first: src/main.ts, src/app.module.ts, src/modules/claims/
- Shared Packages (packages/): Code both apps import: ui components, types, validation schemas. Open first: types/src/claim.ts, ui/src/StatusBadge.tsx, validation/src/claim.ts
- Database (prisma/): Schema, migrations and seed data. The shape of every table. Open first: schema.prisma, migrations/, seed.ts
- Tests (tests/): Playwright journeys across both apps, plus fixtures. Open first: e2e/approve-claim.spec.ts, fixtures/claims.ts
- Configuration (root files): Workspace, task graph and scripts that tie it together. Open first: package.json, pnpm-workspace.yaml, turbo.json
What to inspect
- pnpm-workspace.yaml
- turbo.json
- package.json scripts
- apps/*/package.json
- packages/*/package.json
- prisma/schema.prisma
Hands-on task
Run tree -L 3 -I node_modules and cat pnpm-workspace.yaml. For every top-level folder, write one sentence: what it is responsible for and who depends on it.
Clearance questionIf a type changes in packages/types, can I name every application it reaches?
Gate 03
Architecture
“Which major components communicate with each other?”
Concept
Architecture is the runtime picture: which components exist while the system runs, and how they talk. A folder is not a component; a NestJS module that owns a database table and sends email is.
Why it matters
Skip it and you will miss the parts that are not in the request you are looking at: the email sent after approval, the file storage holding receipts, the manufacturer API that receives approved claims.
NestJS API
Claims module talks to: Next.js (REST /api/claims), Auth module (guards check permissions), PostgreSQL (Prisma), Object storage (signed upload URLs), Notification module (ClaimApproved event), Integrations module (ClaimApproved event).
Evidence: src/modules/claims
What to inspect
- apps/api/src/app.module.ts imports
- Environment variables that hold URLs
- HTTP clients and SDKs in package.json
- docker-compose.yml services
Hands-on task
Open app.module.ts and list every imported module. Then search the API for outbound calls (rg -n "fetch\(|HttpService|S3Client|createTransport" apps/api/src). Draw boxes and arrows.
Clearance questionCan I draw every component that a claim touches, including the ones outside our code?
Gate 04
Runtime
“What actually happens when I run the application?”
Concept
Follow the start-up path from the command you type to the moment the application is ready. Know how development starts, how production starts, where configuration comes from and what must already be running.
Why it matters
Skip it and the first failure costs you an afternoon: a missing variable, a database that is not up, a port taken, a production build that behaves differently from dev.
terminal
One command starts everything in development.
1pnpm dev
- Command (terminal): One command starts everything in development.
- Scripts (package.json, turbo.json, pnpm-workspace.yaml): The root script hands off to Turbo, which runs `dev` in every workspace package that has one.
- Application Bootstrap (apps/api/src/main.ts, apps/web/package.json): Next.js starts on 3000. NestJS creates the app module, sets the /api prefix and global validation, then listens on 3001.
- Configuration (.env, apps/api/src/config/env.ts): Variables are read and validated on boot. A missing required value stops the API before it serves anything.
- Dependencies (docker-compose.yml, prisma.service.ts): PostgreSQL runs in Docker. Prisma connects when its module initialises; the mail transport is created lazily.
- Application Ready (logs): Web on :3000, API on :3001/api, health check green.
- DATABASE_URL missing: Error: Config validation error: "DATABASE_URL" is required. Fix: Copy .env.example to .env. The schema in config/env.ts lists every required variable.
- PostgreSQL not running: PrismaClientInitializationError: Can't reach database server at localhost:5432. Fix: docker compose up -d postgres, then pnpm prisma migrate dev.
Development and production start differently
- DevelopmentProduction
- Start
- Devpnpm dev (turbo, watch mode)
- Prodnode dist/main.js and next start, from the Dockerfile
- Build
- DevNone; compiled on the fly
- Prodpnpm build: nest build and next build
- Configuration
- Dev.env on your machine
- ProdEnvironment variables injected by the platform
- Database
- DevPostgreSQL in docker compose
- ProdManaged PostgreSQL; migrations run in the release step
What to inspect
- package.json scripts
- pnpm-workspace.yaml
- turbo.json
- apps/api/src/main.ts
- apps/web/next.config.ts
- .env.example
- docker-compose.yml
- Dockerfile
- prisma/schema.prisma datasource
Hands-on task
Clone the repository on a clean machine and follow the runtime trace below. Each time something fails, write down which file told you how to fix it.
Clearance questionIf the app fails to start tomorrow, do I know the first three places to look?
Gate 05
Feature Flow
“How does one feature travel through the system?”
Concept
Pick one existing feature and trace it end to end: Approve Warranty Claim. Do not start coding. Follow the actual implementation from the button to the database, then follow the response back to the screen.
Why it matters
Skip it and you will add your change at the layer you happened to open, not the layer that owns the decision. Tracing one feature teaches you the conventions every other feature follows.
Request travelling down
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
apps/web/features/claims/ApproveClaimButton.tsx
① ApproveClaimButton
- What is it?
- A React component on ClaimDetailsPage.
- Responsibility
- Showing the action and its loading and error state.
- Receives
- A click, the claim id from the page.
- Returns
- Nothing; it triggers a mutation.
- Calls next
- useApproveClaim, which calls claimApi.approve()
1export function ApproveClaimButton({ claimId }: { claimId: string }) {2const approve = useApproveClaim();3return (4<Button disabled={approve.isPending} onClick={() => approve.mutate({ claimId })}>5Approve6</Button>7);8}
① UI, ApproveClaimButton apps/web/features/claims/ApproveClaimButton.tsx. Showing the action and its loading and error state. Receives a click, the claim id from the page. Returns nothing; it triggers a mutation. Next: useApproveClaim, which calls claimApi.approve().
② API Client, claimApi.approve() apps/web/lib/api/claims.ts. URL, headers, credentials, turning HTTP errors into ApiError. Receives claimid and an optional note. Returns promise<claimresponse>, or throws apierror(status). Next: POST /api/claims/:id/approve.
③ HTTP Endpoint, POST /api/claims/:id/approve contract (apps/api, packages/types). The contract: path, method, body, status codes. Receives json { note?: string } and the session cookie. Returns 200 claimresponse, or 400, 403, 404, 409. Next: NestJS routes it to ClaimsController.approve().
④ Controller, ClaimsController.approve() apps/api/src/modules/claims/claims.controller.ts. Routing, guards, reading params and the current user. No rules. Receives id, the validated dto, the user from the guard. Returns whatever the service returns, mapped to a response dto. Next: ClaimsService.approve().
⑤ DTO, ApproveClaimDto apps/api/src/modules/claims/dto/approve-claim.dto.ts. Input validation: type, length, unknown fields stripped. Receives the raw json body. Returns a validated object, or 400 before the controller runs. Next: Handed to the controller, then the service.
⑥ Service, ClaimsService.approve() apps/api/src/modules/claims/claims.service.ts. The business rule: only a PENDING claim can be approved. Receives id, dto, user. Returns the updated claim, or throws 404 or 409. Next: ClaimsRepository.decide(), then emits ClaimApproved.
⑦ Repository / ORM, ClaimsRepository.decide() apps/api/src/modules/claims/claims.repository.ts. Data access. The WHERE guards against a concurrent decision. Receives id, target status, decider id, note. Returns the updated claim model. Next: Prisma sends SQL to PostgreSQL.
⑧ Database, PostgreSQL claims table prisma/schema.prisma, prisma/migrations. Storage, the claim_status enum, foreign keys. Receives one update statement. Returns the number of rows changed, then the row. Next: The response starts its way back up.
On the way back:
- PostgreSQL: Returns 1 row changed, then the updated row.
- Prisma: Maps the row to a Claim object: camelCase fields, Date values.
- Service: Emits claim.approved for email and the manufacturer, returns the Claim.
- Controller: toResponse() drops internal fields and builds ClaimResponse.
- HTTP Response: 200 OK with the JSON body.
- API Client: Parses JSON into a typed ClaimResponse.
- Frontend State: onSuccess invalidates ["claims", id]; the query refetches.
- UI: StatusBadge shows Approved; the decision buttons disappear.
What to inspect
- The page route under apps/web/app
- The feature folder apps/web/features/claims
- The API client in apps/web/lib/api
- ClaimsController, ApproveClaimDto, ClaimsService
- ClaimsRepository and the Claim model
Hands-on task
Start at the Approve button. Use go-to-definition, never guessing, until you reach the SQL. At each hop write: what it receives, what it returns, what it calls next.
Clearance questionCan I name the file and function at every hop, in both directions?
Gate 06
Business Logic
“Where are the decisions made?”
Concept
Finding the code is not enough. You must understand the responsibility of the code. Separate UI logic, input validation, authorization, business rules and data access, because each belongs to a different layer.
Why it matters
Skip it and you will copy a rule into the place you are editing. Now the PENDING check exists in the button, the controller and the service, and next year someone changes only one of them.
This lab builds on Gate 05: Feature Flow. Finish it first, then come back.
setIsLoading(true);
if (!claimId) { throw new Error("Claim ID required"); }@RequirePermission("CLAIM_APPROVE")if (claim.status !== "PENDING") { throw new ConflictException(); }prisma.claim.updateMany({ ... })enum ClaimStatus { PENDING APPROVED }
0 of 6 sorted.
1UI
Presentation
Deciding what is allowed
2Controller
Request handling
Business rules
3Service
Business logic
HTTP or SQL details
4Repository / ORM
Data access
Deciding outcomes
5Database
Persistence
Workflow decisions
What to inspect
- Conditions inside services
- Guards and decorators on controllers
- DTO decorators and zod schemas
- Database constraints and enums
- Conditional rendering in components
Hands-on task
Search for every place that mentions PENDING: rg -n "PENDING" apps packages prisma. Label each hit as UI logic, validation, authorization, business rule or data access.
Clearance questionFor the rule I am about to change, can I point at the one place that enforces it?
Gate 07
Data Flow
“What shape is the data at each step?”
Concept
Request flow is the path a call takes. Data flow is how the data changes shape on that path. The same claim is a form model, a JSON payload, a DTO, a domain object, a database row, a response DTO and a UI model.
Why it matters
Skip it and you will add a field to the DTO but not to the response mapper, or format a date in the service that the UI formats again. Most "it saved but does not show" bugs live between two representations.
This lab builds on Gate 05: Feature Flow. Finish it first, then come back.
User Input
the textarea and the Approve click
Receipt matches serial number
What changed: Free text, typed by Nurul Huda, the reviewer.
- User Input (the textarea and the Approve click)
Receipt matches serial numberFree text, typed by Nurul Huda, the reviewer. - Form Model (React state, zod schema)
{ note: "Receipt matches serial number" }Trimmed and checked for length on the client. - Request Payload (fetch body)
{"note":"Receipt matches serial number"}Serialised to JSON. The id travels in the URL, not the body. - DTO (ApproveClaimDto)
ApproveClaimDto { note: "Receipt matches serial number" }Validated again on the server; unknown fields stripped. - Domain Object (ClaimsService)
Claim { id: "7f3c…", status: "APPROVED", decidedById: "u_219", decidedAt: Date, decisionNote: "Receipt matches…" }note becomes decisionNote; status changes; a real Date appears. - Database Record (claims table)
status = 'APPROVED'::claim_status, decided_by_id = 'u_219', decided_at = 2026-10-10 02:14:07+00, decision_note = '…'snake_case columns, an enum type, time stored in UTC. - Response DTO (toResponse())
{ "id": "7f3c…", "status": "APPROVED", "decidedAt": "2026-10-10T02:14:07.000Z", "decidedBy": { "name": "Nurul Huda" } }Date becomes an ISO string; decidedById becomes a name; internal fields dropped. - UI Model (toClaimView())
{ statusLabel: "Approved", tone: "positive", decidedAtText: "10 Oct 2026, 10:14 am", canDecide: false }Human label, local time, and a boolean the buttons read.
What to inspect
- Form state and zod schema
- API client types
- DTO classes
- Prisma model in schema.prisma
- Response mappers (toResponse)
- UI view-model helpers
Hands-on task
Follow one field, the decision note, from the textarea to the database and back to the screen. Record its name, type and format at every step.
Clearance questionIf I add a field, do I know every representation that must learn about it?
Gate 08
Impact Analysis
“What could this change affect?”
Concept
The business flow has a Rejected outcome, but today the code only supports approval. The ticket: add REJECTED to the claim status. Before touching code, identify every place where that status is interpreted, transformed, displayed or persisted.
Why it matters
A small code change can create a large system impact. The enum compiles in five minutes; the monthly approval-rate report silently changes its denominator.
This lab builds on Gate 05: Feature Flow. Finish it first, then come back.
change
Claim Status
+ REJECTED
Select an area to see the file, the reason, and how to verify it.
What to inspect
- Every reader of ClaimStatus (
rg -n "ClaimStatus|status" --type ts) - Exhaustive switches and maps keyed by status
- SQL and report queries
- Email templates
- Seed data and fixtures
Hands-on task
Click through the impact map below, then write your own list for a field your next ticket touches. For each entry: file, reason, and how you will verify it.
Clearance questionCan I list what could break, and how I would notice if it did?
Gate 09
Codebase Clearance
“Can I explain this change before writing it?”
Concept
Clearance is the point where your mental model is complete enough to start. Not perfect: complete enough that the remaining unknowns are written down and owned.
Why it matters
Skip it and you discover the system while changing it, which means your first draft is also your investigation, and your reviewer is the one who finds the gaps.
This lab builds on Gate 08: Impact Analysis. Finish it first, then come back.
Codebase Understanding Clearance
0/10
Can I explain this change and its impact to another developer before writing code?
Business
0 of 4
Architecture
0 of 4
Code
0 of 5
Data
0 of 3
Testing
0 of 3
Impact
0 of 4
What to inspect
- Your notes from gates 01 to 08
- The open questions you still have
- The tests that exist for this area
Hands-on task
Tick every line below that you can answer with evidence. Anything you cannot tick goes into the ticket as an explicit unknown.
Clearance questionCan I explain this change and its impact to another developer before writing code?
AI tips: use AI to accelerate codebase discovery
Once you know how to study a codebase manually, AI can significantly accelerate the investigation. Everything you did in the nine gates is now a checklist for judging what an assistant tells you.
Where it genuinely helps
- Searching large repositories
- Following references
- Summarising modules
- Finding usages
- Identifying dependencies
- Drafting an architecture map
- Tracing API flows
- Listing potentially affected files
- Surfacing unknowns
AI output is a hypothesis until validated against the actual codebase.
Do not ask “explain this codebase” and paste the answer into your notes. Form your own model first, then ask narrow questions that force the assistant to cite files and symbols, and to say UNKNOWN when it does not know. Evidence you can open beats a confident paragraph.
The prompt library
Five prompts, one per gate family. Each forces evidence and gives the assistant permission to not know. Under each prompt: how you validate what comes back. The last one matters most. Strong engineers ask what they know, and then: what don’t I know yet?
Repository mapping
1Analyse this repository and create a codebase map.23Identify:45- applications6- modules7- shared packages8- database9- integrations10- tests11- infrastructure12- configuration1314For each area:15161. Explain its responsibility.172. Reference the relevant files.183. Identify the entry points.194. Mark uncertain findings as UNKNOWN.2021Do not invent implementation details.
How to validate: Validate: open each entry point it names. If a file does not exist or does something else, the map is wrong there, and probably nearby.
This lab builds on Gate 05: Feature Flow. Finish it first, then come back.
AIApproveClaimButton calls claimApi.approve() through useApproveClaim.
AIThe PENDING check is enforced in ClaimsController.
AIOnly administrators can approve claims.
AIApproving a claim emails the customer.
AIClaims are soft-deleted with a deletedAt column.
AIThe endpoint is POST /api/claims/:id/approve.
Manual vs AI-assisted discovery
The goal is not human, AI, answer. It is human, AI acceleration, human validation, understanding. The assistant shortens the search. The engineer still inspects the evidence, corrects the assumptions and owns the final model.
Manual
- Developer
- Read code
- Search files
- Follow references
- Build mental model
AI-assisted
- Developer
- Ask AI for initial discovery
- Inspect evidence
- Validate findings
- Correct AI assumptions
- Build mental model
Not human, AI, answer. Human, AI acceleration, human validation, understanding.
Key takeaways
Principle 01
Don't start with the file. Start with the system.
Principle 02
Understand the business flow before the technical flow.
Principle 03
Trace one feature end to end.
Principle 04
Know where the business logic lives.
Principle 05
Understand the data, not just the request.
Principle 06
Think about impact before implementation.
Principle 07
Identify what you don't know.
Principle 08
Use AI to accelerate discovery, not replace understanding.
Rebuild the order from memory
- Gate 01?
- Gate 02?
- Gate 03?
- Gate 04?
- Gate 05?
- Gate 06?
- Gate 07?
- Gate 08?
- Gate 09?
Which gate comes first?
Closing
Good developers know where the code is.
Good engineers understand why the code is there.
Next: Season 01, Episode 03
Unit testing in development
You can now explain the system and the impact of a change. Next: prove the change works, layer by layer, before anyone else has to.
Publishes