Designing scalable Angular applications
What is the best scalable architecture for Angular applications? This is a difficult question. If you search for “scalable Angular applications”, you will find a lot of articles that have a lot in common. I will try to summarize the most important points, list weak points and afterwards suggest my architecture variant.
The main design recommendation is to introduce an additional layer between component and service classes. It is normally called “Abstraction” or “Facade” layer. Article writers say — this layer acts as sandbox. It only delegates the calls from UI components to the service / core layer and doesn’t have any business logic. Decoupling presentational logic from the core have several benefits, such as:
- UI components stay lightweight because dependencies like async or state management services are not injected into the UI components.
- UI components and the entire application become better testable. We can more easier mock the application’s parts.
- Better separation of concerns. Components don’t need to know who provides the data and where they save the data. The communication between application layers occurs through well-defined API.
- Software artifacts in separate layers can be developed in isolation.
- Centralized application’s state (single source of truth).
- Immutable application’s state which allows to boost performance of Angular apps by using OnPush ChangeDetection.
- Truly debuggable applications because you can always reproduce the past state (time-travel debugging!).
- Unidirectional data flow. Such data flow leads to less failures.
The third concept to be mentioned here, is the Container vs. Presentational components that are also called Smart and Dumb respectively. Container components contain child components (that is why they are called “container”). Presentational components are the leafs in the component tree. They communicate with the parent components via Input and Output decorators. That means, they get data from parent via @Input and emit data to parent via @Output. Only container components may work with underlying services and have some business logic (that is why they are often called “smart”).
You can read about these topics in the following articles:
- Angular Architecture and Specific Layers
- Developing scalable Angular applications
- A scalable Angular 2 architecture
- A scalable angular architecture (part 2)
- NgRx + Facades: Better State Management
- Angular architecture patterns — High level project architecture
- Component architecture with Angular
- Container Components with Angular
The mentioned concepts are really brilliant, but they have weak points in some situations. For instance, just to mention a few:
- In real-time GUIs with messaging (push) architecture. That’s where not the user interactions initiate requesting data per REST, but the backend services push some data to the frontend.
- In GUIs with Canvas / WebGL graphics. These kinds of GUIs don’t have HTML markup for every graphic element. Only a canvas tag is present in the DOM. Therefore, only one component’s template would be exist. Writing a component per graphic element has no sense.
- In web apps with complex workflows. It is not clear where to place a “workflow orchestrator”. A new extra layer for that is overengineering.
- Sometimes, presentational components should work with services too, to avoid repetitive inputs and outputs in a larger component tree with deep nested components. You can read about this issue in this article: Angular Architecture — Container vs Presentational Components Common Design Pitfalls.
To fill in the gaps, I would like to suggest an improved variant of the scalable architecture. The main postulates of such architecture are:
- The software is sliced horizontally by layers. When slicing horizontally, we group the code by software layer. There are three layers: view, facade and service. Every layer has its own responsibility.
- The software is sliced vertically too. When slicing vertically, we group software artifacts by feature modules. Feature modules are well-known in the Angular world. There is a common feature module having features shared among other feature modules.
- The data flow into the following direction: Service layer → Facade layer → View layer. The emitting events, executing logics, dispatching state management actions go into the opposite direction: View layer → Facade layer → Service layer.
- Business logic resides in smart components and smart facades. In my opinion, the term “smart” fits better a piece of software with business or workflow logic because the term “container” is misleading for that (read the mentioned above article). View related logic, triggered by user, normally resides in smart components. But there is also logic such as workflow steps, that reside in facades. I suggest to call such facade a smart facade (similar to smart component). This is especially a case when you have a bidrectionnal communication and the data in real-tme (e.g. over WebSocket).
- View layers of separate modules should not depend on each other. For instance, the view part of “Some Feature Module” should not have dependencies from the view part of “Other Feature Module”. The view layer of one module can inject several facade layers from every other module.
- Facade layers of separate modules may communicate horizontally with each other. By this way, the view layer of one module has an indirect access to every service across the whole application.
- What is about the service layer? In my opinion, the service layers of separate modules can also communicate horizontally with each other (optional if needed).
The building block diagram illustrates what was said.
The next data flow diagram illustrates an imaginary use case for a feature module called SelectionModule. The workflow for the click event is orchestrated by SelectionFacade. The user clicks somewhere on the canvas. The clicked graphic element will be detected. A special “select” action is dispatched to the Redux store via StateManagement service. All related graphic elements will be detected in the store and selected in the StoreFacade. These elements get rendered by the RenderingService (they become colored with a “selecion” color). After that a subscription to a topic “something” via MessagingService will be processed. The backend sends the topic related data that get rendered in real time.
As you can see, the SelectionFacade is a smart facade and the StoreFacade is a dumb one.
That’s all. Have fun!
===== Edit. 04.03.2019. =====
After trying this architecture in a real-world project, I have refactored it. There are 4 layers now. I have introduced a new layer — the business layer. This is a layer where all business logic (use cases & workflows) resides. The facade layer stays dumb. The updated scalable architecture is shown on the basis of two different views — cutting by layers and modules respectively.