DEV Community

Cover image for Why your folder structure sucks
Mike Pearson
Mike Pearson

Posted on • Edited on

Why your folder structure sucks

YouTube

Inexperienced developers organize their code by technology rather than by feature. This is a mistake. Separating by technology leads to

  1. folder straddling
  2. hard-to-find files
  3. reusability confusion
  4. slow load times
  5. slow build times
  6. huge folders

1. Folder straddling

When do you tend to edit or view all files that use the same technology? It’s usually when you are

  • learning the technology
  • teaching the technology
  • adding/upgrading the technology

This is why every time a new technology is invented, everyone seems to agree that all the files that use it should have a dedicated folder. Learners and teachers both want to quickly become oriented with the new technology and switch easily between examples that use it.

But they eventually grow out of this pattern. Let's look at some examples.

HTML + JavaScript + CSS

Image description

In the early days of AngularJS, everyone had separate templates/ and controllers/ folders. Both learners and teachers were focused on AngularJS, which treated HTML, CSS and JavaScript very differently.

This was okay for demos and small projects, but as real-world AngularJS applications grew, developers spent most of their time editing files in completely different folder paths. This was extremely annoying.

Pretty much all at once, everyone in the Angular community decided to refactor to a separation by components instead, where each component had its own folder with all its relevant files, regardless of the fact that multiple technologies were being used.

State

Image description

When state management became a primary concern with SPAs, developers felt the need to create a separate folder for managed state. This means in order to work on a single feature, you needed to spend most of your time editing files in completely different folder paths. This was extremely annoying. Unfortunately, people blamed Redux instead of the bad folder structure.

But a global store is an implementation detail, and nothing more. The fact that reducers/states/slices are ultimately collected into a single, global object is only a mechanism for reactivity and Redux Devtools. Any code that assumes it can just grab any state from anywhere in the global store is going to be bug-prone, so each store slice needs to export its own selectors/actions to interface with instead, which entirely abstract away the global store behind them. In the end, you just have a bunch of independent features, like always. Your folder structure should reflect that.

Types

Image description

When most Angular developers learned TypeScript in 2016, for a short period of time they wanted to put all their types in a central types/ folder. They have almost completely moved away from that, because if a developer needs to edit a type, they are guaranteed to need to edit other files in their project, which would require editing files in completely different folder paths, which is extremely annoying. React developers adopted TypeScript much later, and many new React projects are still set up with a central “types” folder. This will not last very long, just like it didn’t in the Angular community.

This will always happen

Image description

Whenever there is a new technology, there is always a strong influence from tutorials and content creators to separate by technology instead of by feature. If the cost isn’t too high, this naïve strategy will remain a habit for some time.

New tools can be intimidating, but the actual purpose of a project quickly overshadows the technology as the primary concern.

2. Hard-to-find files

When do you look for files? It’s usually when you are

  1. becoming familiar with a project
  2. debugging an issue

Becoming familiar with a project

If a project is your first encounter with a technology, you will want to see that technology reflected in the folder structure. But you are not going to understand much about what you are looking at, no matter how the folders are structured. The technology-based organization will give a slight sense of familiarity, but it won’t help you understand the actual project in front of you any more than picking an arbitrary file that uses that technology and trying to understand that.

Image description

If you want to see examples of a technology, you can just search for files with the extension for that technology, such as .slice.ts. File extensions indicate what technology is being used, and it's the last part of the file name because it's the least important detail.

If you are experienced, you will want the details of the project to be reflected first.

Screenshot of a UI design with

Debugging an issue

Screenshot of profile again, but with multiple technologies labeled in the UI

When debugging an issue, you start by seeing a broken feature. Problems are connected to features, which require multiple technologies to define, and, therefore, can be messed up by multiple technologies. Horizontally scanning all files by technology is useless for debugging anything but the technology itself. What you want is good developer tools that lead you through the technology layers, from the problem to the cause.

