There is a particular kind of friction that experienced developers stop noticing because they've had it for so long. You're working on a feature - let's say users, something as ordinary as a list, a form, and a delete confirmation - and you have six files open at once. You scroll between them. You CMD+click to follow a call into a controller that lives three directories away from the template it renders. You hold the whole feature in your head because the code doesn't hold it for you; the code is distributed across the project according to what type of thing each file is, not according to which feature it belongs to.

MVC taught us to organize by technical role. Models here, controllers there, views in their own folder. It was a useful mental model for explaining how a web application works. As a folder structure to actually work in, it creates a cognitive load that compounds with every file you add and every developer who joins the project.


The Unit Was Wrong From the Start

When you sit down to work on a feature, the unit of your attention is the feature. Not "the model layer" or "the view layer" - the feature. You are thinking about users, or invoices, or orders. Everything you need to understand, change, and ship that feature should be reachable without leaving its context.

MVC organizes code by what it is. A feature-per-folder approach organizes code by what it does. The distinction sounds small; in practice it changes everything about how you work on a project day to day.

When the users feature lives in one folder - the route, the SQL, the template, the validation, the translations - opening that folder is opening the feature. You don't need a map. You don't need to know where the framework puts things. The file system is a map of the domain, and the domain is what you're thinking about when you're working.


Swiping Between Files Is Not a Workflow

The physical experience of working in a separated architecture is worth naming, because we normalize it so quickly that it disappears from our awareness. Three files open side by side. Swiping between them to follow a single request. Jumping to a definition that lives in a different directory tree. Holding the connection between them in working memory because nothing in the structure makes it explicit.

This is not a minor inconvenience. It is a continuous tax on attention, and attention is the actual resource being spent when software is written. Every jump between files is an interruption. Every time you have to reconstruct "what does this controller do with what the model returns and how does the view consume it," you're spending time that could go toward the actual problem.

Feature-per-folder doesn't eliminate complexity. It localizes it. The complexity of the users feature lives in the users folder. It doesn't leak into a shared controller file that also handles ten other things, or a routes file that has grown to six hundred lines, or a model layer that everyone touches and nobody fully owns.


Single-File Templates for the Client Side

The same logic applies inside the template, with one clarification worth making: client-side concerns and server-side concerns are different things, and they don't belong in the same file just because they belong to the same feature.

What does belong in the same file is everything the browser needs to render the component - the HTML structure, the CSS that styles it, the JavaScript that makes it interactive. A template that splits those three across separate files asks you to hold three open files in your head just to understand what one element does. The stylesheet is over there. The script is somewhere else. The markup references both by name, and you follow the references.

The .ree format keeps the client-side layer together: markup, styles, and behavior in one file, readable top to bottom, self-contained for everything the browser cares about. The server-side code - the route, the SQL, the validation - lives right next to it in the same folder, close enough to understand in a single directory listing, but separate because the concerns genuinely are separate.

This is the distinction worth holding: not "everything in one file" but "the right things in the same place." Client-side code is one thing. Server-side code is another. The feature folder is what holds them together without conflating them.


Keep the Helper Where It Lives

The same instinct that scatters code across layer folders also has a way of producing a utils.js file - or helpers.js, or lib/common.ts, whatever the project calls it - that grows quietly in the background, accumulating functions that were written for one place and moved to the shared file preemptively, just in case.

The right rule is simpler: write the function where you need it. If a second part of the codebase needs the same function, that's the moment to extract it into a shared location - not before, and not speculatively. Actual reuse is the evidence that a function belongs somewhere general. One usage is evidence it belongs exactly where it is.

Extracting early is premature abstraction, which is the structural cousin of premature optimization - and causes the same class of damage. You pay the cost of the indirection immediately, in added complexity and navigation overhead, while the benefit remains hypothetical. The shared library grows. The reuse never comes. The cost was real; the justification wasn't.

This matters for the same reason feature folders matter. A helper function sitting in the file that uses it is immediately understandable in context. You read the function, you see what it does, you see what calls it, and the picture is complete. A helper function extracted to a shared utilities file requires you to go there, understand it out of context, and mentally reconnect it to the place that uses it. That round trip is small, but it accumulates, and it means the utilities file eventually becomes a place where functions go to be forgotten - used by one caller, never reviewed, never deleted, outliving the feature that originally needed them.

The deletion argument is particularly clean here. When a feature disappears, its helper functions disappear with it if they lived in the feature file. When those helpers were moved to a shared location "for reuse that never came," they stay behind as dead code that nobody feels confident removing because nobody remembers what it was for.

Extract when there are two callers. Until then, keep it close.


Tailwind Is Code Proximity for Styling

I want to spend a moment on Tailwind, because it illustrates the same principle from an angle that provokes strong reactions from people who haven't spent enough time with it.

A class like card card-large is a semantic name. It describes intent. It tells you this element is a card, and it is large. What it does not tell you is what a card looks like, what large means, what colors are involved, what the spacing is, whether it's a flex container, what the border radius is. To know any of that, you leave the template and go find the stylesheet.

A Tailwind class list like flex items-center gap-4 p-6 bg-white rounded-lg shadow-md border border-gray-100 tells you all of it, right there. Someone who knows the utility classes - and they are learnable, they are consistent, they follow a logic you internalize quickly - can read that string and visualize the element with reasonable accuracy without opening another file. The margin, the padding, the flex layout, the background, the border, the shadow. It's all in the class attribute.

