Organize code by feature, not by type#

Every project needs a folder structure: backend, frontend, CLI, any software. There are two ways to split files:

  • by type: models in one folder, controllers in another, tests in a third;
  • by feature: everything about one feature in one folder.

My rule: always organize by feature. This post explains why, with an example tree and the special case of the “utilities” folder.

The bias: the Windows XP home folder#

Many of us learned to organize files here:

My Documents/
├── My Music/
├── My Pictures/
└── My Videos/

Folders by type. Now store your vacation: notes, photos and videos. Then show it to your family and friends. Where does it go?

My Documents/vacation-notes.txt
My Pictures/vacation/
My Videos/vacation/

Three places, and you jump from one folder to the other while presenting. Grouped by what it is about, it’s one folder:

Vacation 2026/
├── notes.txt
├── beach.jpg
├── dinner.jpg
└── boat-trip.mp4

The same in code#

You usually work on one feature at a time, across all the layers: data model / DB → backend → API → frontend.

Every feature is made of all the layers

  • Grouped by type (left), each folder holds one layer of every feature: to work on feature_2 you open four folders and edit a slice of each.
  • Grouped by feature (right), each folder holds all the layers of one feature: you open one folder.
  • The code is the same: only the cut changes. Cut along the way you work.

In practice#

Wrong: by type#

The classic Phoenix layout splits the core, the web layer and the tests:

lib/
├── my_app/
│   ├── orders.ex
│   ├── products.ex
│   └── product.ex
└── my_app_web/
    ├── controllers/
    │   ├── order_controller.ex
    │   └── product_controller.ex
    └── live/
        ├── order_live.ex
        └── product_live.ex
test/
├── my_app/
│   ├── orders_test.exs
│   └── products_test.exs
└── my_app_web/
    └── controllers/
        ├── order_controller_test.exs
        └── product_controller_test.exs

A change to products touches lib/my_app/, lib/my_app_web/controllers/, lib/my_app_web/live/ and test/.

Right: by feature#

lib/my_app/
├── products.ex                      # MyApp.Products: core logic
├── products/
│   ├── product.ex                   # MyApp.Products.Product: schema, the data model
│   ├── product_controller.ex        # MyApp.Products.ProductController: API
│   ├── product_live.ex              # MyApp.Products.ProductLive: UI
│   ├── products_test.exs            # MyApp.Products.ProductsTest
│   └── product_controller_test.exs  # MyApp.Products.ProductControllerTest
├── orders.ex
└── orders/
    ├── order.ex
    ├── order_controller.ex
    ├── order_live.ex
    └── orders_test.exs
  • Module names follow the path: products/product_controller.ex defines MyApp.Products.ProductController.
  • The core module sits beside its folder: products.ex is MyApp.Products, the entry point of the feature, and everything in products/ is MyApp.Products.*.
  • To run tests next to the code, add "lib" to test_paths in mix.exs (each test path needs its own test_helper.exs).
  • Files the tools expect in a fixed place (e.g. Ecto migrations in priv/repo/migrations/) stay there.

The same works anywhere, e.g. in a frontend:

src/features/cart/
├── Cart.tsx
├── useCart.ts
├── cartApi.ts
└── Cart.test.tsx

Why it works#

  • Features are atomic: drop products.ex and products/ and you drop a feature; create them and you work there, without jumping between tests, core logic and web layer.

  • Features can’t grow too much: when a folder gets big, it’s a sign to split it into subfeatures:

    orders.ex          # MyApp.Orders
    orders/
    ├── order.ex       # MyApp.Orders.Order
    ├── checkout.ex    # MyApp.Orders.Checkout
    ├── checkout/      # MyApp.Orders.Checkout.*
    ├── refunds.ex     # MyApp.Orders.Refunds
    └── refunds/       # MyApp.Orders.Refunds.*
    
  • A feature can be technical too: auth for authentication, rbac for permissions, csv for tools that work on CSV files, web/controller for what all controllers share (e.g. web/controller/fallback_controller.ex, MyApp.Web.Controller.FallbackController). Not only the business features.

No “utilities” folder#

utils/ or utilities.ex is where you put the universe. It has no topic, so everything fits, and it grows forever.

Wrong#

lib/my_app/
└── utilities.ex   # parse_csv, translate, export_xlsx, check_token, format_error, ...

Split it by feature, like the rest of the code:

lib/my_app/
├── csv/           # read and write CSV files
├── translation/   # locales and translations
├── excel/         # Excel import and export
├── auth/          # tokens and sessions
└── errors/        # the error schema, logging and rendering

Wrapping up#

  • Group by feature, not by type: you work on one feature across all layers, so keep its layers together.
  • One folder per feature, with its data model, API, UI and tests, and the core module beside it.
  • Module names follow the path: products/product.ex is MyApp.Products.Product.
  • Atomic features: add or remove a feature by adding or removing its folder and core module.
  • Split big features into subfeatures.
  • No utilities folder: technical helpers are features too (csv, auth, errors).