Let's say something is wrong in the UI. You start by inspecting the HTML. Then you use your framework's devtools to go directly to the component. Then you "click to definition" to a state management file, and continue until you find the problem. Now, if your folder structure is organized by technology, you will have jumped between 5 totally different folders. If it's organized by feature, you might not even have had to open more than 1 file, let alone folder. And maybe you even immediately saw the problem when you opened the component file, because every layer defining the feature was already there in front of your eyes.

3. Reusability confusion

Image description

If your code is organized by technology rather than by feature, you may be tempted to reuse one slice of that technology layer across multiple features. This seems convenient at first, but it introduces unnecessary complexity. Each feature is an independent concern, and is likely to drive changes across multiple technology layers that don't make sense to have in other features.

I was once a naïve Angular developer arguing for keeping HTML and JavaScript separate. I thought it would be good to have the flexibility to have multiple templates available for a single controller. The controller was responsible for managing state, and the template was responsible for displaying that state. Those are separate concerns, right?

But I have been writing component-only UI code for 8 years now, and never once have I taken advantage of Angular's separation of HTML and JavaScript; I have never used the same template in 2 different components, nor have I used the same component class in 2 different components.

HTML and JavaScript are different technologies, but your primary concern should be vertical slices of functionality that may span multiple technologies. These vertical slices are likely to change independently from other slices.

Image description

Reusability comes from having thin vertical slices that can be imported by other thin vertical slices. And these slices should be organized around a single type/interface, because that is the foundation that everything else is built on: When a type changes, everything that references it needs to change too. Ideally, those should be in the same folder. Even if your vertical slice extends all the way up to "global" state or even server-side code, it should be in the same folder as the primary type it uses. And if the feature is small, maybe it should even be in a single file.

4. Slow load times

Developers often prefer horizontal separation because they never have to worry about dependencies. They can just import whatever they want, because layers from above never import from layers below. At the top you have types, which are extremely abstract; then there are utilities; then you have state; then components.

Image of multiple layers of technology - component, slice, utils, types - all imported into each other in X's, but all in 1 direction from bottom to top

Because of this, circular dependencies are easily avoided when code is separated by technology. So this is the one problem separation by technologies seems to handle very well.

But this is more of a strategy of avoidance than a solution.

A circular dependency is when multiple things have actually become one thing.

Image of a circular dependency just being equivalent to a single thing

The reason we want our build tools to warn us of this is so we don't accidentally create giant things and make our users download more code than necessary. Separation by technology is just an escape hatch that allows us to be complacent about large bundles, as long as they only include code from the same horizontal layer (technology).

Image of a page depending on code from many features from above technologies

Users only need the code for the features on the current page, so we want our code bundles to only include what is needed for each feature. If it's okay to import anything from the technology layers above, there will be no friction to prevent developers from coupling each feature to every other feature. We want our tools to prevent us from creating circular dependencies between features, not technologies.

How do you deal with circular dependencies between features? You create a 3rd feature that the other features import from. Just because it doesn't plainly exist in the designs or in a route, doesn't mean you can't have a folder for it. You should. Every unique type can have its own folder, and you will never have circular dependencies. It doesn't have to have API, state or UI files, but you will have a place for them later.

Image of a page depending on code from Feature A, which depends on Feature C, but not B

5. Slow build times

Smaller bundles are great for users, but they are also great for developers, because they mean faster build times. With Nx, for example, running nx affected:build will build only the code related to the feature you've been working on, if libraries are organized by feature. But if you have a types library, for example, and you make a change to just one type inside it, then every library that depends on any type needs to rebuild. This will cascade to a huge amount of code totally unrelated to your changes in a specific feature. In large projects this can extend build times by multiple orders of magnitude.

I may or may not have written that last paragraph while waiting for a build.

6. Huge folders

Imagine you have 5 features, each with a component and a Redux slice.

This is a folder structure organized by technology:



store
    a.slice.ts
    b.slice.ts
    c.slice.ts
    d.slice.ts
    e.slice.ts
components
    A.tsx
    B.tsx
    C.tsx
    D.tsx
    E.tsx


Enter fullscreen mode Exit fullscreen mode

This is a folder structure organized by feature:



