Developing scalable Angular applications
A couple of months ago I started working on a new version of a software product. The project was to modernize an existing product to make if a responsive single-page application. As in most modern single-page applications, we were expecting it to be a fat client which encapsulates a decent amount of business logic in the future.
When a single-page application grows over time, there will be troubles maintaining and extending features. This will often lead to early technical debt, a lots of frustration on breaking things and fixing bugs.
One of the biggest challenges in designing a single-page application might be: extending existing logic and implementing new features quickly without breaking the app.
What does it means that app is scalable?
Single-page applications will always run as a single, separate application instance for every user, so there is no higher number of users challenge compared to back-end applications such as MVC. Instead single-page apps have different set of challenges such as increasing the size of data loaded, growing complexity and size of the code which results in longer loading times, slower applications.
An application with bad, or not scalable architecture, tends to be very hard to develop after some time.
Modular design
Angular2 itself make us use components. Basic philosophy here is to make everything a component, even pages and the application. Some very simple but very important tips in designing components are to keep them small and dumb as possible.
In our application, we used the idea of ‘smart’ and ‘dummy’ components. The idea behind the division is to clearly define the components of the application that contain business logic, communicate with services and do actions. In comparison, dumb components contains minimum logic or no logic at all. All the communication happens between dumb components and containers using @input parameters and @output event emitters.
We also kept a clear separation between app module, core module, shared module and feature modules. It is easy to create cyclic dependencies between modules when responsibilities are mixed between core, shared and feature modules. Feature modules should be kept isolated and independent as much as possible. Core and shared modules should be essentially service modules when feature modules are often widget modules.
We adapted a layered architecture in designing modules. The top layer includes UI (presentation) components on which users would be directly interacting. The facade represents a set of model objects as an abstraction to hide business logic and to provide simpler interface to UI components where necessary. These models may consume async (HTTP) services or state management services.
Unidirectional data flow
In complex single-page applications, one of the biggest challenges is to deal with the data and state information to flow across layers of the application. This is usually the part of the application which gets complicated very easily.
In Angular2+, data flows from top to bottom; from parent component to child component and from the component to the template. There are instances where components are dependent on each other at completely different points in the component tree. These occurs when having to pass data deep down the tree, and react to events several up in the component tree; or having sibling components interdependent. If we only use Angular@Input and @Output as the component communication mechanism, developers would run into troubles quickly.
One of the ways to deal with these issues is to enforce a unidirectional data flow on an application wide level. One could implement Angular services for this. But we found Redux provides a solution for more complex component interaction scenarios ensuring unidirectional data flow.
Explicit state management
State, is an special type of data which flows through the application. Everything what users see on the screen is a reflection of the state of the application, When users interact with the application, some action is performed or any event occurs/some data loaded and the view is updated. Such a cycle would result an application state change.
The idea behind Redux is that the whole application state is stored in one single store, which represent the current application state. A Redux store is immutable. That means it can not be modified, every time a state change occurs, a new object has to be created.
Flux is a fancy name for the observer pattern modified a little bit to fit React. Flux is a pattern and Redux is a library.
There are some big advantages of this approach.
- We could decouple components and modules from each other.
- Maintainability
- Application could persist the state into local storage, server and boot from it
- Serialize user actions and attach them, together with a state snapshot, to automated bug reports, so that the product developers can replay them to reproduce the errors.
- Maintain an undo history or implement optimistic mutations without dramatic changes to how the code is written.
- Testability. Travel between the state history in development, and re-evaluate the current state from the action history when the code changes.
- Provide alternative UIs while reusing most of the business logic.
A possible solution
Ngrx/store is a RxJS powered state management for Angular applications, inspired by Redux which has lots of features you could use in your Angular applications. We decided to go for a simpler implementation of Redux pattern with our own state management services in the Angular application. We kept state management layer separated from asnyc HTTP services to preserve single-responsibility principle.
Summery
This architecture is the way we implemented things in our recent projects. It is one way of doing things in Angular applications.
Unfortunately, even a framework as opinionated as Angular can only enforce the basics of application architecture. That’s sufficient for small or medium applications. However,when comes to complex applications dealing with complex data collections and complicated user flows, adapting Redux pattern.