In this article, we'll look at how to build large Angular applications. I'll discuss techniques that will help you keep your project scalable, maintainable and robust. Let's get started 🚀.
Although it has a rather steep learning curve, after you get the hang of it you can start developing pretty quickly. Creating small and even medium sized applications with a few modules and one hundred or so screens is rather an easy thing to achieve. On top of that, maintaining such applications is also fairly simple as long as some thought was put into the organization of the project.
However, things are a little different when you are faced with constructing enterprise level applications. Thousands of screens, uncountable forms, complex bi panels loaded with graphs and a seemingly infinite set of new features. On top of that, the bigger the project the more people work on it further increasing the complexity of the problem.
In a scenario like this, some of the most common problems that arise are as follows.
Some components end up being implemented more than once because a developer simply couldn't find the original. This creates confusion and when a change needs to be made to the component you must update multiple files.
Another example of this is API logic. If a developer doesn’t know a wrapper for an endpoint has already been implemented he might end up creating a duplicate that might end up having different parameters. As a consequence, any changes to that endpoint in the back-end will warrant updating multiple files.
This can also happen for common CSS styles such as system colors and types. If these styles aren’t centralized an update to the system palette or typography will mean having to change hundreds of files. This only gets worse as the system gets larger.
Newer versions of Angular have been cutting down on compilation time considerably. Regardless, it is not hard to reach an application size that will take 30 minutes or more and for a large platform to break the hour mark is just a matter of time. In an environment where automated testing is part of the pipeline this means that every PR sent by a developer will take more than an hour to be approved. Additionally, a team of 8 developers that each commit twice a day means a server will spend 16 hours a day compiling and that’s ignoring the cases where conflicts occur.
Also, as the application gets larger running it locally will consume more resources and take longer. Overall, long compilation times make the development process cumbersome and slows team progress.
Normally when the application is ready for deployment it will be compiled into a set of artifacts. This means that regardless of where the change was the entire application has to be recompiled and updated. Even if it is a single line of code.
These are certainly not the only issues that web applications face (large or small). Also, there are more than one way to handle each and every one of the problems mentioned above. In this post I’ll like to discuss some of the solutions that I have come across throughout my career. Also, many of the proposed solutions are language agnostic.
Don't Depend on Dependencies
I’ve found that many issues you encounter during development are caused by bad choices when it comes to what libraries and utilities to use. For example, assume you need a very basic image slider that would take 3 hours to implement. You can build it or pick up a library that has that feature and save 3 hours of work. However, if every time you have to make a choice like this you pick the second option you will end up, in the long run, with a bloated system that is limited to what these libraries can offer.
First of all, over the period of a couple years, some of these libraries are bound to be deprecated. Also, if you need to implement a feature that a library does not allow you only two choices (a) implement the solution from scratch which beats the purpose or (b) submit a pull request and hope that it gets accepted. Of course, you could fork the project and manage it yourself but at the end of the day you were better off investing 3 hours to make this simple image slider.
By no means I am suggesting that you should implement everything from scratch. Quite the contrary, whenever possible you should use tools that allow you to build better products in less time. However, a lot of consideration should be put into these decisions in order to avoid setbacks in the future.
When picking a CSS framework, be sure to pick one that solves your problems specifically. Avoid mix and matching multiple frameworks as this creates a bloated application. If you are only going to use the reset utilities and grid system then only import those parts and document that decision in the Read.me file. If you are going to use components such as forms and buttons make sure the design team approves and that any changes that they want to make are possible.
In summary, avoid mix and matching libraries that do similar things and pick as few tools as needed to solve your problems. Whenever possible implement features by hand.
Clean Code Practices
This goes without saying but clean code goes a long way in making any size project (of any language) live a longer and healthier life.
This post by Chidume Nnamdi goes into more detail on how to write better performing code for Angular.
Typescript comes configured by default in Angular CLI application however not everyone uses it to its full potential. By defining function signatures and reutilizing types and interfaces throughout your entire application you create consistency. First of all, you are able to detect and prevent errors during compilation. Also, typescript enables better code completion and enhances developer efficiency. There is virtually no reason not to use Typescript in any project and for enterprise applications it is essential as it allows you to scale your code.
Lazy Load Modules
This technique is very simple to implement and consists of delaying the loading of a module until the user actually needs it. Therefore, the client only loads the main module instead of the entire application. And as the users access certain parts of the application the needed feature modules will be fetched. This is done by configuring your main routing module as indicated below.
This technique significantly increases performance and for large applications is a must. It is possible to implement multi level lazy loading. In other words, a lazy loaded module can also lazy-load its sub-modules.
Multi Project Workspace
Before moving along to the other topics a quick briefing on multi-project workspaces in Angular. When developing in Angular using CLI you are developing within a workspace. The workspace contains the source code for one or more projects as well as a series of support and configuration files that are shared between these projects. By default the Angular CLI will create a folder structure for a single application. However if you run the command below when creating a new project you’ll end up with an empty workspace:
From here on out you can create applications and libraries that live within the same workspace:
An application is compiled individually and can be served as a web app. A library, on the other hand, is better suited for components, services and utilities that are shared among applications within a workspace. This is described in detail on the angular website.
Extract API Logic
When creating an API wrapper in Angular a common approach is to create a few services within the main application for each API and implement any wrapper logic within the methods of the service. However, as more features get added the application will increase in size and become harder to maintain. Also, this means that API wrapper code is bundled with application code and will have to be compiled together. This is illustrated in the figure below.
This can be done more semantically by creating an Angular library for each independent API. That library will hold all services that have wrapper logic for all endpoints of that single API. This also includes the interfaces and/or type definitions for the data returned by the endpoints.
As a consequence, everything associated with an API will be separated into a single library. This isolation means better testing and the ability to share the API logic with multiple applications in the same workspace. On top of that, unless there are changes to the library, that code doesn’t need to be compiled again. This new proposed layout can be seen in the image below.
Create a Library of Components
The same concept can be applied to visual elements commonly used. In order to keep the main application thin and light you can extract visual elements into its own library. Some examples of things that could be placed into that library include:
The benefits from this extraction are the same mentioned above for the API library. Allows you to develop visual elements in an isolated way improving testing and debugging. Also, it makes it easier to track what visual components are available to the front-end developer. Especially when coupled with a component catalog tool such as storybook. The image below illustrates the proposed adjustment.
Fragment your Application
There is still one more step we can take towards improving the scalability of the application and that is to actually split the application into smaller logical pieces. This means that they are independent from one another, are compiled and deployed separately. They still exist within the same repository and angular workspace though.
To guarantee separation of concern an application should not import any code from another. All code that is needed by more than one application should be extracted into a component, API or utility library. This helps prevent dependency loops and simplifies the relationship between these units. The updated architecture can be seen below.
Much like the other improvements, this further simplifies development, debugging, testing and deployment of each sub application. If needed, each feature can now be compiled and deployed independently. Multiple teams can be on different features in parallel and not interfere with one another at all. Another benefit is that because all the applications are within the same workspace they all share the same node dependencies, lint styles and angular version. This further improves consistency.
Finally, to update the visual of the platform all it takes is updating the Component library. Likewise, updates to an API implementation will only reflect in a single library.
But There is a Catch...
This approach is not without a cost however. You might have noticed that I mentioned that each feature will be deployed independently. This means that each feature is located in a different URL as indicated in the image below. Because of this each time you visit a route from another feature the browser will reload. This will cause a visual interruption as well as clear the state kept in memory. On top of that, depending on how login state is managed you might have to login once per feature.
There are solutions to each of those problems. If the visual interruption is an issue for your use case you can use an intermediate application that is responsible for rendering the layout. The wrapper application can then inject the appropriate sub module inside the layout through an iframe. This way you can show a loading screen while transitioning between applications, effectively solving the interruption problem.
As far as the login issues go, one of the solutions is to share state between apps through local storage (assuming as the applications are hosted in the same domain). You can use this state to inform the other applications that you are already logged in.
And that concludes the end of this post! I hope you found this to be useful 🤓. In case you would like to get in touch: linkedin.