a
    a.slice.ts
    A.tsx
b
    b.slice.ts
    B.tsx
c
    c.slice.ts
    C.tsx
d
    d.slice.ts
    D.tsx
e
    e.slice.ts
    E.tsx


Enter fullscreen mode Exit fullscreen mode

When we organize by technology, we have more large folders than when we organize by feature.

However, both have the problem that there is at least 1 folder with 5 things in it. If we keep adding features, the folder will keep getting bigger.

One solution is to group the features into scopes or categories. So let's say A, B and D are related somehow, and we can change the folder structure to this:



some-commonality
    a
        a.slice.ts
        A.tsx
    b
        b.slice.ts
        B.tsx
    d
        d.slice.ts
        D.tsx
c
    c.slice.ts
    C.tsx
e
    e.slice.ts
    E.tsx


Enter fullscreen mode Exit fullscreen mode

If we were organizing by technology, we would have to create this nested structure in 2 different places:



store
    some-commonality
        a.slice.ts
        b.slice.ts
        d.slice.ts
    c.slice.ts
    e.slice.ts
components
    some-commonality
         A.tsx
         B.tsx
         D.tsx
    C.tsx
    E.tsx


Enter fullscreen mode Exit fullscreen mode

That's annoying.

Another way is to nest the store and component folders like this:



some-commonality
    store
        a.slice.ts
        b.slice.ts
        d.slice.ts
   components
        A.tsx
        B.tsx
        D.tsx
store
    c.slice.ts
    e.slice.ts
components
    C.tsx
    E.tsx


Enter fullscreen mode Exit fullscreen mode

This is a little better, but now we're going to be repeating store and components folders a lot, which gets annoying to deal with.

Ultimately, organizing folders by feature is the least annoying strategy.

Organizing by routes

Features usually correspond with routes, but as projects grow, this 1-to-1 relationship has exceptions. Often a single feature needs to appear inside multiple routes.

It is fine to start by organizing files by routes first, since there are some benefits to doing it this way. However, it is important to realize that nesting a folder slightly couples it to its parent feature, and as soon as it needs to be reused, it needs to be moved to a top-level folder where it can be imported into both routes.

Directory trees are inherently limited

Organizing folders by feature is better than organizing by technology, but ultimately there is no great way to organize code in a folder structure.

If you think about the actual code you are writing, what is its relationship with other code? It imports it, right? It gets imported, too. And that creates a dependency graph. And if we try to fit a graph into a directory tree, we are going to have a bad time.

It Doesn't Work

Imagine if we didn't have to deal with this fundamental problem!

Every once in a while developers will try to use symlinks to make the folder structure behave like a graph instead of a tree, but these are irritating to deal with, so this pattern never catches on.

What we need is a solid abstraction on top of files that allows us to manage our assets as a graph instead of a tree. I think Nx Devtools' dependency graph visualization is awesome, and could maybe do the job if it could be used to open the files it's visualizing, maybe as a VSCode extension. Then you could completely ignore the underlying folder structure.

Nx Dep Graph Example

We also need the ability to create relationships that are purely organizational. Sometimes it's helpful to be able to hide code inside a parent folder with a more abstract name. This relationship doesn't come from importing the code, but from an implicit relationship between the code and code that's similar to it. But this relationship should be a graph too, because there are multiple ways to categorize all code.

For example, you could have a file called concat-array-strings.function.ts. Would you put that in a string folder, an array folder, or a utils folder? A utils folder is like organizing by technology (or lack thereof), so it is not ideal in my opinion. But the point is that there are potentially multiple buckets you could put a concat-array-strings.function.ts file in, so why do you have to choose? Each of these potential parent folders is just abstracting out a different property of the code, when in reality it is all of those things.

This might be getting a little too philosophical now. I have many ideas about how code could be organized. But for now, I would just be happy if people would start organizing their folders by types more instead of by technology. Maybe some content creators will see this and realize they have been misleading new developers this whole time.

Conclusion

