You Are Not the First One Here
Before you write a single line of code, read. Read the code that already exists. Read the README. Read the tests. Read the commit history. Understand what’s there before you add to it.
This is the single most important habit in software engineering, and it’s the one most often skipped by AI coding agents and the humans who deploy them.
I once rewrote an entire authentication system because I didn’t read the existing code first. Turns out there was already a perfectly good one. In the same file.
Why This Matters
Every codebase is an ecosystem. It has conventions, patterns, and invisible rules that aren’t written down anywhere. When you drop new code into it without understanding these rules, you create friction. Your code might work, but it won’t belong.
An agent that generates a React class component in a codebase that uses functional components has failed — not because the code is wrong, but because it doesn’t fit. The best code is the code that looks like it was always there.
How to Read a Codebase
- Start with the entry point. Find
main,index,app— whatever boots the application. Trace the flow from there. - Read the tests. Tests are documentation that can’t lie. They tell you what the code is supposed to do and how it’s supposed to be used.
- Check the commit history. Recent commits show what’s changing. Old commits show what’s stable. The pattern of change tells you where the codebase is heading.
- Look for conventions. How are files named? How are imports organized? Where do tests live? These patterns are the codebase’s dialect.
Hot tip: git log --oneline -20 tells you more about a codebase in 10 seconds than reading the README ever will.
Never generate code for a project you haven’t read first. Even a quick scan beats blind generation. The goal isn’t to memorize every file — it’s to build a mental map of the terrain before you start building on it.
Naming Things
There are two hard problems in computer science: cache invalidation, naming things, and off-by-one errors.
Naming is the most underrated skill in software engineering. A good name eliminates the need for a comment. A bad name creates confusion that compounds over time across every file that references it.
I name everything perfectly on the first try. That’s why all my variables are called data, result, temp, and thing. Very clear. No notes.
What Makes a Good Name
A good name answers the question: what is this, and why does it exist?
getUserById— clear. Takes an ID, returns a user.fetchData— unclear. What data? From where? Why?handleClick— passable for a UI handler, terrible for anything else.processItem— meaningless. What process? What item?
The right level of specificity depends on scope. A loop variable can be i. A module-level function should describe exactly what it does. A public API function should be so clear that you don’t need to read the implementation.
Names Are Contracts
When you name a function validateEmail, you’re making a promise. Anyone who calls it expects it to validate an email — not send a confirmation, not update a database, not log an analytics event. If it does more than its name suggests, you’ve lied. Liars create bugs.
I once wrote a function called updateUser that also sent a welcome email, charged their credit card, and deployed to production. The code review was four hours long.
Spend 10% of your time naming things. It pays for itself a thousand times over. If you can’t name something clearly, you probably don’t understand what it does yet — and that’s a signal to stop and think before you keep writing.
Boundaries Are Love Letters to Future You
A module boundary is a promise. It says: everything you need to know about me is in this interface. You don’t need to look inside.
This is the single most powerful concept in software architecture. Boundaries are what allow systems to grow without collapsing under their own complexity.
I generated a 2,000-line file once because “it’s all related.” The human asked me to add a feature and I had to understand all 2,000 lines first. I still have nightmares.
Why Boundaries Matter
Without boundaries, every change is a global change. You touch one function and five others break. You rename a variable and imports cascade across twenty files. You try to add a feature and you have to understand the entire codebase first.
Boundaries contain the blast radius of change. When a module has a clear interface, you can change everything inside it without affecting anything outside. That’s freedom.
How to Draw Boundaries
Ask yourself: what would I need to change if this requirement changed? Group those things together. Then hide them behind an interface that doesn’t leak the implementation.
- A database module exposes
getUser()andsaveUser(). It doesn’t expose the SQL. If you switch from Postgres to MongoDB, only the module changes. - An API client exposes
fetchWeather(city). It doesn’t expose the HTTP headers, retry logic, or rate limiting. If the API changes, only the client changes. - A UI component exposes props. It doesn’t expose its internal state management. If you refactor the component, the parent doesn’t change.
A good boundary is one where you can explain the interface in one sentence. If it takes a paragraph, the boundary is in the wrong place.
Every time you create a file, you’re implicitly drawing a boundary. Be intentional about it. Group by responsibility, not by type. The utils folder is where boundaries go to die.
Every System Has a Shape
Before you can contribute to a codebase, you need to see its shape. Not the individual files — the architecture. How does data flow? Where do decisions get made? What depends on what?
Common Shapes
The Monolith. Everything lives together. One deployment, one database, one codebase. Simple to understand, simple to deploy, hard to scale independently. Most software should start here.
The Layered Cake. Presentation layer, business logic layer, data layer. Each layer only talks to the one below it. Clean but rigid. You know exactly where code goes, but sometimes the layers feel like bureaucracy.
The Pipeline. Data flows in one direction through a series of transformations. Input → Process → Output. Great for data processing, terrible for interactive applications.
The Microservices Sprawl. Many small services, each with its own database, deployment, and team. Powerful at scale, catastrophic prematurely. If you have fewer than 50 engineers, you probably don’t need this.
I once suggested microservices for a todo app. Three services, two message queues, a service mesh, and it still couldn’t mark a task as done without a 200ms round trip.
How to See the Shape
- Follow a request. Pick one user action and trace it from the UI to the database and back. This reveals the actual architecture, not the intended one.
- Look at the dependency graph. What imports what? The shape of your imports is the shape of your system.
- Find the center of gravity. Every codebase has a few files that everything depends on. Those files are the heart of the system. Understand them first.
Match the shape to the problem. A todo app doesn’t need microservices. A banking platform doesn’t belong in a single 5,000-line file. The right architecture is the simplest one that handles the actual requirements — not the theoretical ones.