Back home

Code colocation: Screaming Architecture

Call it Screaming Architecture, Scalable Architecture, Clean Architecture or...well you name it, but when it comes to organizing code in a project grouping files into directories like components, hooks, types, utils, and constants is the most common use case I face. While this might seem like a sensible approach at first glance, I find it to be one of the least effective methods for organizing code specially when the project starts to grow.

The Flaws of Horizontal Splits

Grouping by type rather than by domain creates a disconnect that can lead to confusion. For instance, you might find useTodos sitting next to useAuth, but far away to the todoUtils.

Why is that? This separation means that when you're working on a feature, you have to jump between different directories to find everything you need, and that may become overwhelming

What belongs together should live together. We should aim to minimize unnecessary jumps between different scopes.

It doesn't matter what those things are - what matters is what they do.

Just to make things clear, this is not about features, it's one step deeper than that. It's about domains.

You might be thinking, “What if two features need the same thing? Then I need to put it into a shared utils directory.” Not necessarily! You can create another domain for that "thing."

I'm not saying you can't extract reusable components outside of domains; rather, you should give them their own domain—even if that means creating something like src/date/ for date-related utilities.

Let's say you need a date picker on both the dashboard and in the todo list, create a src/date domain and move all date-specific utils, hooks, types, and components there.

Show me the thing

Alright, things clear! Let's dive deeper into adopting this vertical split approach.

Note that this approach really shines when you're dealing with larger, more complex applications but in any case you can take advantage of a better organization in your code base.

.
└── src/
    └── modules/
        ├── auth/
        │   ├── components/
        │   │   ├── LoginForm.tsx
        │   │   ├── SignupForm.tsx
        │   ├── hooks/
        │   │   ├── useAuth.ts
        │   │   └── useLoginStatus.ts
        ├── date/
        │   ├── components/
        │   │   ├── DatePicker.tsx
        │   ├── hooks/
        │   │   ├── useDateFormat.ts
        │   ├── utils/
        │   │   ├── dateUtils.ts
        │   │   └── timezoneUtils.ts
        │   ├── constants/
        │   │   └── dateFormats.ts
        ├── shared/
        │   ├── hooks/
        │   │   ├── useDebounce.ts
        │   ├── utils/
        │   │   ├── stringUtils.ts
        │   │   └── numberUtils.ts
        └── todos/
            ├── components/
            │   ├── TodoList.tsx
            │   └── TodoItem.tsx
            ├── hooks/
            │   └── useTodos.ts
            ├── utils/
                └── todoUtils.ts

We've got our main domains: auth, date, and shared, and todos. Each of these has subdirectories for components, hooks, types, utils, and constants. This allows us to have some organization within the domain while keeping everything related in one place.

The shared directory is for truly generic stuff that doesn't belong to any specific domain. But remember, we're trying to minimize this - if something can logically belong to a domain, it should go there.

While horizontal splits based on type may seem logical at first glance, they often lead to fragmented codebases that are harder to navigate and maintain as the app grows. By embracing an approach that focuses on domains rather than features or types we can build applications that are not only easier to work with but also reflect the real-world functionality we aim to deliver.

Anyway, here I'm telling you what work for me in a certain context, maybe your use case is different, so keep your mind open, be curious and don't take it as a rule of thumb!