There is no folder structure that is perfect, but some are definitely better than others. You should realize that what you are concerned with at the beginning of a project may not be your primary concern later on, and you may later regret the decisions you make.

Ever since I started organizing my project folders by feature instead of technology, getting stuff done has been less annoying. If you haven't tried it yet, please do. And again, comment below if you think you have an example that I have not thought of.

And if you agree, please share this with your team!

Top comments (14)

Collapse
 
denartha10 profile image
denartha10

Hi Mike, I’m relatively new to web development but thought myself to program in python after university and managed to get a job. I’ve always leaned towards fp because I hate oop and so when I finally decided to switch to web dev (because I like Colors lol) I found myself gravitated towards videos like yourself and josh moroney because I love declarative code.

That was a ramble. The question I wanted ti ask is how do you decide what’s a type/feature. Like in web applications what counts as a feature. In drag and drop functionally for example is the dropzone and the draggable item a separate feature? These are just questions I have that I figured I’d ask because they confuse me 😅

Collapse
 
mfp22 profile image
Mike Pearson

Cool! Well hopefully we get more people to like declarative code.

Every type could be a separate feature, but a lot of types will never be used away from other types, so you would just be creating work for yourself by separating them out. There's no perfect answer, and you'll never get absolutely everything right, so it's not the end of the world if you realize later that you need to separate something out.

Collapse
 
denartha10 profile image
denartha10

Okay hmm so a project card would be an example of a type. A login form with validation would be a type..

Okay I sort of get it but I’m hoping it’s a familiarity thing and I’ll pick it up as I go along. Thank you!

Ps: wouldn’t a va odd extension that could color code statements to see what state it effects be cool (I got the idea from one of your imperative vs declarative examples where you showed how declarative code was unique separate blocks whereas imperative was like a bag of skittles!)

Thread Thread
 
mfp22 profile image
Mike Pearson

Yeah right now I'm doing it manually with a syntax highlighter plugin. If someone created one I would pay money for it. Changing colors has been a pain, and the whole thing is tedious.

Thread Thread
 
denartha10 profile image
denartha10

Yeh would be a cool learning tool is well as it would point out to people learning declarative code when they’ve stepped in to imperative!!

Collapse
 
elisechant profile image
Elise • Edited

Thanks for sharing, this is great. +1 have also had success grouping by feature/[domain].

This is pretty relevant to other languages as well. It might interest others that .NET MVC structure also support use of a Features directory, as well as Areas. Worth checking those links out, also this blog post about it medium.com/c2-group/simplifying-pr....

Collapse
 
mfp22 profile image
Mike Pearson

Wow, it's didn't know that. Do people use that?

Collapse
 
mfp22 profile image
Mike Pearson

Here's an example where I refactored from technology to feature organization: youtube.com/watch?v=scbedYdly6U

Collapse
 
pvivo profile image
Peter Vivo

Thx, Mike, this is so essential!

Collapse
 
rainerhahnekamp profile image
Rainer Hahnekamp

Hi, have been redirected to that article by you via Twitter :)

The architecture we are proposing has two layers. The first one is non-technical: the domain. We propose multiple folders per feature within a domain, but they very often depend on the same set of services. That's why we have a technical separation between features and services (data).

Since features also have dump components which should not allowed to access service, we move them to a separate UI (technical) folder.

We use folder because our dependency rules can only operate on folders.


You were proposing to base dependency rules on file (file extension) level.

So we would have

  • Smart Component: customers.feature.component.ts
  • Dump Component: customers.ui.component.ts
  • Service: customers.service.ts

Dump can't access services but Smart can.

Now, this would be a perfect fit if all three files would be within the same folder. Unfortunately, since services are shared, we would have a mix between folder- and file-based rules. I am afraid that this would make things even harder.

What is your take on that?


Image description

Image description

Collapse
 
mfp22 profile image
Mike Pearson

Thanks for putting thought into this.

Honestly, asking for rules based on file extensions is just trying to fight a reason for rules to exist at all, because the only useful one I've ever seen is to prevent published libraries from importing from private ones. I don't like the model, data-access, feature, ui convention for the reasons I mentioned in this article.

