How To Structure Your App In a Way That Scales.

pietmichal

Michał Pietraszko

Posted on June 27, 2021

How To Structure Your App In a Way That Scales.

The best codebases I found myself working on had their folders structured around the features the app provides.

Some folks might tell that it is very close to the Domain-Driven Design's principle of bounded contexts.

The App We Will Structure

Thinking of application as a feature that contains features creates a robust mental model that can be easily mapped to the folder structure of your project.

The following example will refer to a Single-Page Application written in JavaScript that consists of the following building blocks:

  • Routes - root components displayed when an URL is used,
  • Components - logical units handling state and rendering,
  • Queries - functions calling an HTTP API,
  • Styles - CSS bound to the specific component they are named after,
  • Services - logical units handling overarching problems

Remember, this pattern can be applied to any programming language, framework, and problem domain.

For example, a game could use shaders, prefabs, entities, and systems as its own building blocks instead.

My goal here is to present the idea in an easy-to-digest way. For this reason, I'll make a few simplifications when we go through the evolutionary steps.

The Genesis

Our app will start with login and register features.

It should be able to take input data and communicate with the API.

When a user is logged in, then they will be able to see some kind of message that they have an active session.

The simplest way to start is with one file. From this point, we will take a few steps.

src/
├─ index.js
├─ style.css
Enter fullscreen mode Exit fullscreen mode

The features are apparent when someone opens the index.js file.

The Buckets

Now, imagine the business wants the app to do more. They say, that after the user logs in, they should see the dashboard with charts representing important data.

You start writing code and at one point the sense of guilt ensues... the index.js file became too large and you think that as a good engineer you should organize your code better.

Usually, people organize the code in what I like to refer to as buckets and end up with something like this.

src/
├─ services/
│  ├─ session.service.js
├─ components/
│  ├─ button.component.js
│  ├─ input.component.js
│  ├─ piechart.component.js
│  ├─ linechart.component.js
├─ routes/
│  ├─ login.route.js
│  ├─ register.route.js
│  ├─ dashboard.route.js
├─ styles/
│  ├─ input.component.css
│  ├─ button.component.css
│  ├─ piechart.component.css
│  ├─ linechart.component.css
│  ├─ dashboard.route.css
│  ├─ login.route.css
│  ├─ register.route.css
├─ queries/
│  ├─ login.query.js
│  ├─ register.query.js
│  ├─ dashboard.query.js
├─ index.js
├─ style.css
Enter fullscreen mode Exit fullscreen mode

Is there an objective problem, at this point, with this? No. Things might feel alright because every concept has its own bucket. There is not much functionality, but as it grows - your feelings might change.

More Features

Now, the business says that we should add some reports that will allow users to see critical information - for example, how much money they've gained and how much money they've lost. These are expected to include tabular data and charts.

Let's add more to the buckets.

src/
├─ services/
│  ├─ session.service.js
├─ components/
│  ├─ button.component.js
│  ├─ input.component.js
│  ├─ data-table.component.js
│  ├─ piechart.component.js
│  ├─ linechart.component.js
│  ├─ barchart.component.js
├─ routes/
│  ├─ login.route.js
│  ├─ register.route.js
│  ├─ dashboard.route.js
│  ├─ loses-report.route.js
│  ├─ gains-report.route.js
├─ styles/
│  ├─ input.component.css
│  ├─ button.component.css
│  ├─ data-table.component.css
│  ├─ piechart.component.css
│  ├─ linechart.component.css
│  ├─ barchart.component.css
│  ├─ dashboard.route.css
│  ├─ login.route.css
│  ├─ register.route.css
│  ├─ loses-report.route.css
│  ├─ gains-report.route.css
├─ queries/
│  ├─ login.query.js
│  ├─ register.query.js
│  ├─ dashboard.query.js
│  ├─ gains-report.query.js
│  ├─ loses-report.query.js
├─ index.js
├─ style.css
Enter fullscreen mode Exit fullscreen mode

That's a lot of files scattered around.

Ask yourself the following questions.

Is it immediately obvious to you what features the app consists of?

Is it clear what features are dependent on each other?

Feature-driven Folder Structure

Let's take a step back and write down what features and areas of concern the app covers.

  • Login
    • Receives data input
    • Cares about current session
  • Registration
    • Receives data input
    • Cares about current session
  • Dashboard
    • Visualization via charts
    • Cares about current session
  • Loses Reporting
    • Visualization via data table
    • Visualization via charts
    • Cares about current session
  • Gains Reporting
    • Visualization via data table
    • Visualization via charts
    • Cares about current session

