Kamil Józwik

Organize your repository with "features"

A feature-driven structure enhances clarity, maintainability, and AI-assisted development.

dev

As developers, we pour our hearts into crafting elegant algorithms and robust architectures. Yet, one of the most fundamental aspects of a healthy codebase is often the one we neglect the most: the file structure. The way we organize our files can be the silent architect of our project's success or its slow, creeping demise. A logical structure fosters clarity and maintainability, while a chaotic one breeds confusion and technical debt.

This article will guide you through the common pitfalls of traditional repository organization and present a more modern, feature-driven approach. I've found this approach helping me to transform my projects from tangled messes into models of clarity, making them easier to navigate, scale, and even enhance with the power of AI.

The allure and agony of traditional structures

When starting a new project, most of us default to what we know best: organizing files by their type. This leads to the familiar top-level directories we've all seen and used:

/src
├── components
├── services
├── utils
├── hooks
├── styles
└── pages

On the surface, this seems perfectly logical. Everything has its place, neatly categorized by its technical function. However, as a project grows from a simple prototype into a complex application, this seemingly clean structure begins to reveal its deep-seated flaws.

The "everything drawer" problem

These type-based folders quickly become monolithic "everything drawers." The components directory, for instance, might contain a mix of tiny, reusable UI elements like buttons alongside massive, feature-specific components for user profiles, product galleries, and checkout processes. This makes finding anything a chore. As a developers we are forced to scroll through hundreds of files, trying to mentally piece together which ones relate to the specific feature they're working on. The context is lost, and productivity plummets.

The web of dependencies

A more insidious problem is the "spaghetti" of dependencies this structure creates. When code is organized by type, features are smeared across the entire codebase. The logic for a single user-facing feature—say, managing a shopping cart, might have its components in /components, its business logic in /services, its state management in /hooks, and its UI in /pages.

This scattering of related files leads to a complex web of inter-folder dependencies. The users feature might need to import from products, which in turn might depend on orders. This tight coupling makes the codebase rigid and fragile. A small change in one area can trigger a cascade of unexpected side effects across the application, turning simple refactoring tasks into week-long ordeals.

Organizing by feature

The solution is to flip the traditional model on its head. Instead of organizing by file type, let's organize by feature. In this model, all the code related to a single, cohesive piece of functionality is co-located within its own dedicated folder.

Let's revisit our e-commerce application. A traditional structure might look like this:

# The traditional (bit chaotic) way
/src
├── components
   ├── UserProfile.js
   ├── ProductCard.js
   ├── AddToCartButton.js
   ├── OrderSummary.js
   └── ...
├── services
   ├── authService.js
   ├── productService.js
   └── orderService.js
└── ...

Now, let's see how we can restructure it by feature:

# The Feature-Driven (Clear) Way
/src
├── features
   ├── auth
   ├── components
   └── LoginForm.js
   └── services
       └── authService.js
   ├── products
   ├── components
   ├── ProductCard.js
   └── ProductList.js
   └── services
       └── productService.js
   └── cart
       ├── components
   ├── AddToCartButton.js
   └── CartView.js
       └── hooks
           └── useCart.js
└── shared
    ├── components
   ├── Button.js
   └── Modal.js
    └── utils
        └── formatCurrency.js

The difference is night and day. Each feature is a self-contained module. Need to work on the shopping cart? Everything you need is right there in /src/features/cart. The cognitive overhead is dramatically reduced. Furthermore, we've introduced a shared directory for genuinely reusable code that isn't tied to any specific feature, maintaining a clean separation of concerns.

Linter

To force proper usage of the /features folder, you can set up your linter to enforce that all files must be placed inside a feature folder and some files can not be imported outside of their feature folder.

If you are using ESLint I recommend using eslint-plugin-boundaries for this purpose, as it is very flexible and allows you to define boundaries based on your project structure.

The AI advantage 🤖

This feature-driven, decoupled approach isn't just about human convenience; it's about preparing your codebase for the future of software development — one that is increasingly supported by AI.

Large Language Models (LLMs) and AI coding assistants are powerful tools, but they work best when they can operate on a focused, relevant context. When you ask an AI to refactor a feature or fix a bug, it needs to analyze the relevant code.

In a traditional, tangled structure, the AI has to sift through dozens of unrelated files across multiple directories to build a complete picture of the feature. This consumes a massive amount of its context window (the amount of information it can process at once), leaving less room for the actual task. The result? The AI's suggestions are often less precise, more generic, and sometimes completely wrong because it's working with a diluted, noisy context.

In a feature-driven structure, however, you can point the AI to a single folder. That folder contains everything it needs—the components, services, hooks, and types for that specific feature. With a clean, concentrated context, the LLM can work with surgical precision, providing highly relevant, accurate, and immediately useful assistance.

The ripple effects of a clean structure

The benefits of a well-organized, feature-driven repository extend far beyond just AI-assisted development:

  • Enhanced scalability: Adding new features is as simple as adding a new folder, without disturbing the existing codebase.
  • Improved team collaboration: Developers can work on different features in parallel with a much lower risk of merge conflicts. It becomes instantly clear who "owns" which part of the code.
  • Easier onboarding: New team members can get up to speed much faster because the project's structure mirrors the application's functionality. They can learn the codebase one feature at a time.
  • Painless refactoring: Since features are decoupled, you can refactor or even completely rewrite one feature with minimal impact on the rest of the application.
  • Better testing: Each feature can have its own dedicated tests, making it easier to ensure that changes don't break existing functionality.
  • Clearer documentation: Documentation can be organized alongside the code, making it easier to keep everything in sync. Each feature can have its own README file explaining its purpose, usage, and any specific quirks.
  • Future-proofing: As AI tools continue to evolve, a clean, feature-driven structure will be better positioned to take advantage of their capabilities, making your codebase more adaptable to future technologies.

Conclusion

The structure of your repository is not a trivial choice — it's a foundational decision that will affect every stage of your project's lifecycle. By moving away from outdated, type-based hierarchies and embracing a modern, feature-driven approach, you create a codebase that is not only a pleasure for humans to work with but is also primed for the next generation of AI-powered development tools. Take the time to invest in a clean structure; your future self will thank you.