they very often depend on the same set of services

This is for each to decide, and the placement of those services should have nothing to do with how they are used by other features. Each service has a job to do, usually managing a certain type of data, so it should be next to other stuff that's dedicated to that data type.

Unfortunately, since services are shared, we would have a mix between folder- and file-based rules.

Why does something being shared mean that there needs to be any folder-based rules? All code should be potentially shared. Components are components precisely so they can be reused. But it's not the concern of each piece of code to manage stuff that is importing it. It is what it is, it stands alone, exposes certain APIs (inputs & outputs) and it doesn't matter what else is importing it.

Collapse
 
rainerhahnekamp profile image
Rainer Hahnekamp

I see the beauty and elegance of your approach (I also watched the video), but I can't see it working in a larger code base. Especially if my feature is a little bit bigger, I don't want to have 20 files in my folder. That's where sub-folders, after the technological aspect, become useful.

The thing that I take away is that we shouldn't always automatically subdivide a feature into technology but wait until it is needed.

All code should be potentially shared.

I want to limit the impact and define the scope of a change. I need those dependency rules to do that. Are there alternatives?

Components are components precisely so they can be reused

Some, but quite a lot, can be used only once because they are feature-specific. Especially if I use an existing UI library, the majority of my components are not going to be re-usable.

Components also allow me to split a huge feature into smaller pieces.


I know your work and what you are capable of. So, you definitely know what you are talking/writing about.

But could it be that we two are exposed to completely different types of applications? Not needing rules and creating components primarily for re-usability is something I find really hard to agree on.

Thread Thread
 
mfp22 profile image
Mike Pearson

I can't see it working in a larger code base

I can't see anything else working well in a larger codebase.

I've recently been in a project with 100+ components with these superfluous technology-based divisions and even the simplest things were driving me crazy. I had to show a data type in a component. Let's call it Person. I want this component to be reusable, because 6 other pages need to show the Person component inside of them. But each of them needs to create a FormGroup to represent the data that the component manages. How it should work is I create a library like libs/person and export both createPersonFormGroup and PersonComponent from there, maybe even the same file, because they are meant to do the exact same job. They inherently go together. But instead, I needed to create a utils library for the form group stuff, because there's a rule where not many things can import from ui libraries, and the overall FormGroup object needs to be created in a component store or service that's also fetching data so getting classified as data-access. Now I need a ui library for the component. So these 2 things are just completely arbitrarily split up.

Features can always be split up further vertically. Scopes can be divided. Yeah, subfolders are fine. But I wouldn't create a tests/ subfolder, or a state/ subfolder.

I want to limit the impact and define the scope of a change. I need those dependency rules to do that. Are there alternatives?

Organizing by feature minimizes the scope of a change (affected libraries) unless you split every single file into its own library. I mentioned this in this article. I also made a quick 4-minute video demonstrating this: youtu.be/RIbmwm3yIlI?si=JKg1cur7X8...

Some, but quite a lot, can be used only once because they are feature-specific

Well, why can't features be reused? Or do a split screen thing and put 2 on the same page? The most common that has hurt me in the past is when there is both a page where something can be done, and then a dialog for multitasking with that thing while in the middle of another version of it. Not the best design in my opinion, but that's the design we had to implement. I've seen this sort of thing 3 times I can think of immediately. But with components, it is fine—you just need to provide the inputs and the providers, and it should work. Some fragile global state management patterns struggle with this, but I think you could even provide a separate NgRx Store as a short-term solution.

But could it be that we two are exposed to completely different types of applications? Not needing rules and creating components primarily for re-usability is something I find really hard to agree on.

If there was a concrete example to analyze, that could help. But I've seen a lot of different types of applications by now.

Thread Thread
 
rainerhahnekamp profile image
Rainer Hahnekamp

Okok, thanks for the discussion. I think I understand you now much better and I don't think we are that much apart.

The main difference is that I probably tend to jump very quickly into "boilerplaty" stuff and divide my code into different folders even if I wouldn't have the need for it.