Think about the whole app as a feature.

Also, think about each bullet point as a separate feature.

Each feature is specialized in one problem domain.

Some features are shared between features.

Let's map this to the folder structure.

Please keep in mind that structure might differ depending on a person and the team working on the codebase!

src/
├─ shared/
│  ├─ session/
│  │  ├─ session.service.js
│  ├─ data-table/
│  │  ├─ data-table.component.js
│  │  ├─ data-table.component.css
│  ├─ data-input/
│  │  ├─ button.component.js
│  │  ├─ button.component.css/
│  │  ├─ input.component.js/
│  │  ├─ input.component.css
│  ├─ charts/
│  │  ├─ piechart.component.js
│  │  ├─ piechart.component.css
│  │  ├─ linechart.component.js
│  │  ├─ linechart.component.css
│  │  ├─ barchart.component.js
│  │  ├─ barchart.component.css
├─ login/
│  ├─ login.route.js
│  ├─ login.route.css
│  ├─ login.query.js
├─ register/
│  ├─ register.route.js
│  ├─ register.route.css
│  ├─ register.service.js
│  ├─ register.query.js
├─ dashboard/
│  ├─ dashboard.route.js
│  ├─ dashboard.route.css
│  ├─ dashboard.query.js
├─ gains-report/
│  ├─ gains-report.route.js
│  ├─ gains-report.route.css
│  ├─ gains-report.query.js
├─ loses-report/
│  ├─ loses-report.route.js
│  ├─ loses-report.route.css
│  ├─ loses-report.query.js
├─ style.css
├─ index.js
Enter fullscreen mode Exit fullscreen mode

Ask yourself the following questions, again.

Is it immediately obvious to you what features the app consists of?

Is it clear what features are dependent on each other?

From my experience, a developer can immediately tell what features the app has and where they have to go if they have the task of modifying the code.

Feature of Features... of Features?

The problem I've experienced when applying this pattern was the shared program expanding to unmanageable size creating a similar problem to "the buckets" approach.

There is one trick to deal with this.

Take a look at the structure above and try to tell what shared features are not related to everything?

...

The charts and *data table features.

The important thing to remember is that the feature-driven pattern has no limit to how deep the structure can go.

It should go as deep or as flat to ensure comfort which is subjective.

Check the following example of how the structure can be made to represent the relationship between features even better.

src/
├─ shared/
│  ├─ session/
│  │  ├─ session.service.js
│  ├─ data-input/
│  │  ├─ button.component.js
│  │  ├─ button.component.css/
│  │  ├─ input.component.js/
│  │  ├─ input.component.css
├─ login/
│  ├─ login.route.js
│  ├─ login.route.css
│  ├─ login.query.js
├─ register/
│  ├─ register.route.js
│  ├─ register.route.css
│  ├─ register.service.js
│  ├─ register.query.js
├─ reporting/ 
│  ├─ data-table/
│  │  ├─ data-table.component.js
│  │  ├─ data-table.component.css
│  ├─ charts/
│  │  ├─ piechart.component.js
│  │  ├─ piechart.component.css
│  │  ├─ linechart.component.js
│  │  ├─ linechart.component.css
│  │  ├─ barchart.component.js
│  │  ├─ barchart.component.css
│  ├─ dashboard/
│  │  ├─ dashboard.route.js
│  │  ├─ dashboard.route.css
│  │  ├─ dashboard.query.js
│  ├─ gains-report/
│  │  ├─ gains-report.route.js
│  │  ├─ gains-report.route.css
│  │  ├─ gains-report.query.js
│  ├─ loses-report/
│  │  ├─ loses-report.route.js
│  │  ├─ loses-report.route.css
│  │  ├─ loses-report.query.js
├─ style.css
├─ index.js
Enter fullscreen mode Exit fullscreen mode

Now, when you traverse the codebase, you can clearly see what you are looking at and what are the dependencies that you take into consideration.

This way you can add as many features as you need and the structural complexity should be proportional to the actual problem the app tries to solve.

Final Words

Keep in mind that there is a lot of space when it comes to organizing code in a feature-driven way and people can come up with different structures.

There is no objectively correct structure.

You can also mix "the bucket" and feature-driven approaches.

This is because sometimes it might be easier for the eyes to just put shared single components into components folder to avoid many single file folders.

The important thing is to define your own rules of thumb and stick to them.

You can always reflect back and refactor the structure as the codebase evolves.

💖 💪 🙅 🚩
pietmichal
Michał Pietraszko

Posted on June 27, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related