DEV Community

Cover image for How To Structure Your App In a Way That Scales.
Michał Pietraszko
Michał Pietraszko

Posted on • Updated on • Originally published at michalpietraszko.com

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.

Discussion (11)

Collapse
jackmellis profile image
Jack

I like the use of the file extension (.component.js, .query.js) to denote the type of a file within a domain. However, personally I prefer to separate my concerns out a bit more.

If I'm looking for something that updates a specific cookie, or fetches some data from an api, for example; I'd probably spend longer digging around all of those feature folders for it than if I had my service layers separate from everything else. I always know my api requests or cookie updates will be in the service layer.

I also like to keep presentational and non-presentational stuff as far apart as possible. The view layer should only be concerned with 2 jobs: rendering data, and emitting events. It should almost operate in a bubble where it doesn't know anything about axios, or fetch, or cookies. The underlying tech is not a presentational concern. But if you put your services directly next to your components, you're tightly coupling the entire stack of your application.

Ack I'm rambling about architecture again! 🤦

Collapse
brokenthorn profile image
Paul-Sebastian Manole • Edited on

DDD honestly has been the best thing I've learned that helps with structuring business apps. When writing business apps, or just about any app that's more than trivial, you think in and about contexts. A domain exists within any app. There's a domain in everything. But if your app is more complex, you need to start organising it in a way that makes most sense. That's where DDD helps keep things localised, to keep them in focus when working in a certain context, because usually such apps have more than 2 or 3 contexts (or distinct features) that it needs to handle. DDD calls these bounded contexts.

Of course DDD is more than this so I recommend anyone to learn DDD if they want to get better at writing well organised, easy to maintain apps from the start. You don't have to read the blue book (there's too much stories at the beginning that can get you bored), but read a good book on applied, practical DDD, watch a few videos on Event Storming, to get a better idea about how to find bounded contexts, and soon you'll never want to structure your apps differently, whenever you start a new project.

Collapse
aschwin profile image
Aschwin Wesselius

True, DDD can help a lot in a sense of thinking into structured code. However, the pitfall (not only with DDD btw) is that decomposition (into structures) is most likely based on functional aspects.

Why is this a pitfall? Because it makes it prone to volatility of the business (rules). I suggest anyone to read the book Righting Software to understand more about this.

Doing decomposition right is a daunting task and few have mastered this.

Collapse
brokenthorn profile image
Paul-Sebastian Manole

Apps will always be prone to the volatility of the business rules. Doesn't mean it's a bad thing. It's just the way things are.

Thread Thread
aschwin profile image
Aschwin Wesselius • Edited on

Who says it's a bad thing? I also just state that is how it is. This also means this should be taken into consideration while putting a system together if you want to be able to maintain the system later on.

If you look around hardly any system is build this way. If you look even further, this is where the systems start to fail. This means it is crucial to understand this fact.

I'm happy Juval Löwy wrote the book "Righting Software" so we can learn from his experience and hands on knowledge of decades building systems the right way. Systems build on time, on budget and with zero defects.

Thread Thread
brokenthorn profile image
Paul-Sebastian Manole

Cool... So is he the only one that wrote about how DDD system break down or whatever?

Collapse
koladev profile image
Mangabo Kolawole

Interesting architecture. It's the first time I see an architecture without a page folder. Please, do you have a project with public source code based on this architecture?

Collapse
pietmichal profile image
Michał Pietraszko Author

I don't have any public examples at the moment. If I find or create, something then I'll share it with you!

Collapse
gpukys profile image
Gerimantas

This really feels like the way Angular apps structure their code. Which I like!

Collapse
codecustard profile image
Emmanuel Barroga

Really great architecture and completely different from what you usually see!

Collapse
fastcodesoluti1 profile image
fastcodesolutions

Awesome Article