Welcome! We continue our series of articles about frontend architecture.
Mindset and Architectural Thinking
In this article, we will talk about one of the most important things, in my opinion — the mindset behind architecture. What do I mean by that? It’s the understanding of what our job as developers really is and how we plan to do it.
From my experience, this is one of the most underrated and missed aspects of software development. Most projects I’ve seen were developed in a “whatever works” way. You take some boilerplate template, start adding folders and files — that’s your project initialization — and then every problem is solved in whatever way the current developer thinks of (or whatever the AI suggests).
This approach leads only to chaos after a few years, where only people who have worked in the company long enough can understand what, when, and how something was developed.
The Alternative — Designing Systems in Advance
The alternative is to design systems in advance. For me, this is what application architecture means:
The idea of how we build a project and how we approach solving problems through code
P.S. I don’t believe there is a universal template for good architecture. There are many books with elegant and well-explained approaches, but the real world is chaotic, and it’s often hard to follow guides or patterns strictly. Over time, I came to the idea: Good architecture is the one that makes it easy to evolve the project. And that depends on the specific team, people, and company culture. Sometimes even not-so-perfect patterns (and sometimes anti-patterns) work better than “proper” ones — just because people understand them and are comfortable using them. Meanwhile, “best practices” can bring more confusion and problems if the team cannot fully understand them. And that’s totally fine.
My Approach to Architecture
So, here are my thoughts and my approach to architecture — in general, and in frontend specifically.
First of all, I really like DDD (Domain-Driven Design) model. For me, these are the most flexible and logical patterns, because I clearly understand where in the system I need to make changes to keep my code structured and reliable.
P.S. I’ll try to describe everything in simple and practical language, as I see it in my everyday work. You can always read the official books if you want more theory and details.
I’ll start with an example and share a few thoughts, and then explain what benefits this approach brings.
Domain-Driven Design
DDD are slightly different implementations of the same idea — splitting the application into layers, each with a clear responsibility.
This separation helps us:
- Keep control and confidence in our code through the whole project lifecycle,
- Reduce development and maintenance costs as the project grows,
- And minimize the number of bugs caused by complexity.
In practice, this means a structured organization of layers and well-defined interaction between them.
Project Structure
Besides DDD, I usually organize my projects according to the Screaming Architecture principle (from Robert Martin’s "Clean Architecture" book).
For example, I created three modules — cart, catalog, and user — to show the structure:
src
├─ modules
├─ catalog
├─ users
└─ cart
Also, from my experience, I usually need two shared modules (I mark them with _ prefix for visual separation):
-
_shared— code for shared functionality between modules (for example, basic React components or hooks) -
_core— common interfaces and base classes used by the whole application (I’ll describe this in the next articles)
src
├─ modules
├─ _core
├─ _shared
├─ catalog
├─ users
└─ cart
Each module contains the classic three abstraction layers:
src
├─ modules
├─ _core
├─ domain
├─ infrastructure
└─ presentation
├─ _shared
├─ domain
├─ infrastructure
└─ presentation
├─ catalog
├─ domain
├─ infrastructure
└─ presentation
├─ users
├─ domain
├─ infrastructure
└─ presentation
└─ cart
├─ domain
├─ infrastructure
└─ presentation
Interaction Between Layers Inside a Module
The core of our system is the Domain layer in each module. It contains everything related to business logic. This code should be as independent as possible from frameworks and libraries, because the most stable applications are those where the business logic doesn’t depend on external tools. And the most reliable way to achieve that — is to avoid dependencies at all :)
Infrastructure — data access layer (it can be a database, backend API, localStorage, cookies, etc.)
Presentation — user interaction layer (UI components, React hooks, etc.; for backend — controllers)
Dependency Direction
The interaction between layers looks like this:
Infrastructure <- Domain -> Presentation
It’s controlled through contracts — TypeScript interfaces and types.
This is the most important part of this article, because this is the mindset you should follow when working on any task. For every new feature, you should split it into different layers and think about which part belongs where and why. For every bug, you should ask on which layer it appears and where it’s best to fix it.
Trade-offs and Benefits
This approach usually slows down development at the start of a project,
but it gives you many advantages later:
- Business logic changes are centralized and isolated from UI components.
- API interaction is isolated in the Infrastructure layer, so I can change it without fear of breaking business logic.
- The same with UI — changes in Presentation can’t break business logic.
- I don’t waste time deciding “where should I put this code”, so development becomes more structured and focused on real business needs.
And that’s not the full list. There’s one more interesting thing — the business side.
Sometimes we have to write not-so-perfect code under deadlines or pressure. But I noticed something unexpected (by my experience):
Business people are usually okay with spending more time and money early if they understand that it will save a lot more in the future.
Interaction Between Modules
Ideally, every module (except shared ones like _shared and _core) should be independent from others. All interfaces should be generic — this gives maximum flexibility and reliability. But in the real world, especially in frontend, it’s hard to reach full isolation. Usually, pages combine several modules at once. So I don’t recommend setting strict limitations here.
If you have better ideas — I’d love to read them in the comments 🙏
There’s only one rule I always follow (and I think it’s clear): never import business logic modules into shared ones (_shared or _core).
Conclusion
Architecture is not just about folder names or fancy diagrams — it’s about how we think when we write code. It’s a way to organize not only our projects, but also our mindset as developers.
The main goal of layering and clear structure is not to make things “perfect,” but to make them manageable, predictable, and easy to grow. When every team member understands where each piece of logic belongs, development becomes faster, cleaner, and less stressful.
Yes, this approach takes more effort at the beginning. But in return, it gives stability, confidence, and freedom to evolve your product without fear of breaking everything.
What’s Next
That’s all for now.
In the next article, I’ll describe one of the _core classes I actively use — Result, and explain this pattern in more detail.
Top comments (1)
Great read (a bit above my level haha), thanks Dmitrii. Question though - How do you handle the trade-off when a tight deadline is "pushing" the team to skip the layered approach?