This is not verbosity for its own sake. It is the elimination of an indirection. The styling intent is co-located with the element being styled, which means the element is self-describing in a way that semantic class names never quite are.

An experienced eye reads a Tailwind class list the way a musician reads sheet music - the notation is the sound. You don't need to go find a recording to know what it will sound like.

The counter-argument is that Tailwind classes are hard to read until you know them. That's true, and it's also true of card card-large - it's just hard to read in a different direction. With an unknown utility class, you can look it up and learn it once. With an unknown semantic class, you have to find the definition every time, and the definition itself is arbitrary rather than systematic.

The reasonable follow-up is: what about reuse? Writing the same cluster of Tailwind classes for every text input across the application is its own kind of problem. The answer isn't to go back to semantic class names - it's components. Reepolee supports custom ReeTag components: you define <input-text> once in a .ree file, use it everywhere, and it compiles server-side with no runtime overhead. The Tailwind classes live inside the component definition, visible to anyone who opens the file.

And yes - this is a contradiction, or at least a tension. A ReeTag component does hide the implementation from the call site, which is exactly what we said was the problem with card card-large. The difference is that the hiding is one level deep and deliberately bounded: you choose to encapsulate <input-text> because you'll use it fifty times and changing the styling of every text input from one place is worth the trade-off. The Tailwind classes are still there, in one file, readable in thirty seconds. It is not a stylesheet spread across thousands of lines with specificity wars buried in it.

This is the fine line every developer has to find for themselves. There is no rule that draws it cleanly. Too little encapsulation and you're copying Tailwind class strings everywhere, fragile and inconsistent. Too much and you're back to card card-large, opaque and unmaintainable. The right answer sits somewhere in the middle and moves depending on the project, the team, and how long the thing needs to live. What I can say is that the instinct toward encapsulation should come from demonstrated reuse - the same logic as extracting a helper function - not from a desire to make the call site look tidy. And once you have those components, the question of what they should do - whether they should behave like native browser elements or like something custom - is a separate argument, covered in Boring UI Wins.


Deletion, PRs, and the New Developer

Three practical consequences of feature-per-folder that don't get enough attention:

Deleting a feature is deleting a folder. In a separated architecture, removing something requires hunting across the layer tree for every piece of it - the model, the controller action, the route, the view, the partial, the helper, the test file in yet another mirrored directory. You miss things. The dead code stays. In feature-per-folder, you delete the folder and the feature is gone, completely, by construction.

Code review for a feature touches one folder. A pull request that implements "add invoice export" should show changes concentrated in one place, readable in sequence, reviewable without a mental map of the project. When that PR instead shows eight files across four directory levels, the reviewer has to reconstruct the feature's logic from fragments rather than reading it as a whole.

A new developer can understand a feature by looking at one folder. They don't need to understand the whole layer architecture before they can follow a single request. They open the users folder and the users feature is right there - not split across three trees they haven't learned yet.


Not Every Feature Stands Alone

It would be dishonest to take this argument to its logical extreme and claim every piece of code can live in a feature folder with no shared dependencies. It can't. Authentication touches everything. A database connection is not owned by any single feature. A logging utility, a base layout, a shared validation rule - these things exist because the alternative is duplicating them in every folder, which trades one problem for a worse one.

The point isn't that shared code is wrong. It's that shared code should be earned by the same logic as extracted helper functions - by demonstrated need across multiple features, not by architectural habit or the assumption that something might be reused someday. And more importantly, the developer has to be the one who decides where those lines are. The folder structure of a project is a statement about how you intend it to grow.

A project where everything bleeds into shared layers is a project without a stated direction.

A project where every feature folder is genuinely self-contained for its own concerns, and shared code exists only where sharing was unavoidable, is a project someone made decisions about. That difference is visible three years later when the person maintaining it isn't the person who wrote it.


Why I Keep Coming Back to This

I have worked on projects that used separation of concerns in the traditional sense - full MVC, strict layer discipline, everything in its architectural place. Some of those projects were well-organized by the standards of the approach. They were still harder to work in than they needed to be, and the difficulty grew with the project rather than staying constant.

What I notice, every time I come back to a feature-per-folder, single-file approach - and I have come back to it enough times now that I've stopped treating it as a preference and started treating it as a conclusion - is how much less I have to hold in my head. The code is where I expect it to be because the organization reflects how I think about the domain, not how the framework thinks about its own internals. After three years on a project, opening a feature folder still feels like opening a drawer that has exactly what I put in it.

That feeling is not aesthetic. It's a reduction in the time spent reconstructing context rather than doing work.


How Reepolee Treats This

Reepolee is structured around these ideas from the ground up. Generated resources land in feature folders, not in a layer hierarchy. The .ree templates keep everything a view needs in one file. The SQL lives next to the route that uses it. Translations are generated per-resource rather than accumulated in a single global file that grows without obvious structure.

When you add a resource, a folder appears. When you remove one, the folder goes with it. The generated structure reflects the domain - what your application is about - rather than the framework - what your application is built with.

For small to mid-sized applications, where the team is small and the codebase needs to stay navigable over years rather than months, this matters more than it might appear. The overhead of a separated architecture is manageable when a team of twenty is maintaining it with dedicated ownership of each layer. It's a real drag when two or three developers are moving across the whole system and need to keep the whole feature in view at once.

A useful signal: open the project and navigate to a feature you haven't touched in six months. How many files do you need to open before you understand what it does? In a well-organized feature-per-folder project, we find the answer is one or two. If it's five or six, the structure is working against you.