The Vertical Codebase
Skip to content BlueskyGitHub
The Vertical Codebase<br>Apr 13, 2026 — Frontend, Architecture, API Design
Photo by Darryl Low<br>한국어<br>Add translation
I’ve always had opinions about how to “properly” structure a codebase. I think most engineers do. My stance has obviously evolved over time as things changed, but I found a tweet of myself from two years ago that I still 100% agree with:
Dominik 🔮<br>@tkdodo
components / hooks / types / utils (and constants) is the split<br>I’m seeing in many codebases, yet it’s the one I dislike the most.<br>It groups by type, not by domain. “useTheme” will live next to<br>“useTodo”, but not next to ThemeProvider … why?
*]:rounded-xl">
- Jan 23, 2024
Why indeed? 🤔 The horizontal split has never made sense to me. I guess it’s convenient when you’re starting out, but that’s about it. Most of the things you write will be components anyway, and the other folders might just have a couple of files.
Of course, over time, this becomes a nightmare, and once you have it in place, it’s very hard to get rid of. Once again, the Sentry codebase is a good example. 10+ years of product development with this structure has lead to over 200 files in the top level components directory (opens in a new window). What they have in common? That they are components. Nothing else.
From analyticsArea to workflowEngine, you’ll find everything in there. Well actually, you’ll likely find nothing, because you don’t know where to start looking.
If your startup is NGMI Not Gonna Make It , the structure likely doesn’t matter. Maybe that’s why most people don’t think it’s a problem - because they can’t envision how this might look years from now. But once you get to a position where you’ve “made it”, taking steps to make sure your codebase can survive the scale and growth seems like a good idea. And getting rid of the horizontal structure is a good start.
But AI doesn’t care !?
Some engineers never look at any code ever again, because agents do all the things - writing, reviewing, fixing - in a fully automated way. If you’re in the camp that firmly believes that even if you’re drowning in tech debt, the next model will just throw everything away and re-write it in a weekend, this blogpost is likely not for you. But then again, if you don’t read code, you likely don’t read blogposts either, so this seems like an empty Venn diagram anyway. If you’re here, you very likely care.
Matt Pocock (opens in a new window) gave a great talk last week at the AI Engineer Europe event about Why Software Fundamentals Matter More Than Ever (opens in a new window), and I think he’s right.
In my opinion, agents need mostly the same things humans need to work efficiently: boundaries, constraints, and fast feedback loops. That includes a project structure that is easy to navigate, a good setup of lint rules and TypeScript, as well as a fast and reliable test suite. That’s why agents are so good at new codebases, but not very effective on codebases that have grown organically over years.
So yes, we should very much care, even if we don’t have to navigate the codebase ourselves.
Code colocation
Cognitive Load is what matters (opens in a new window), and having to jump around a codebase a lot unnecessarily adds to that. Code that changes together should live together. Most people I know would not move the props of a component to a separate file. It’s usually:
src/components/widget.tsx1
export type WidgetProps = { id: string }
export function Widget(props: WidgetProps) {}
That makes sense. We’ll want to see what props are when we read or change the component without having to navigate away. We can also export the WidgetProps if we need them somewhere else.
Still, in a horizontal structure, this file would live in src/components/widget.tsx. The props are an integral part of the component, it’s public interface, so we likely won’t think twice about this colocation. This is good.
But what happens if we e.g. add some data fetching?
Component with data fetching1
export type WidgetProps = { id: string }
export function Widget(props: WidgetProps) {
const { data } = useSuspenseQuery(widgetQueryOptions(props.id))
Where does widgetQueryOptions live? It’s a util, so it’ll likely be in something like src/utils/widget.ts ? 🤨
Scaling Up
Again, all of this is probably fine when you don’t have a lot of code, but it doesn’t scale. I know this is a fuzzy term, but having a utils directory that colocates things just because they are things that are neither components nor hooks is pretty arbitrary.
The Sentry codebase has functions in utils/analytics that are logically coupled to components/analyticsArea. The functions exposed from utils/feedback are only used from within components/feedback. Code related to profiling is split into components/profiling, types/profiling, utils/profiling and views/profiling. Make it make sense.
Coupling and Cohesion
What we’d want is low coupling and high cohesion (opens in a new...