Angular architecture patterns — High level project architecture
These series of articles have arisen after months of research and work on Angular 2 projects. It’s based on experience and observations while trying to create a project architecture which will be sustainable and what’s more important it represents a set of guidelines to keep the project organized and maintainable. The goal is to divide the application into functional layers and define responsibilities and restrictions for each one of them. The theory behind is framework agnostic and can be applied in any framework but the code examples are written in Angular 4.
We’ll be explaining each piece of the application starting from a higher level to more details. The final result will be an example project to illustrate how some of the architectural problems can be solved.
Disclaimer: The architecture explained here is a suggestion which means it doesn’t have to fulfill your needs but it can help you structure your own project. It provides guidelines and suggestions to decouple certain parts of your application in order to make it more flexible and maintainable.
This is not a strong set of rules because every type of application requires it’s own architecture to consider.
The series includes the following chapters:
- High level project architecture (current chapter)
- Detailed project architecture
- Additional application features
Demo application with full source code is published on Github.
In the first chapter we will go through the initial steps for creating a blank project and we’ll describe a high-level project architecture.
Overview
Angular has the great environment and supporting tools which help you to get started and set basic hello world application. The great thing about starting with Angular is that there’s no tons of starter kits and boilerplates to waste time with, figuring out which one satisfies your needs. After a bit of googling you’ll find out there are only a few of them which have everything, you need to create a blank app. Here’s the list of the most popular ones:
There’s also some other starters available but these ones are proved which means it will be regularly maintained and up to date with the newest Angular updates.
In this article, we’ll talk more about how to organize an Angular project after initial setup with one of these tools and how to make scalable architecture.The theory behind is framework agnostic and can be applied in any framework but the code examples are written in Angular.
Before we start developing any kind of project there are some important things to consider in order to get a high-level overview of what we need to do:
- Setup a blank project
- Define high-level project architecture diagram
- Define each layer in more details
- Add extra features to fulfill the project
Blank project
Setting up a blank project is probably the easiest part. In literally two minutes you can generate a new project by choosing one of the previously mentioned tools. We chose Angular CLI because it’s developed by the Angular team, the generated project is totally blank and clean and of course, you get a CLI tool with it. CLI (command line interface) means of interacting with our Angular application by issuing text commands in the console. This way we can create new components, services, build an application for development and production etc. by using the command line.
The good thing is that everything is pre-configured, you don’t need to mess with a webpack.config.js file if you don’t want, but at the same time, there’s the ability to create one and override the existing configuration or add new plugins.
We’ll not go too deep into how to setup Angular CLI and if you need more info you can find it here.
Basically, everything you have to do is to install Angular CLI on your machine, create a new project, with built-in command, and run it in the browser (supposed you have all prerequisites installed).
npm install -g @angular/cli
ng new PROJECT_NAME
cd PROJECT_NAME
ng serve
High level project architecture
Well, this is the tricky part. It depends on the size of the application you are building, how many team members work on the project and what personal preferences you have. We are going to assume that we are working on the project which tends to grow and new team members, with different level of expertise, should be able to join the team and start working on one part of the application even if they are not familiar with the whole architecture graph.
These requirements impose for the application to have some basic features:
- Modular design
- Unidirectional data flow
- Predictable state management
- Communication layer for async requests
- Decoupled presentational layer from core layer
We can already sketch a rough block diagram of the requirements we have so far. The following design is inspired by Nicholas Zakas and his sandbox principle implemented in Aura framework. Let’s analyze each feature to see what benefits it brings in our life.
Modular design
This means that we should be able to plug in/out our modules easily when needed, make each part of the app testable and enable multiple team members to work together on the project. This approach decouples the application into blocks of functionality and makes it easier to maintain and test.
From the picture above we can see that application is divided into three layers - presentational, abstraction and core. The presentational layer is used for displaying HTML and handling user interactions. Abstraction layer handles communication between presentational and core layers. Core layer contains application core logic e.g. data manipulation, communication with outer world etc. Each layer is separated into modules, organized by features. Modules are standalone blocks of code which can be plugged in/out. By looking the figure 1 each block would be separated module. Features are logical units the modules are divided by. It means we will separate async services and data management into their own modules because they do different jobs. The same goes for presentational modules. We can separate authentication module from dashboard module and so on.
Presentational modules are standalone units independent of other modules. They include necessary dependencies (e.g. configuration, utility module…) from application core and they do that through the abstraction layer. The abstraction layer provides an access to application core so that presentational and core layer can be decoupled. The presentational modules include their own routes as well. This is not necessary but we can give this responsibility to presentation layer since it fulfills the whole picture of one feature module, because all related parts are in one place. Another benefit of this approach is that routes have a tendency to grow more complex so it’s much easier to maintain them when they are separated. More about routes configuration can be found in Angular Routing Docs.
The same thing stands for the other application layers. We can add our services as modules. They can be internally developed or added as third party dependencies. For example, we can add utility module which holds all helper functions and logic reusable through the whole application. We can also use modules for shared UI components (smart and dumb) and include them in other presentational modules.
Unidirectional data flow
In modern SPA frameworks, everything is a component. They are the main building blocks for creating and controlling user interfaces. Angular (and other modern frameworks) will organize components in a hierarchical tree, which means that components can have a parent and children. Let’s imagine that our components communicate with each other with no rules. Any one of them would be sending data and firing events to each other and after a while, it would become very messy and we would be lost in the woods of data requests and responses.
With this kind of organization, we need to assure unidirectional data flow within our parent and child components. The main rule is that actions go up and data flows down. Every component will accept @Input() parameters to receive the data from their parent and be able to send the @Output() event to notify subscribers that something has happened.
Components are divided into smart (containers) and dumb (components). Smart components are the ones which listen for their children’s events, communicates with the application core layer and pass the given data to their child (dumb) components. Dumb components know nothing about the rest of the application. They just display the user interface and handle user interactions by passing the events to their parent.
Components, in general, need to be as small as possible and as dumb as possible. For more information about components composition, you can read: Components demystified, Data flow angular2 applications, Components interaction.
Predictable state management
A state is a javascript object which holds the application data structure. Here we can store the data needed to display to the user like a list of products, information about logged in user etc. The state is one of the most important things in the whole application architecture (I dare to say the most important one). Why? Because modern single page applications have multiple state mutation sources. User interactions, https requests, push notifications from real-time services, peer to peer connections, SQLite database storage, all of them can mutate the state. Predictable application state is essential in order to avoid confusions and has different versions of state with different data across your application.
We need to have one centralized place to hold our data. For this purpose, we have chosen ngrx store which is basically redux version for Angular with implemented observables. You could choose some other library or develop it by yourself, but one essential thing you should pay attention to is to make the functions, which change the state, pure and not mutate existing objects. It means that if we have javascript object e.g. product
var product = {
id: 8,
name: "Television"
};
and if we want to change the value of name property, we need to create a brand new product object with the changed value. Why are we doing that?
This is important because mutation causes bugs which are not easy to debug and Angular change detection will not work as expected if the data is not immutable.
To explain immutability more clearly let’s say we have a UI component which has an input parameter Product. The component waits for the Product object to be passed in, in order to display the product data in the template. Angular will perform change detection on that component (and pass in new product data) only if the reference (in memory) of the component’s input parameter has changed and the reference is changed only if the data was created with pure function . The reference will change only if we recreate the existing object entirely. The old one will be garbage collected because it has no reference in the memory any more. This way Angular speeds up the change detection by eliminating the components which input parameters didn’t change. As the result, the whole branch of the component tree can be ignored.
For more information about change detection here’s the great article and talk by Pascal Precht ʕ•̫͡•ʔ.
Communication layer for async requests
Almost every action which mutates the state will be asynchronous. We‘ll have HTTP services for sure, but it’s very possible that after some time the requirements change and we need to introduce some kind of local storage, e.g. SQLite database (for storing user preferences, PWA mobile app etc.). Or we might need web sockets, or peer to peer communication. All these types of communication are asynchronous.
In asynchronous calls, we need to handle errors, display common notification messages, intercept requests, parse responses etc. It’s very unhandy to handle all of these actions separately for each request. We would end up with a lot of repeated code, which would break the DRY principle, and we would waste a lot of time writing the same things all over again. For the sake of simplicity, we need one place to handle communication logic — communication layer.
There’s one rule we have to follow: Don’t let the async services know about the state management logic. Maybe it comes naturally to save the response data into the state right in the service, but in that case, we tightly coupled communication layer with data management layer. Do you remember what we talked about in the Modular design section
Async services should only perform async requests and return data. We can also do some extra logic here, like set default headers, parse the response, handle common errors or plug in data adapters. We’ll talk about handling data after performing async requests later in more detail. For now, we should separate these concerns.
Decoupled presentational layer from core layer
Decoupling presentational logic from core means that we should not be injecting core dependencies like async or data management services into UI components. That means your components should not look like this:
Maybe you got used to handling state updates or manage the HTTP requests from the smart component (container). This architectural approach advocate that this kind of coupling should be avoided. Why is this important to us? Our modules should have single responsibilities and do only one thing, which is defined by the Separation of concerns principle. This means that presentational layer should care only for the presentation and not be putting their hands into the core logic. We have to draw the line somewhere and say that async services and data management are the core parts of our application. Other benefits from this design are:
- Components will be much easier to test because we don’t need to inject store and async services in our tests
- Components will be much easier to work with because they are small
- Easier to split up into multiple developers tasks
So the smart components will be aware of the application state but they will not know how to manage it. The same goes for the other core stuff. However, they will have access to an abstraction layer to communicate with the rest of the application and manage their child components by passing data to them and by handling their events.The abstraction layer will behave as a mediator and the facade for the presentation layer. It means that it will expose an API and coordinate the communication between multiple presentational components and application core.
The abstraction layer will behave as a mediator and the facade for the presentation layer. It means that it will expose an API and coordinate the communication between multiple presentational components and application core.
In this blog post we established a basic idea of the project we are going to build. We decoupled project concerns and defined a foundations where we will be adding additional features on. In the next blog post we will describe each functional module in more details and start building them in the code. Stay tuned.
Originally published at netmedia.io.