0% found this document useful (0 votes)
709 views

eBook Angular Enterprise Architecture Tomas Trajan Angular Experts v2

Uploaded by

Lemu Yokoi
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
709 views

eBook Angular Enterprise Architecture Tomas Trajan Angular Experts v2

Uploaded by

Lemu Yokoi
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 207

Angular Enterprise

Architecture
Learn how to set up a new Angular enterprise project in
a scalable, maintainable way with automated
architecture validation to ensure long term success of
your project!

Tomas Trajan
@tomastrajan

v2.4.0 ©2024
Page internationally left blank

2 Tomas Trajan
@tomastrajan
Table of content
About author 10
Foreword 11
Acknowledgements 11
New experience and lessons learned 13
Reasons for releasing v2 of the ebook 14
Optional NgModules 14
Esbuild 14
Automated architecture validation 15
Main differences between v1 and v2 of the book 15
Theory 16
Why clean architecture matters? 16
Typical scenario of an average Angular project 17
Chaotic dependency graph 18
Clean dependency graph 20
Hope based architecture 22
Angular Enterprise Architecture teaser 23
Base underlying reality 24
The browser 24
The bundler and the bundles 26
Angular 28
Standalones (previously declarables) 28
Template context 29
Injectables 31
Injector hierarchy 32
Other injectors 35
Function based logic 35
Eager vs lazy part of the application 37
Eager part 37
Lazy part 37
Routing based lazy loading 38
Defer based lazy loading 39

3 Tomas Trajan
@tomastrajan
Table of content
Key concepts 41
Lazy loading 41
Lazy loading benefits 41
Faster start up times 42
Faster developer feedback loop 44
Isolation 45
Lazy loading heuristics 48
Isolation 50
Isolation guarantees 53
Local impact 53
Local testing 53
Isolation improves extensibility and maintainability 54
Architecture promotes isolation 54
Feature as a black box 54
One way dependency graph 56
Implementation hints 57
State vs view logic 57
Correct level of abstraction 59
Framework vs application business logic 59
Short term vs long term thinking 59
Optimizing for the wrong thing 60
Common negative consequences of adding abstractions 61
Business logic related 61
Framework related 62
Correct level of abstraction 62
How to handle verbosity 63
Summary 64

4 Tomas Trajan
@tomastrajan
Table of content
Angular Enterprise Architecture 65
The big picture 67
Types overview 67
Core (eager) 68
Standalone core 69
Angular (and state management) specific providers and setup 70
Infrastructure specific providers 70
Initialization logic and kick-starting of processes 70
App specific core logic needed from start 71
App specific core logic shared by more than one lazy feature 72
Grouping core logic in folders per domain 73
Other utils 73
Providers from NgModules 74
Layout (eager) 75
Single main layout used by the whole app 76
Multiple layouts 77
Custom layout per feature 78
UI (eager / lazy) 79
Standalones only 80
Generic reusable UI components 81
Eager / lazy bundling implications 82
How to handle types and interfaces 84
How to handle complex pipes 87
Feature (lazy) 89
Isolation and the "black box" or "throw-away" nature of lazy features 90
Nested lazy features 91
Fractal nature of the lazy sub features 93
How to share logic between multiple lazy features 94
Types of logic that we might want to share 94
The "extract one level up" rule 95
Example of sharing logic between lazy features 98
Sharing UI components between lazy features 98
Sharing NgRx state slices between lazy features 100
Sharing patterns between lazy features 102
Sharing lazy features themselves 103
Why are there no eager features 105

5 Tomas Trajan
@tomastrajan
Table of content
Angular Enterprise Architecture 65
The big picture 66
Types overview 67
Pattern (lazy / eager(rare)) 107
Why do we call it pattern or a "drop in" feature 108
Pattern vs ui, core and feature 109
When to create a pattern 110
Pattern examples 113
Document manager 113
Approval process 113
Change history (audit log) 114
Notes / comments 114
Helper types 115
App (eager) 116
Main (eager) 116
Ignoring infrastructure types 117
Relationship between types 118
The most common dependency graph issues 122
Automated architecture validation 124
The eslint-plugin-boundaries anatomy 125
Basic setup 125
Types definitions 126
Rules definitions 132
Configuration of rules per project 137
New project bs existing project (strictness) 137
Architecture vs implementation 138
Limitations and alternatives 139
Ideal fit 140

6 Tomas Trajan
@tomastrajan
Table of content
Hands on architecture 141
Setup and project creation 141
Setup environment 141
Create new workspace 143
Setup IDE (Webstorm, VS code) and generate application 143
Prettier support 147
Bundle size analysis (and budgets) 149
Dependency graph analysis 153
Eslint support 155
Schematics support 157
Schematics pre-configuration 161
3rd party schematics 162
Automatic migrations with ng update 163
Component library and styles utils 164
Add Angular material 164
Add Tailwind 167
Final cleanup 169
Scaffold architecture skeleton 170
Preparing folder structure 170
Core 170
Layout 172
Feature 176
What about "Shared Feature" 179
UI & Pattern 179
Setup automated architecture validation 180
Adding more types and rules 188
Example project 191
Growing the workspace and collaboration 192
Adding other apps and libs 192
Collaborating with multiple teams 192

7 Tomas Trajan
@tomastrajan
Table of content
Wrap up 194
Links 196
The eBook and example project 196
References from the content 196
Tools and libraries 197
Contact and social 198
Offered products, workshops and services 198
Resources 199

8 Tomas Trajan
@tomastrajan
Page internationally left blank

9 Tomas Trajan
@tomastrajan
About the author

Tomas Trajan
Google Developer Expert for Angular & Web Technologies
@tomastrajan

I enable developer teams to ship successful Angular


applications through the delivery of expert level core
architecture, training & consulting
A Google Developer Expert for Angular & Web Technologies working as a
consultant and workshop teacher with a focus on Angular, NgRx, RxJs and
NX. Currently, empowering teams in enterprise organizations worldwide by
implementing core functionality and architecture, introducing best
practices, sharing know-how and optimizing workflows.
Tomas strives continually to provide maximum value for customers and the
wider developer community alike. His work is underlined by a vast track
record of publishing popular industry articles, leading talks at international
conferences and meetups, and contributing to open-source projects.

public

10 Tomas Trajan
@tomastrajan
Foreword
In this book we are going to learn how to scaffold a new enterprise-ready
Angular application with clean, maintainable and extendable
architecture and make sure it stays that way with the help of automated
tooling-based architecture validation!

The content of the book is structured in a couple of main parts which will
helps us to first build an understanding of the underlying concepts and their
influence on the proposed architecture.
Then we’re going to explore each part of the architecture in depth, and finally
we’re going to use everything we’ve learned hands-on to create a new
Angular workspace, application and scaffold the architecture from scratch!
Just like in the example repository which showcases the end result!

11 Tomas Trajan
@tomastrajan
Foreword

info The example repository can be downloaded on the "purchase


success" page by providing the email address used for the
purchase and then clicking on the download link.

The provided example repository also represents a great starting point


for developing a new Angular application as it comes with the entire
architecture validation setup as well as commonly used libraries like
Angular Material, Tailwind CSS and Jest for testing!

As with the previous versions of this book, it is meant to be a living document


which will be updated with new content and improvements over time.
Please, do share your feedback, both positive and negative, as well as
"content requests" and testimonials as it will help me to improve the
content and your feedback will make the book better for everyone else!
You can reach me at @tomastrajan or via email at [email protected]

thumb_up All the future updates of the content will be available for free to
everyone who already purchased the eBook! Usually in the form
of email with the short description of what's new and a download
link for the updated version.

12 Tomas Trajan
@tomastrajan
Acknowledgements
This book is only possible because of the amazing Angular community whose
members have been very generous with sharing all the know-how and
experience they have gained over the years. This continuous exchange and
challenging of ideas brings the whole ecosystem forward!
I would like to thank all the people who have contributed to this book. A
special thanks to Kevin Kreuzer for long-term collaboration and feedback on
all things Angular. Many thanks to Tomasz Ducin for great ideas and Daniel
Glejzner for ongoing feedback.

New experiences and lessons learned


Looking forward, we will continue developing new approaches that can both
improve and simplify the development of Angular applications at enterprise
scale.
These approaches are then implemented and tested in large enterprise
environments of my clients. For example, the main client with 180+ Angular
apps and 30+ libraries as well as other clients where teams work on a smaller
number of product focused applications.

13 Tomas Trajan
@tomastrajan
Reasons for releasing v2 of the eBook
This is the second major version of this book. The reason I went with overall
rework instead of incremental updates is that there have been lots of very
impactful developments in Angular starting with v14 with the introduction of
standalone APIs.

Optional NgModules
This allows for managing declarables like components, directives and pipes
without NgModules as well as a new simpler approach to creating and
configuring applications with the new “provideX” approach.
This change has made NgModules optional and therefore has a significant
impact on the way we architect Angular applications.

Even larger apps in standard Angular CLI


workspace with esbuild
Another important breakthrough was the introduction of the esbuild for
Angular build pipelines, which is significantly (~70%) faster than previous
webpack-based builders.
This means that even pretty large applications can be developed in Angular
CLI based workspaces without the need to switch to something like NX
which would bring additional complexity and associated learning curve!

14 Tomas Trajan
@tomastrajan
Reasons for releasing v2 of the eBook
Automated architecture validation with eslint
Being able to automate architecture validation and guarantee that it stays
the way it was intended over the lifetime of the project is a major
improvement that increases development velocity and the odds of project
success!

The main differences between version 1


and version 2 of the book
For the folks who already purchased the v1 of the book, the following will be a
quick overview of the key differences between the versions:

v2 uses and focuses exclusively on the new standalone APIs and no


NgModules at all
v2 comes with the automated architecture validation with the help of
eslint boundaries plugin, which is a major win that helps us to keep the
architecture clean over the lifetime of the project
v2 splits the concept of “shared” into two separate and more focused
concepts of “ui”(generic UI components) and “pattern” (drop-in features /
non-routed features / data bound UI components)
v2 extracts the concept of “layout” outside the “core”

15 Tomas Trajan
@tomastrajan
Theory
The first part of this book will help us build up theoretical understanding and
mental models because a strong foundation will allow us to make sense of the
approaches and tradeoffs of the proposed architecture approach.
In other words, it’s always a good thing to understand why we’re doing what
we’re doing, which is vastly superior to just following the provided steps. This
will help us keep making the right decisions once our project grows beyond
the most basic setup!

Why does clean architecture matter?


Virtually all Angular projects that have been created exist because they serve
some kind of need that is perceived as valuable by their users.
What is valuable to the users also tends to change over time as the world and
the landscape keeps changing with ever-increasing pace. For example, in the
past it was prevalent to roll out our own authentication solutions where users
needed to register and create new accounts. Nowadays, in the world of
single click OAuth-based sign-ups and sign-ins, the old approach may lead to
a perceivable drop in users who just can’t be bothered to fill out forms to
start using your product.
In practice, virtually every single user flow that is implemented in the
frontend needs to adapt and improve to stay competitive with other similar
offerings on the market, which brings us to the need to be able to evolve our
products at a good enough pace!

How does this relate to architecture?

16 Tomas Trajan
@tomastrajan
Theory
A typical scenario of an average Angular project
At the beginning of the project, a developer or a team of developers is able
to move quickly and ship new features with expected velocity.
Over time, the technical debt, especially in the form of ever-increasing
coupling between the unrelated parts of the code base, will start to have a
noticeable impact on delivery speed.
The new features and updates to existing features will take more time and
often introduce bugs in what is from the outside perceived as a completely
unrelated feature of the application!

We have seen this time and time again in various code bases (both apps
and libs) within many organizations. As such, it’s a predictable trajectory
for most of the Angular applications without a clear idea about how the
architecture should look like and how to maintain it over time.

This book will allow us to build a strong foundation together with automated,
tooling based architecture validation which can preserve development
velocity as the project enters the mid to long-term stages of its lifecycle!
Let’s explore a couple of examples of what tends to happen in real life
applications without much thought about the architecture…

info Following examples will use madge , a great node.js based tool
for visualizing of dependency graphs.

17 Tomas Trajan
@tomastrajan
Theory
Chaotic dependency graph
The madge is one of the best tools that we can use to evaluate the overall
health of the code base from the perspective of its dependency graph. We’re
going to discuss how to add it to the project later in the hands on
architecture part of this book.
Right now, we’re going to use it to explore example visualizations which
showcase how it looks like when architecture gets more and more tangled
over time.

info The image above is a small cut-out, which displays approximately


15% of the overall dependency graph of a real life project.

18 Tomas Trajan
@tomastrajan
Theory
Actual names of the files don’t really matter, but it is immediately obvious
that the dependency lines are all over the place.
More so, the red nodes represent actual cycles in the dependency graph,
which is a sign of bad architecture and increased coupling.

thumb_up It is always possible to implement any given feature in a way that


will not introduce cycles into dependency graph!

Please take a deep breath before looking at the next dependency graph,
this is from a real project!

Few files made it to the cutout (maybe 5% of the overall dependency graph),
but the dependency graph edges are completely out of control!

19 Tomas Trajan
@tomastrajan
Theory
It takes little imagination to understand that extending such a project with
new features or adjusting of the existing one turns into month-long pain of
new regressions popping up at unrelated places. The main reason for that is
that everything depends on and knows about everything else!

Clean dependency graph


Some teams manage to keep the project dependency graph clean over a
long time. This is in general rare as it takes a lot of focus during code review,
but definitely possible. Let’s have a look at an example of a dependency graph
from such a project.

The cutout of the dependency graph has no cycles (red nodes), even a lazy-
loaded feature is clearly recognizable (bottom), there is a general sense of
“one-way-ness” from left to right.

20 Tomas Trajan
@tomastrajan
Theory
As we can imagine, maintaining and extending such a project will prove much
easier compared to the examples depicted above.

To make the point even stronger, the last dependency graph is from a
project called Omniboard - a SaaS product that helps you to analyze,
understand and manage codebases in large enterprise environments
and which is definitely of medium (to large) size developed by a single
developer (me).

As we can imagine, being able to ship features fast, even 4 years after the
project kickoff is of vital importance and the clean architecture allows me to
do just that!
Another example of a cutout of a desirable clean one way dependency graph
from a library from one of our customers can be found below.

Again the overall left to right direction, no cycles, clearly distinguishable


clusters are present.

21 Tomas Trajan
@tomastrajan
Theory
Hope-based architecture
As we discussed previously, it is 100% possible to keep the architecture clean
over the lifetime of the project, but it is at best unusual. Projects often start
with many good ideas and folder structure, which tries to set the correct
tone for the development.

info It is 100% possible to keep the architecture clean over the lifetime
of the project, but it's much easier with help from tooling!

And on rare occasions, the team’s overall skill level and attention to detail will
be enough to preserve it in the long term…
But without the support of dedicated automated tooling, we’re just hoping
that it stays that way and everyone involved will pay attention and review
perfectly 100% of the time, which is just highly unlikely.

That’s why I like to call it the “hope-based architecture”!

In this book we’re going to learn how to stop relying on hope that everybody
does a perfect job 100% of the time and take that burden away with the
eslint-plugin-boundaries which will act like a friendly senior reviewer who
will have our back each time we want to merge something into our
repository!

Now it’s time to have a glimpse of what we’re going to learn to prime our
curiosity!

22 Tomas Trajan
@tomastrajan
Theory
Angular Enterprise Architecture teaser

That’s it, we’re done, you’re ready to implement robust, maintainable and
extensible Angular applications for your organization!

But where would be the fun in that, so let’s make sure that everything from
this diagram makes perfect sense and falls naturally into its place. For that, we
have to embark on a journey, and the first stop is building a solid foundation
of base concepts!

23 Tomas Trajan
@tomastrajan
Theory
Base underlying reality of the browser,
the bundler and Angular
Now it’s time to explore and learn about some of the main technologies
which will have the largest impact on the way the proposed architecture
looks and the way it works.
In this way, I like to think about it as the “base reality” which we have to take
into consideration so that we can end up with a strong foundation on which
our project can stand with confidence!

The browser
Angular applications will run in the browsers of our users. When a user wants
to use our application, then the user and the browser have to perform a
couple of steps which can be summarized as:

1. A user enters the application URL in the URL bar (manual / bookmark / …)
2. The server responds with the index.html file which contains references
to the eager JavaScript bundle files
3. The browser triggers loading of these bundles

The loading of the bundle will take some time, depending on the
network conditions
The server should preferably serve compressed (gzip) bundle files that
reduce the number of bytes that have to be transferred over the
network

24 Tomas Trajan
@tomastrajan
Theory
The bundles usually contain hashes in their file names, which allows
browsers to cache them safely (without risk of using outdated bundles
when the new version of the app was released)
In general, network is rarely a limiting resource for most users, and later
caching of the bundle files removes the “network time cost” entirely
(until the next release of the application)

4. Once loaded, the JavaScript is parsed and executed, and the Angular
application is bootstrapped

The parsing and execution can take a considerable amount of time, even
on desktop computers today
The parsing and execution represent the main bottleneck and will be
affected the most by increased eager bundle size as a result of bad
architecture

5. The application then triggers loading of additional lazy bundles (e.g. for
the initial feature based on the exact url) or later based on user interaction
(e.g. navigation to another lazy feature using a nav item or
programmatically)

These bundles are subject to the same processes and constraints as


eager bundles. We also want them to be as small as possible to limit the
amount of network traffic, parsing and execution that browser has to
perform before displaying the new feature to the user

25 Tomas Trajan
@tomastrajan
Theory
The bundler and the bundles
Now that we established that the JavaScript bundles and their size play a
significant role in the overall application startup story and can make or break
the user experience, let’s explore how bundles come into existence.
Each Angular project consists of many Typescript files, and these files are
referencing each other with the help of imports and exports (JavaScript
module syntax).

Today the browsers already support JavaScript modules and would allow
for native loading of all the referenced files, but there is one huge
problem with this approach. The browser can’t know which files to load
before it loaded previous files which reference them.

Because of this, native loading of JavaScript modules would turn into a huge
waterfall of requests and would lead to a terrible startup performance, even
with use of something like HTTP2 or streaming!
Because of this reason, virtually all frontend frameworks use some kind of
tooling like webpack or esbuild to pre-bundle these files into larger
JavaScript bundle files to be loaded by the browser in a much faster manner.
With one extreme of the spectrum being using browser to load individual
JavaScript files with native module support, the other extreme would be a
huge JavaScript bundle file that will contain all the source code of our
application.
This would be also suboptimal because the large bundle will take a non-trivial
time to be transported over the network.

26 Tomas Trajan
@tomastrajan
Theory
But even more problematic, the browser will then need to parse and execute
all the JavaScript in that bundle file in one go! This can take a couple of
seconds even on the most powerful modern desktops and would lead to
prohibitively slow startups on virtually all mobile devices.

There must be a better way!

JavaScript import / export syntax supports dynamic imports which are


then picked up by the tooling as a natural split point to extract additional
JavaScript bundle files.
The webpack (or newer esbuild ) which is the main tooling behind the
Angular build pipeline recognizes these dynamic imports and does exactly
that.
In practice, there will be some configuration that will determine how exactly
the files will be bundled together. Anyhow, it will always start with them being
referenced by the dynamic import (and no standard / eager import) from the
files that are bundled in the original main bundle!
Now it becomes obvious that clean architecture with a one-way dependency
graph (the way the files reference or import each other) will have a significant
impact on the structure and size of the produced JavaScript bundles!

warning Both extreme approaches, loading files one by one (native


module loading) and bundling all files into one huge bundle are
suboptimal, and therefore the tooling helps us find balance with a
smaller number of reasonably sized bundle files!

27 Tomas Trajan
@tomastrajan
Theory
Angular
The last piece of our puzzle which sets the constraints on what can function
as a good architecture is Angular itself.
Angular comes with many well-defined concepts like routes, components,
directives, pipes or services which are very useful and provide us with a solid
frame when thinking about how to implement any particular user flow and
corresponding UI.
For example, it’s immediately clear that the actual UI has to be implemented
using the components because they have a template. On the other hand,
backend requests, data transformation or state handling fit better into
services because they do not have to deal with templates and allow us to
focus on the data.
These base building blocks, like components or services, …, then play by the
rules set of the two main underlying systems in Angular itself and can be
grouped into two main groups, the standalones (previously declarables) and
injectables.

Standalones (previously declarables)


Standalones (previously declarables) are building blocks which have
template (components) or can be used in the templates of components
(directives, pipes) and any arbitrary combination of these.

The previous “declarables” name comes from the older NgModules based
API where each declarable had to be an explicit part of the
declarations: [ ] array of exactly one NgModule .

28 Tomas Trajan
@tomastrajan
Theory
Nowadays, with the advent of standalone APIs, the declarables are no longer
declared in the parent NgModule but are “declared as standalone” with the
help of the standalone: true flag inside their @Component , @Directive or
@Pipe decorator.

info In this book we’re going to focus fully on the modern Angular
standalone approach!

Once we have created a couple of standalones and want to combine them,


meaning we want to use some other component, directive or the pipe in the
template of the parent component, we will have to import them and add
them to the imports: [ ] array of that parent component and in doing so
we’re building the “template context” of that component…

Template context
Template context is a very useful mental model when working with
standalones. It boils down to the need to "collect" all the standalones that are
used in the template of any given component and make them available in the
imports: [ ] array of the component inside its @Component decorator…

Previously with the modules, this has been a bit more complicated because of
one additional level of indirection, the NgModule itself. In that case, the
template context was created on the level of the NgModule .
This meant that everything which was declared (declarables) or imported
(standalones or other NgModules) by the given NgModule was part of the
same template context and could be used in the templates of the
components indiscriminately.
29 Tomas Trajan
@tomastrajan
Theory
The NgModule based approach had the following tradeoffs:

It required less boilerplate to create and manage template context of the


lazy-loaded features
Use of NgModules led to excessively large template contexts (e.g. one
template context for the whole application or a huge (and common)
SharedModule). This had a negative impact on the bundlers which were
unable to extract additional lazy bundles because of the eager references
of all the declarables from that module
The indirection of component template context managed by the parent
NgModule prevented using of better modern tooling which requires single
file compilation capabilities (impossible when component needs its parent
NgModule to know about what can be used in its template)

In this book we’re going to focus fully on using standalones with their explicit
management of the template context with the help of imports: [ ] arrays.
Then, every single template context dependency is equal to the explicit
import on the Typescript file level which is equal to a line in a dependency
graph.
This parity and lack of indirection is great because it makes it much easier to
understand and reason about the impact of each such import. It will also help
us make the right decisions in regard to what should be the location of any
given standalone component, directive or a pipe from the architectural point
of view!
Learn more about the template context from this article where I take a deep
dive into how it is constructed for NgModules vs standalone components,
what problems it solves and how it evolved in Angular over time.

30 Tomas Trajan
@tomastrajan
Theory
Injectables
The second main group is the injectables. They represent building blocks like
services, route guards or interceptors which can be injected into each other
with the help of inject() (or older constructor-based injection).
In this book we’re going to use inject() based injection exclusively
because constructor-based injection nowadays only works fully with a
workaround in the tsconfig.json , where we have to set the
"useDefineForClassFields": false .

This allows us to compose their functionality, which is very practical and


allows us to split implementation into well-defined and focused units of
code.

As the name suggests, the injectables are part of the Angular


dependency injection system.

The key distinctive feature of injectables is the absence of their own


templates and being unrelated to the concept of template and template
context in general!
Similar to the standalones, to inject one service from another, we first have to
import it on the Typescript file level which is again equal to a line in a
dependency graph.
As we can see, this explicit impact on the dependency graph will again have
to be considered in the context of the overall architecture. This will allow us
to determine what is the correct desirable location of implementation of any
given service.

31 Tomas Trajan
@tomastrajan
Theory
Compared to the standalones with fully explicit references and self-
management of the template contexts, the injectables still work with one
level of indirection which are the injectors and the underlying injector
hierarchy. The injector hierarchy then determines which instance we’re going
to get when we chose to inject one service in another.

info One of the main distinctions between the standalones and


injectables is that standalones self-manage their template
context directly while injectables are managed by the underlying
injector within injector hierarchy.

Injector hierarchy
In Angular, the injectables and the dependency injection system are
implemented with the concept of "injector hierarchy".
From the practical point of view, the injector hierarchy determines how many
instances of a given injectable (like service) will be created and which
instance will be injected into a specific consumer. For example, another
service or a component based on the closest injector within the hierarchy!
The hierarchy implies there will be multiple injectors organized in a
hierarchical tree structure that will map very well on top of the dependency
graph of our application and therefore also architecture.
Even though we’ve spoken little about it yet, in general there are two main
parts of each Angular application, eager and lazy loaded, and we’re going to
discuss it in depth very soon. For now, this simple distinction will be enough
to help us show some interesting properties of the injector hierarchy.
32 Tomas Trajan
@tomastrajan
Theory
Let’s zoom in on the Angular injectors in the eagerly loaded part of the
application. There are 3 main injectors here, two of them managed by Angular
behind the scenes. The third one, the root injector, is the one with which we
are going to interact directly by registering our injectables as providers!

All three injectors in the eager part of the application represent single eager
injector hierarchy, and the injectables registered in these injectors will be
application wide singletons or in other words, there will be only one instance
of any given injectable so whoever (including anything in lazy loaded
features) injects that injectable, it will get the same instance.

info Injectables that we register in the root injector will be application


wide singletons or in other words, there will be only one instance
shared by every consumer which injects that injectable!

Let’s explore what happens when we start adding lazy loaded features and
with their own injectors…

33 Tomas Trajan
@tomastrajan
Theory

Adding a lazy-loaded feature creates its own isolated lazy injector, called an
EnvironmentInjector , in the injector hierarchy.

This has a couple of very important consequences:

1. Lazy-loaded features can be fully isolated from each other and the eager
part of the application, this is one of the key benefits of the proposed
architecture, the injectables provided on the lazy loaded
EnvironmentInjector can’t be accessed from the sibling lazy loaded
features nor eager root injector!
2. Lazy-loaded features can access injectables which belong to eager
injector hierarchy which enables them to access injectables from the core
3. Providing the same injectable in multiple lazy-loaded injectors will result in
multiple instances of the given injectable (one per injector)
4. Providing the same injectable in both lazy and eager injector hierarchy will
result in multiple instances of the given injectable (one per injector)

34 Tomas Trajan
@tomastrajan
Theory
In practice, we’re going to provide our injectables in two ways:

1. Application wide singletons - in the eager injector hierarchy using


@Injectable({ providedIn: 'root' }) , in the core/ folder (could be
further structured into sub folders by domain)
2. Lazy feature scoped injectable - in the providers: [ ] array of the lazy
loaded feature routes config (more on that later)

Other injectors
There are other even more fine-grained injectors like ElementInjector
which is configured with the providers: [ ] array of a component or a
directive and allows us to create a unique instance of a given injectable for
each instance of component (or a directive) on which it was registered.
These solve other, more specific use cases and are therefore not as relevant
for the topic of overall Angular application architecture.

info A typical example of an injectable which will be provided on a


component is something like NgRx component store.

Function-based logic
Angular has recently added a new way of creating a couple of the useful
building blocks like route guards or HTTP interceptors without needing to
author a full-blown, class-based service with the @Injectable decorator.

35 Tomas Trajan
@tomastrajan
Theory
There are commonly called “function-based” guards (or interceptors). The
key benefit is that this approach makes it possible to author them in a more
light-weight fashion. For example, we could only author a function which
corresponds to a method of the original route guard class.
Such logic often needs to be composed with logic from other injectables like
services, for example function-based auth guard might need to inject
AuthService to retrieve auth state.

1 export function roleBasedAuthGuardFactory(role: Role): CanActivateFn {


2 return () => {
3 const authService: AuthService = inject(AuthService); // injection
4
5 if (authService.isAuthenticated() && authService.hasRole(role)) {
6 return true;
7 } else {
8 router.createUrlTree(['/unathorized']);
9 }
10 };
11 }

Angular makes such injection possible by running these function based


building blocks in the “injection context” which allows us to use inject() to
inject other injectables in these functions (or their factories).
We can and should treat these function-based building blocks in the exactly
same way as other injectables when considering overall architecture!

This means that we would implement them in the same location as we


would implement standard Angular service.

36 Tomas Trajan
@tomastrajan
Theory
Eager vs lazy part of the application
Previously, we have already mentioned lazy-loaded features and lazy bundles.
Now it’s time to bring these concepts into the spotlight and discuss how they
are implemented in Angular.

Eager part
Eager part of the application is the part that is loaded initially. This
corresponds in practice to the eager main.js bundle.

This might look slightly different when using esbuild which might
extract additional eager bundles (which are called chunks when
generated by the esbuild). But the most important thing stays the same,
all these eager bundles will be loaded in the beginning, at the Angular
application startup.

Then, everything that was imported using standard import / export


JavaScript syntax will end up in these eager bundles.

Lazy part
As for the lazy part, we have spoken a lot about the lazy-loaded features and
that their implementation will be located in the lazy bundles.
We also said that the bundler knows that it should extract a lazy bundle when
it encounters a dynamic import() statement.

37 Tomas Trajan
@tomastrajan
Theory
Routing based lazy loading
We’re going to use these dynamic imports explicitly in the Angular
application route configuration, most commonly as loadChildren: () =>
import('features/<feature-name>/<feature-name>.routes') in the
app.routes.ts .

info In code examples throughout this book, we’re going to use the
<some-placeholder> when we want to indicate that the
actual name of the feature or the placeholder will be different in
your application.

It would be similar for the lazy sub features which would be referenced with
the help of the loadChildren in the parent-feature.routes.ts file.

Besides loadChildren , it is now possible also to loadComponent which


lazy loads a standalone component, but we recommend against it
because using route config is much more flexible as it simplifies adding
other child sub routes which makes it extendable from the get-go!

To summarize, the lazy bundles are extracted by the bundler when it


encounters dynamic import() statements and in Angular we’re going to
explicitly use those in the route config of the application itself and
potentially also in the route configurations of lazy features (if they have lazy
sub features).
This nesting of the lazy loaded features can grow as much as we need it to,
and we’re going to speak about it more in depth in the later chapters…

38 Tomas Trajan
@tomastrajan
Theory
Defer based lazy loading
Angular 17 introduced @defer as a great new way to lazy load individual
standalone components directly from the template of the parent
components with very little effort which is required on our side! Learn more…
1 @defer {
2 <my-org-heavy />
3 }

The impact of the @defer lazy loading syntax on the overall architecture is
minimal because we’re already implementing all of our components as
standalone and therefore, “defer ready”.
In general, we should only use the defer-based lazy loading to lazy load
components with large impact on the bundle size of any given lazy feature
with typical examples like:

Rich text editor


Chart
Data table
Map

These are usually implemented and consumed from the 3rd party libraries,
but the same approach would be recommended also for the large local
components.

But if it’s so easy to use, then why shouldn’t we lazy load every single
component?

39 Tomas Trajan
@tomastrajan
Theory
The best way to think about this is looking at the two extremes of the
spectrum. Let’s imagine a lazy loaded feature (page) with 10 components
which are nested in each other, e.g. container -> list -> item -> form ->
rich-editor -> controls -> dropdown -> option -> …

1. Every component is lazy loaded — we will need to load the previous one
before we know what to load next, which would lead to a waterfall of
requests! This is definitely suboptimal and leads to longer time to load the
whole page and worse UX!
2. Every component is eagerly loaded (added as a standard dependency in
the imports: [] of parent Angular standalone component) — this works
great until one of those components becomes excessively "heavy" (e.g.
chart, editor, …) In that case it will become beneficial to split that one
heavy component away from the main lazy loaded feature bundle to
prevent delaying of the whole feature!

info The @defer is a great new way to lazy load heavy components
directly from the template of the parent components, and it's
best used when lazy loading components like charts, rich text
editors, data tables, or maps which could be local to the current
application or come form a 3rd party library.

40 Tomas Trajan
@tomastrajan
Key concepts
Previously, we have set the stage by discussing all the things that will have an
impact on the shape of our architecture like the browser, the bundle and the
systems within Angular itself.

Now it’s time to explore some of the key concepts which are shaped by
the underlying base reality and will have a large impact on the resulting
architecture!

Lazy loading
As we’ve learned, the Angular application consists of the eager and lazy part
of the application.
These parts are separated based on which kind of JavaScript import (static or
dynamic) has been used when consuming the logic from those files. And at
the same time, that in Angular, the lazy part is implemented with the help of
routing (and defer).
The bundler then materializes this difference into a separate set of eager and
lazy JavaScript bundle files.

But why do we bother with lazy loading at all?

Lazy loading benefits


Let’s discuss what are the benefits of using lazy loading in our Angular
applications for the users, us as the developers, the architecture itself and
the long-term health (maintainability and extensibility) of our project!

41 Tomas Trajan
@tomastrajan
Key concepts
Faster startup time for the users
Lazy loading of all features (and sub features) means that in the beginning
we’re only going to load the eager part (e.g. layout and some state logic
which needs to be there from the beginning) and the first lazy loaded feature
which the user is trying to access based on the current url.
This means that the user will need to download and execute a lot less
JavaScript bundle files compared to a situation with no lazy loading at all.
These savings will get more beneficial proportionally to the size of the
application. In an application with four roughly the same sized lazy features
and the eager part, user needs to download and execute ~40% of the overall
JavaScript to bootstrap the application and display the first feature.
In an application with nine lazy features, it becomes only ~20% and so on…
Downloading less JavaScript will always be faster than downloading one
large bundle which includes the whole application!
On the other hand, there is a counterargument that for many organizations,
the user base has access to great connection and sufficient bandwidth (e.g.
5G or even ethernet in the offices) and that the bundles are going to be
cached by the browser after the initial download negating the value and
hence questioning the investment into implementation of clean lazy loading
based architecture.
First, the bundles are only cached until there is a new release of the
application after which the users have to download new bundle files. This
combined with the common practice of continuous delivery practiced by
many organizations means that the bundle caching will provide only a limited
benefit in practice.

42 Tomas Trajan
@tomastrajan
Key concepts
But even if we assume that users have perfect connection and unlimited
bandwidth to fall back on, there is still a bigger issue that can only be
addressed by lazy loading.
Once the JavaScript bundle files are downloaded, they need to be parsed
and executed. This can take a non-trivial amount of time even on the most
performant desktop machines, and naturally, it gets even worse on mobile
devices.
This can be explicitly evaluated using the Chrome Dev Tools performance tab
with the “Start profiling and reload app” button. After that, we can search for
the “Evaluate Script” and “Evaluate Module” block in the flame chart.

The image above is taken from an application with clean architecture and
most of the code contained within the lazy-loaded features. Because of this,
the parsing and execution of JavaScript only takes a very short amount of
time.

43 Tomas Trajan
@tomastrajan
Key concepts
Faster developer feedback loop
Another big benefit of separating application logic into lazy loaded features
is the faster developer feedback loop.
The speed of the feedback loop can be measured as a time from saving a
change in the developer’s IDE to being able to see that change in action back
in the browser.
This feedback loop easily plays out thousands of times per day per developer.
It’s when they are working on a piece of code to fix a bug, add or extend a
feature, and as such, every saving translates into increased productivity and
less distraction.

I believe most people have an experience which would go something like


this, pull updates from the repository, run npm install (or even better
npm ci ), and how did I end up on YouTube, again?!

thumb_up Quick feedback loop is essential for preserving focus on a task!

The lazy loading of features and the corresponding separation of the eager
and lazy bundle files have a direct impact on the speed of this feedback loop.
The reason for that is that the dev process needs to rebuild and re-bundle
only the bundle file of the lazy feature that we’re currently working on!
In practice, it should be easy to imagine that rebuilding 87KB will be much
faster than rebuilding 3.2MB and it in fact is. With proper architecture and lazy
loading, working on a feature usually translates to a sub-second round trip (in
order of 100s of ms) while in an unstructured application each change can
easily climb to 3+ seconds per change!
44 Tomas Trajan
@tomastrajan
Key concepts
Isolation
Another huge benefit of properly implemented lazy loading of the features
and sub features is that it promotes (and to a degree enforces) isolation
between those parts of the code base.

We’re going to explore the benefits of isolation in its own chapter, here
we’re going to focus on how lazy loading promotes isolation.

As we have learned, Angular has an injector hierarchy which is a system which


allows us to inject the correct instance of a dependency like a service or
config object based on our current location within the hierarchy.
We have also discussed that each lazy-loaded feature gets its own injector
which can be used to register providers only for that lazy-loaded feature (and
all child lazy sub features) in the providers: [] array of the <feature-
name>.routes.ts instead of global singletons ( providedIn: 'root' ).

This creates a first explicit line of defense against incorrect imports within
our codebase, e.g. consuming service from Feature A in a sibling Feature
B .

Let’s illustrate this "defense" using the following scenario:

Feature A contains a Service A

The Service A is registered in the lazy injector of Feature A by being


explicitly added to its providers: [ ] array in the routes config (more on
that later)
The Service A is then imported and injected in a component in the
Feature B

45 Tomas Trajan
@tomastrajan
Key concepts
Such implementation would fail at runtime when we would try to navigate to
the Feature B with the NullInjectorError: No provider for Service A!
error which would give a strong hint that we’re doing something we were not
supposed to!
This would play out the same if we tried to inject Service A1 registered in
the lazy Sub Feature A1 in the parent Feature A …

SubFeature A1 contains a Service A1

The Service A1 is registered in the lazy injector of SubFeatureA1 by


being explicitly added to its providers: [ ] array in the routes config
The Service A1 is then imported and injected in a component in the
parent Feature A

The following diagram illustrates these scenarios, both sibling and parent-
child, and how they would play out in practice.

46 Tomas Trajan
@tomastrajan
Key concepts
As a side note, if the service had to be used in both the Feature A and the
Feature B , we would have to extract it one level up, in this case the core/ ,
and register it as providedIn: 'root' . This fulfills the actual requirement
while preserving clean architecture and a one-way dependency graph, and
we’re going to discuss it at length later.

thumb_up Besides this, we’re going to implement additional layer of


automated architecture validation rules in the later chapters, but
scoping of the providers to lazy features to increase isolation is a
great way to start even in existing codebases without the
automated architecture validation!

47 Tomas Trajan
@tomastrajan
Key concepts
Lazy loading heuristics
Now that we know about the benefits of lazy loading, and that the “unit of
lazy loading” is a lazy-loaded feature, let’s discuss some heuristics that can
help us determine what constitutes a lazy-loaded feature in our application!
Previously, we’ve learned that lazy loading of features usually happens on the
route level. This provides us with a hint that the most common collection of
use cases that can be extracted as a lazy loaded feature is a page!

As the features grow in complexity, it is not unusual there are some


conditional views within the feature itself, which are often implemented with
the nested routing config which belongs to that feature.
This is then accompanied by the second level navigation and will usually look
something like the following…

48 Tomas Trajan
@tomastrajan
Key concepts

The main advantage of using second level navigation instead of relying on of


some custom ad-hoc state and @if statements is that it makes it easy to
add a deep-linking capability (being able to send link to somebody to recover
the state of the application) which is usually a very desirable feature and
considered an UX best practice!
If that is not enough, Angular even allows us to have multiple sibling routes
active at the same time, but this is an advanced feature that is rarely used in
practice. Learn more about Angular named outlets.

thumb_up Implementing all sub-views with help of the nested routing and
navigation makes it easy to add deep-linking capability which is
considered an UX best practice especially valued by the users!

49 Tomas Trajan
@tomastrajan
Key concepts
Isolation
Previously, we have already touched on the topic of isolation as one of the
main benefits of lazy loading features in our applications, but this only
scratched the surface.

Soon we’re going to see that the establishment and enforcement of


isolation (as in clean separation between the parts) represents the
central focus of the proposed architecture as it’s the main idea at its
heart.

Isolation reduces coupling and stands in opposition to another well-known


approach in software engineering, the DRY, which stands for “Don’t repeat
yourself”

translate DRY - Don’t repeat yourself

The DRY is a principle aimed at reducing the code repetition in the codebase,
replacing duplicate code with abstractions. The idea is that each piece of
knowledge or logic should be represented in a single place in a system.
If the same information or logic is repeated in multiple places, any new
requirement that requires a change in how it should work will require us to
change the logic consistently in all these places. This can lead to errors and
inconsistencies…

50 Tomas Trajan
@tomastrajan
KeyAsconcepts
it turns out, in frontend applications, it’s 3 - 5x more valuable to have
more isolation than to focus on removing every instance of repetition
at the cost of increased coupling and introduction of additional
abstractions!

Let’s unpack what that means…


The argument we’re trying to make is that in frontend codebases it’s better to
have some amount of duplicated code (e.g. in multiple lazy features) because
that will allow for its independent evolution as the requirements change, and
they do change!

In frontend, it’s also much more common to encounter requirements


that are very ad-hoc, e.g. to add some custom rule or handling as part of
a specific flow for a tiny subset of users.

The isolation (which often goes hand in hand with slight code duplication) will
support these use cases in a much better way, and it will make their
implementation and maintenance much faster and less painful!

info In frontend, it’s much more common to encounter and need to


implement requirements very specific. Because of this, the
isolation and the flexibility it provides are much more valuable
than abstracting away every single instance of repetition!

Now if we contrast this with a codebase which abstracts away parts of this
flow into a shared abstraction which couples these features and these
business flows together…

51 Tomas Trajan
@tomastrajan
Key concepts
In such cases, we often end up in a situation where the abstraction becomes
increasingly complex, in an attempt to handle all these edge cases in one
place which over time leads to unmaintainable “god” components and
services. We have unfortunately seen this play out in real life codebases far
too often!

Does this mean there should never be any abstraction?

Not at all! That’s why we say that isolation is roughly 3 - 5 times more valuable
than DRY, so we should definitely abstract away reusable components and
services, especially when they are generic UI widgets like a button or a
calendar, but we should think twice (or five times) before abstracting away
actual flow specific business logic!

thumb_up Abstracting away repetitive logic is still a great thing to do to


improve quality of the codebase we should just keep in mind that
in frontend, isolation and hence independence is roughly 3 - 5
times more valuable which translates into waiting for at least 3
occurrences of repetition before going for abstraction. As always,
there is no silver bullet!

52 Tomas Trajan
@tomastrajan
Key concepts
Isolation guarantees
With isolation in place, we get a couple of great guarantees which will greatly
improve our ability to maintain and extend the project over time.

Local impact
Changing implementation of a lazy feature can’t have any impact on any other
feature, core logic or shared UI widgets.
This also means we need to understand a much smaller fraction of the
codebase to be able to make the change with confidence. This also means
that the new team members will be productive and produce much better pull
requests, way earlier, than in codebases with less isolation and more
coupling.

Please keep in mind that the changes in the core, ui or pattern parts of
the code base will still have a wide-ranging impact and therefore a
reasonable test coverage is still required!

Local testing
In the same way, we are guaranteed that changing implementation of a given
feature will not break tests of any other feature.

53 Tomas Trajan
@tomastrajan
Key concepts
Isolation improves extensibility and evolution of code base
Focusing on isolation and the resulting isolation guarantees improves the
long term extensibility of the code base. It should be easy to imagine that
making changes in an isolated lazy loaded feature will be less work intensive
than in a “feature” which is tightly coupled to the rest of the codebase.

Architecture promotes isolation


The architecture proposed in this book focuses and promotes isolation as the
key feature and goal to be achieved in the project codebase, soon we’re
going to learn about all the building block types and the rules about which
building blocks can consume other building blocks, which ends up in some
building blocks being fully isolated!

Feature as a black box


Another angle to look at isolation can be that …

1. if we’re dealing with multiple lazy-loaded features


2. and we know that they are fully isolated from each other
3. then we get a lot of flexibility about how we choose to solve specific use
cases within these features

54 Tomas Trajan
@tomastrajan
Key concepts

As the Angular and frontend landscape evolves over time, we might want
to update our best practices without necessarily wanting to invest time
in migrating all of our existing code to these new best practices.

The reason for that is that these old features work as expected and such
effort is not really providing any business value and could be spent better
elsewhere.
Properly isolated architecture allows us to keep these older features as they
are while introducing new best practices in the new features that need to be
added at this point of time!
This also means that if some of the features are not completely up to the
desired quality standards, the impact of that “dirtiness” is again isolated and
therefore limited only to that feature!

info Isolated features can be threaded as a black box, so even if the


quality of their internal implementation is not as high as we
would like, the impact of that “dirtiness” is limited only to that
feature!

55 Tomas Trajan
@tomastrajan
Key concepts
One way dependency graph
The one-way dependency graph is another key concept of the proposed
architecture.
As we have seen in the “Why does clean architecture matter?” chapter, the
madge-based analysis of the real life projects often uncovers very chaotic
dependency graphs. Such graphs often contain many cycles that make things
even worse as they become harder (or prohibitively expensive) to untangle
over time.
Following is a diagram of a very simple dependency graph which illustrates
the general “one way ness” the green arrows and what it would mean to add a
cycle (red arrow).

In such a graph, if we imagined the right most boxes are lazy features, the
direction of arrows means that it would be possible to remove each of the
features without affecting the rest of the application.

The reason for that is that nothing depends on them!

56 Tomas Trajan
@tomastrajan
Implementation hints
This book focuses on Angular architecture and does not prescribe one
specific way on how to implement actual features or manage state in our
applications.
This is not necessarily a bad thing as there are many different ways to manage
that and different teams will choose specific approaches and state
management libraries based on their needs and preferences.
It also helps to keep the content focused to solve one specific problem, the
architecture without unnecessary distractions. This is also underlined by the
fact that architecture can in fact be implemented independently of actual
business features and flows.

Nevertheless, we’re going to explore two topics that are more


implementation-centric but are related to the concepts in the proposed
architecture and therefore useful.

State vs view
(headless business logic vs view logic)
From experience, it’s usually a great idea to split logic into two main parts:

State handling / headless business logic


View logic / component logic

This also corresponds to the two underlying systems we learned about


previously, the template context and the injector hierarchy.

57 Tomas Trajan
@tomastrajan
Implementation hints
It is realistic and desirable for most components to be basically “logic free”
where the component would only get access to some state (e.g. from service
or NgRx selector ) and displays it in the template.
At the same time, any user interactions that need to be handled by the
component method will be immediately delegated to the service (or
dispatch an NgRx action).
1 @Component({
2 standalone: true,
3 selector: 'my-org-example',
4 })
5 export class ExampleComponent {
6
7 view = this.store.selectSignal(selectExampleView);
8
9 handleItemStatusChange(item: Item, status: Status) {
10 this.store.dispatch(updateItemStatus({ item, status }));
11 }
12
13 handleItemDelete(item: Item) {
14 this.store.dispatch(deleteItem({ item }));
15 }
16
17 // ...
18 }

thumb_up It is realistic and desirable for most components to be basically


“logic-free”. Such component only gets access to some state
(from service or NgRx selector) and delegate user interactions to
the service (or dispatch an NgRx action).

The exceptions to this recommendation are components that deal with


forms and things like dialogs. These can be then encapsulated into their own
specific “type” of a component, e.g. editor or confirm dialog, and those
should be the only place with actual view logic…
58 Tomas Trajan
@tomastrajan
Implementation hints
Correct level of abstraction
Another implementation hint which we would like to recommend based on
our long term experience from large enterprise environments is to resist the
temptation to introduce “cute little abstractions” into our codebases!

Framework vs application business logic


In general, ~95% of our focus and efforts should be spent on developing
application business logic! Or in other words, logic which delivers value to
the users in terms of user flows, pages, forms, because we already do have a
framework, the Angular itself! The same would apply for the state
management library of choice like NgRx, NgXs or RxAngular…

This is in stark contrast to trying to build something like


“MyOrgAngular+” with a collection of base components, tiny wrappers
that pre-package little pieces of logic or combine separate concepts,
like selectors and actions, in unexpected ways…

While always negative, the downsides are limited in smaller organizations


with just a couple of projects, but grow proportionally larger with every
additional project as these abstractions tend to diverge over time per code
base!

Short term vs long term thinking


Little abstraction may feel like a good idea, especially in the short term. We’re
saving us some effort (usually very little) by abstracting away a couple of
repetitive lines of code.

59 Tomas Trajan
@tomastrajan
Implementation hints
What is rarely acknowledged is that the price for doing so is that we’re
introducing coupling between the parts of the codebase that could have
stayed fully independent and hence isolated!

In the long term, isolation between the unrelated parts of the codebase
provides project with much-needed flexibility to be able to evolve and
meet the ever-changing requirements which are so common in the
frontend!

Optimizing for the wrong thing


At its root, the desire to add little abstractions stems mainly from the
overvaluation of DRY in comparison to isolation.
This is then commonly experienced as a desire to reduce the number of lines
of code (LOC) because that is perceived as the ultimate sign of good
engineering, and therefore it becomes a goal in itself. But a little bit of
repetition is a tiny price to pay for preservation of independence!
As always, there is no one hard rule that could help us determine what is the
right thing to do in every scenario, that’s why the heuristic of placing 3 - 5
times more value on isolation means that we should start considering
abstracting something away when we had at least 3 (or preferably even
more) occurrences in our codebase.

thumb_up Waiting for at least 3 (or more) repetitions helps us see if the
abstraction is a good fit for a wider range of the use cases!

60 Tomas Trajan
@tomastrajan
Implementation hints
On the other hand, we should always refrain from abstracting away Angular
(or state management library) based code as that would introduce another
set of major downsides as we’re going to discuss in the next section…

warning We should always stay away from abstracting away Angular (or
state management library) based code. Trying to build our own
Angular+ will predictably lead to problems down the road!

Common negative consequences of adding abstractions


In our experience from a large enterprise environment with 180+ Angular
SPAs, 30+ libs and across a considerable amount of time spanning
approximately 7 years (Angular 5 to Angular 18) we encountered a wide
range of negative consequences of adding custom little abstractions.

The abstractions themselves can be grouped into two broad categories,


business logic related and the framework related (e.g. Angular, state
management library, testing setup, …)

Business logic related


Framework in a framework — parts of the business logic becomes so
abstract and complex that it feels like using a homegrown framework

61 Tomas Trajan
@tomastrajan
Implementation hints
Framework related
Base components, forms, directives, … - Angular then predictably evolves
in another direction than envisioned, which is not compatible with the
assumptions made in the custom logic, which leads to often very large
amount of effort to undo and rework this as the custom abstraction
“poisoned” the rest of the actual business logic
Custom abstraction can often lead to a situation which breaks automatic
migrations provided by the in form of ng update, a great example could be
something like *myOrgFor / *customIf and the Angular 17 migration to
new control flow
Custom testing and mocking-related abstractions (often also from 3rd
party libs) can lead to broken tests as we have seen with the advent of
standalone APIs
Various random bugs over time

Correct level of technical abstraction


Now we should possess a good understanding of the issues caused by the
introduction of unnecessary little abstractions, but what is then the correct
level of abstraction?

Angular
State management library of choice

That’s it, staying on the level of Angular and a chosen state management
library represents the most responsible long-term choice for the project and
the organization.

62 Tomas Trajan
@tomastrajan
Implementation hints
How to handle “verbosity”
Not abstracting away everything will leave us at the default level of verbosity
of Angular and the chosen state management library. This is not necessarily a
bad thing as such verbosity tends to get better over time as Angular and the
selected state management library evolve together with the ecosystem
embracing new better patterns and practices.
For example, Angular 16 brought takeUntilDestroyed which is much more
concise than the older takeUntil(this.destroy$) pattern which required
the creation of additional Subject and use of the ngOnDestroy lifecycle
hook.
Another example is a steady decrease of verbosity in NgRx with every major
version bringing some improvements in that department. For example
creator APIs, action groups, functional effects or createFeature APIs.

As we have seen from real life experience, verbosity will decrease over
time “by itself” and the best thing about it is that such abstractions are
built upon feedback of tens of thousands of developers and will
therefore be much more likely of better quality than a home-grown
abstraction by a single team of developers.

Now, there will always be some verbosity to deal with, especially if we


prioritize isolation over DRY and abstractions, so what are some of the tools
that can help us deal with this tradeoff?

63 Tomas Trajan
@tomastrajan
Implementation hints
Angular (NgRx, …) Schematics - generate building blocks like components,
services or whole state features in a consistent way following the latest
best practices together with a testing setup
Consistency in naming allows for using basic “search and replace”
functionality of our IDE, which can get us pretty far, especially when
employing a bit of regex “magic”
The ng-morph - an amazing and a bit advanced library that allows us to
implement code transformation scripts with ease, can be really worth it in
environments with lots of projects

Summary
Abstraction can be a useful and powerful tool when used correctly, in
practice we have seen it being overused and doing more harm than good,
hence the recommendation to prioritize isolation.
Every team and project is different and an abstraction, when used correctly
can also be a tool that increases the team delivery velocity. Just keep in mind
that such abstraction is more likely related to repetitive business flow than to
saving a couple of LOC in Angular or state management library boilerplate!

Your experience
Do you have an experience which would support or contradict the
recommendation to prioritize isolation over DRY?
Let me know @tomastrajan or drop me an email at [email protected].

64 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Now that we have built all the necessary theoretical understanding of
concepts, underlying realities, building blocks and approaches that will have
an impact on the proposed architecture, it’s time to start exploring the
architecture itself!
The first stop on this journey will be to go through all the building block
“types” from the perspective of the architecture and learn more about…

What types are available?


What are the relationships between the types?
What is typically implemented inside any given type?

Afterward, we’re going to implement it hands-on, step by step in an example


project together with the eslint rules for automated validation of the
architecture which will guarantee that it stays clean over the whole project
lifetime!
But first, let’s have a look at the big picture!

65 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
The big picture
The following diagram captures the essence of the proposed architecture.
Please make sure to pay extra attention to the eager / lazy boundary and
notice that all the building block types are present in this one single diagram
including core , layout , ui , pattern and feature !

66 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Types overview
Now it’s time to dive deeper into each of the types individually before we
bring them together into a real life application example!
The types also neatly fit into their own folders like core/ or feature/
which will make working with them a very intuitive experience once the
workspace was set up initially.

info Every type will have a quick summary in the beginning which will
make it easier to revisit and refresh the information later on.

The formatting of the following pages will be also optimized to be easy


to print out and use as a reference when working on a project. Because
of this, there might be a bit more white space at the end of some pages
compared to the book’s baseline.

67 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Core eager

The first type we’re going to discuss is the core which will live in the core/
folder of our application.

Implementation in core/ folder


Eager, available from start, part of the initial bundles size
Only injector based (headless) building blocks like services, interceptors,
guards, functions…
Application configuration and setup in the provideCore()

Content can (and should) be sub-structured based by domains (or


features), e.g. core/user/ , core/auth/ or core/product/
Core logic is globally accessible and can be accessed by any layout ,
feature and pattern

Core is the place to extract logic, if it needs to be used by more than one
feature , pattern or layout

The core is the right place for all logic that needs to be available from the
start like authentication state, current user or guards… Or in other words,
anything that is necessary to be able to display information in the layout
before loading of a specific lazy feature.
The second most common content of the core will be domain (or feature)
specific injector based (headless) logic that is shared by more than one lazy-
loaded feature.

68 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Standalone core
With the advent of standalone APIs, the core also transformed from
previously common CoreModule to provideCore() . This is the best location
for us to provide, define and setup, everything that should be available from
the start.
As such, provideCore() should be the sole location of this setup and the
initially generated app.config.ts should only contain the call to the
provideCore() itself. That way, there is no ambiguity or possibility to miss
something as everything is available in this one single place!

So what does a typical provideCore() look like?

1 export interface CoreOptions {


2 routes: Routes;
3 // other options and parameters...
4 }
5
6 export function provideCore({ routes }: CoreOptions) {
7 return [
8 provideAnimations(),
9 provideRouter(
10 routes,
11 withComponentInputBinding(),
12 // other router features...
13 ),
14 // other Angular based, 3rd party or local providers and state...
15
16 // perform initialization, has to be last
17 {
18 provide: ENVIRONMENT_INITIALIZER,
19 multi: true,
20 useValue() {
21 // add init logic here...
22 },
23 },
24 ];
25 }

69 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Let’s discuss what is the typical content of the provideCore() that can be
found in the most Angular application. Remember that all these examples
always represent headless (injector) based logic, so no components,
directives or pipes as those will have their dedicated place elsewhere.

Angular (and state management) specific providers and setup


The provideCore() is the right place to set up all global Angular providers
like provideAnimations() , provideRouter() , provideHttpClient() (with
interceptors) or providers from state management libraries like NgRx
provideStore() .

All these “provideX” APIs usually support passing of additional configuration


to further parametrize or enhance provided functionality.

Infrastructure specific providers


Besides providers from Angular and the state management library of choice,
provideCore() is also a place to register providers from 3rd party libraries
which often handle “infrastructure” use cases like logging, translations,
analytics and other…

Initialization logic and kick-starting of processes


The provideCore() is also the place where we’re going to kickstart all initial
and long-running processes that need to take place at the application
startup.

70 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
This will be performed with the help of the special
ENVIRONMENT_INITIALIZER provider which allows us to run any logic inside
its useValue() function. For example, when using NgRx, this could be a
place to dispatch an AppInit event which can then kickstart other
processing in the NgRx effects like resolving of the authentication token or
loading of the user.
We should always make sure that this logic is implemented as last within the
provideCore() otherwise we might get an error because we might be
injecting some providers which weren’t provided yet.

App-specific core logic needed from start


This represents app-specific business logic in the form of services, guards,
state management specific logic and more.
This logic is then something that we need from the start. A great example to
illustrate this point is logic to manage authentication state such as
authentication token and the user entity. This state needs to be available
from the start because it could be used to:

Perform other backend requests - auth token will be used by interceptors


to set it as HTTP header to authenticate performed backend requests
Determine if a user has access to a specific feature - user entity can
contain roles or other mechanism that can be used in guards to determine
if user can navigate to a specific lazy feature (frontend / UX only, real
security is the sole responsibility of backend which must determine which
data can be provided as frontend can always be “hacked” using dev tools)
Display user info - user info such as name or avatar as a part of layout
before loading of a specific lazy feature
71 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Following this example, any other logic that manages state or performs some
processing that is necessary at the application startup will be implemented in
the core.

App-specific core logic shared by more than one lazy feature


Besides logic that needs to run from start, the core is also a place to
implement anything that needs to be used by more than one lazy feature!
Let’s imagine the following scenario. Our application has an order lazy loaded
feature which displays and manages a list of orders. Initially the logic to
perform CRUD on order related entities was stored in the order lazy feature,
for example feature/order/state/ .
Now there is a new requirement to build a new dashboard feature which
(besides other things) will also need to have access to the order state
because we would like to display some order related stats, and there is no
dedicated API endpoint for it, the dashboard has to work with exactly the
same API endpoints and data as previously built order feature.

We have already learned that isolation between lazy features is one of


the key properties of this architecture, which means we’re not able to
just import services from order feature in the new dashboard feature.

So how can we solve this problem?


The solution is to extract the logic which manages the order state into the
core, specifically the core/order/ folder because every lazy feature can
consume things exposed by the core as that fulfills the one-way dependency
graph constraint.

72 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Grouping core logic in folders per domain
In general, it is a good idea to group this logic in subfolders by domain, that
way, the previously discussed authentication state could be located in
core/auth/ or core/user/ or even both if that level of granularity makes
sense for that use case.
Similar to that, logic shared by more than one lazy feature, for example, NgRx
based order state slices which manages CRUD for the order entity, would be
stored in the core/order/ folder.

thumb_up When grouping logic in subfolders, always use domain-based


grouping, e.g. core/auth/ or core/user/ instead of
grouping by building block type like core/services/ or
core/reducers/ !

This grouping approach is much better than an alternative approach of


grouping by building block type like core/services/ or core/reducers/ !
In practice, there may be exceptions to this rule, for example, we might want
to group all our interceptors in core/interceptor/ folder or generic utils in
core/utils/ but we should strive to come up with domain-based folder
structure in as many cases as possible!

Other utils
The core is also the place to implement other simple function-based utils.
Typical examples could be functions to parse and transform format for things
like dates, query params, or anything else…

73 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Providers from NgModules
Even though we’re fully focused on the modern standalone approach and
APIs, we might still need to use some organization specific or 3rd party library
which only exports NgModules.
The provideCore() allows us to specify providers: [ ] , but to consume
NgModule we would need to be able to specify import: [ ] array which is
not supported.
The way to solve this problem is to use the importProvidersFrom() and pass
in the desired NgModule .
This helper function will then get access to all the providers of that module
while ignoring all the declarables honoring the distinction between two main
Angular systems, template context and injector hierarchy!
1 export function provideCore(options: CoreOptions) {
2 return [
3 // ...
4 importProvidersFrom(SomeChartsModule), // 3rd party library
5 importProvidersFrom(MyOrgAuthModule), // organization specific library
6 // ...
7 ];
8 }

Learn more about how to migrate existing NgModules into standalone APIs
from this article.

74 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Layout eager

With our core ready, our application can bootstrap and even kickstart some
processes, but it won’t display anything to the user just yet.
Typically, most applications will use one or more layouts as a “frame” around
the slot in which we’re going to display specific lazy loaded features based on
user navigation or interaction.

Previously, we usually implemented layouts as a part of the CoreModule


as NgModules managed both template contexts and providers of the
injector hierarchy. Now with the standalone APIs and clear separation
between these two systems, the layouts are extracted into their own
type and layout/ folder.

Implementation in layout/ folder


Eager, available from start, part of the initial bundles size
Only template context based (standalones) building blocks like
components, directives, and pipes
Can and most likely will consume services and state from core/
Auth state to filter which menu items should be displayed
User state to display user avatar
Can and will most likely consumer some standalones from ui/ like
Avatar , Popover , Dialog , Button or Menu

In case of single main layout which is used for the whole app it will be used
directly in the template of the AppComponent

75 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
In the case of multiple layouts, it will be used as part of the route config
Applications can have more than one layout, e.g. login, signup,
authenticated
Some application may need a custom layout per feature and in that case
the layout/ folder may end up empty, the AppComponent will then end
up with a template consisting only of single <router-outlet /> and each
feature will implement its own layout within its own feature folder

Single main layout used by the whole app


This case describes an application which can use a single main layout for all its
lazy loaded features including the loading (and or signup) pages.
Such a layout will then be used directly in the template of the AppComponent
and such layout will contain at list one <router-outlet /> as a part of its
own template as a target slot to display lazy loaded features.
1 @Component({
2 standalone: true,
3 selector: 'my-org-app',
4 template: `<my-org-main-layout />`,
5 })
6 export class AppComponent {}

76 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Multiple layouts (e.g. login and main layouts)
Another common scenario is when an application has a relatively low number
of layouts, e.g. one for authentication purposes and then another once to
display every other lazy feature once a user has logged in.
This is best solved as a part of the application routes config in the
app.routes.ts and the AppComponent will contain just a lone <router-
outlet /> in its template.

The gist of the solution is to have two main configs with two main contexts,
e.g. '' and '/app' which will use respective layout as its component and
then define lazy loaded features as their children.
1 export const routes: Routes = [
2 {
3 path: '',
4 component: AuthLayoutComponent, // <--- login, signup, ... layout
5 children: [
6 {
7 path: 'login',
8 loadChildren: () => import('./feature/login/login.routes')
9 }
10 // signup feature, password recovery feature ...
11 ],
12 },
13
14 {
15 path: 'app',
16 component: MainLayoutComponent, // <--- main layout
17 children: [
18 {
19 path: 'orders',
20 loadChildren: () => import('./feature/orders/orders.routes')
21 }
22 // dashboard, profile, settings feature ...
23 ],
24 }
25 ]

The layouts themselves will then be implemented in the layout/ folder.

77 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Custom layout per feature
Another option is that the features are so vastly different that they need to be
able to define custom layouts, one per each feature.
In that case, the AppComponent will again contain just the
<router-outlet /> , the layout/ folder will be empty and each feature will
define its own layout as a part of the implementation of that feature, for
example feature/dashboard/layout/ .
In such a case, the app level routes config will be very simple and will only
define lazy loaded features.
1 export const routes: Routes = [
2 {
3 path: 'orders',
4 loadChildren: () => import('./feature/orders/orders.routes')
5 },
6 // dashboard, profile, settings feature ...
7 ]

Each feature will then define its own layout as a part of its own routes’ config.
1 export const routes: Routes = [
2 {
3 path: '',
4 component: DashboardLayoutComponent, // <--- custom (per feature) layout
5 children: [
6 {
7 path: '',
8 component: DashboardComponent,
9 },
10 // other dashboard related routes...
11 // other dashboard related sub lazy features...
12 ],
13 },
14 ];

Of course, it’s also possible to express our layouts as a combination of all the
approaches mentioned above.

78 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
UI eager lazy

Now it’s time to talk about the UI type which is going to live in the ui/
folder. The improved spiritual successor of the SharedModule , this type is
special in that it’s basically the only type that will in practice be consumed by
both the eager and lazy parts of our application.

This eager / lazy versatility is enabled by the new amazing standalone


approach which allows us to cherry-pick only the standalones that we
really need. This stands in contrast to receiving all of them at the same
time which was the main reason for previous best practice to never
import the SharedModule in eager CoreModule or AppModule .

Implementation in ui/ folder


Only template context based (standalones) building blocks like
components, directives, and pipes
Eager / lazy - each individual standalone will be imported and used
explicitly in each feature, pattern or layout that needs it, the bundler is
then able to determine the optimal bundle into which such a standalone
will be bundled
Only generic reusable UI components, directives and pipes which
communicate only through inputs and outputs
Never bound to a specific state through service or state management
library (as it can’t import from the core anyway)

79 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Standalones only
The UI as a type and therefore ui/ as the folder should only ever contain
standalones (components, directives and pipes) or in other words, only the
building blocks which are related to the template context system of Angular.

To refresh what we have learned previously, the main reason why we


want to separate them into their own type is that they play by a different
set of rules. This stands in contrast to the injectables and injector
hierarchy.

Specifically, it’s the fact that every single standalone component needs to
build its own template context. This allows us to cherry-pick only what is
used and therefore also get the most optimized bundles, which also means
the smallest possible bundle size.
This is also why we can use these generic UI components, directives and
pipes also within the eagerly loaded part of the application, namely the
layouts without any detriment!
The consequence of this approach is that the ui/ should never contain
implementation of any services or other headless logic and at the same time
these components should never really import it from the core either.
As an example, if we ever felt that a component from UI needs to import and
inject some service, then this should most likely happen in the parent
component instead (e.g. in layout or feature or pattern). Such component
can then retrieve such a state from the service and pass it down to the UI
component using the input binding.

80 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Generic reusable UI components
The simplest way to convey what is meant by the generic UI component is to
imagine something that is commonly provided by the 3rd party component
libraries like Angular Material, PrimeNg or TaigaUI. Components like card ,
button or menu and directives like drag , drop or focusTrap .

Most applications will use some kind of component library, be it a 3rd party
open source or internal library of the organization, the chances are high that
the application will not need to implement the whole base suite of UI
components from scratch.
On the other hand, it’s also pretty typical that the application will need to
implement a couple of additional generic UI components which are re-used
across multiple lazy features.

For example, in an application that visualizes information about many


code repositories, such a generic component could be responsible for a
stylized way to display file paths and code blocks.

The key feature of the generic UI components is that they never consume any
data directly from injected services or from a state management mechanism
like selectors. Instead, they will delegate this responsibility to the parent
component and receive data through inputs and notify parent about the
change through the outputs.
This constraint makes them universally reusable in every feature, pattern or
layout of the application without any need to introduce various conditions,
workarounds, and therefore coupling.

81 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Eager / lazy bundling implications
The standalone nature of the UI allows us to consume it in both eager and lazy
parts of the application without negative consequences for the initial bundle
size. The reason for that is that each standalone is consumed explicitly in a
“cherry-picking fashion”.
But what are the actual implications and how will this play out with bundler in
the real life application?
Let’s illustrate this using the following couple of examples which will run us
through various scenarios of how an example generic UI component, the
<avatar /> (which displays user avatar), could be used in an application.

1. Eager only - Component is only used in main layout to display the avatar of
the currently logged-in user and nowhere else. Therefore, it will be
bundled in the eager main.js bundle. In such a case, it would also make
sense to consider moving its implementation from ui/ to layout/
2. Eager and lazy - Component is used in main layout to display the avatar of
the currently logged-in user and also in features like tasks, orders, and
projects where it displays user avatar for the user which is responsible for
handling the respective task, order or project. The avatar is therefore used
in both eager part (layout) and multiple lazy features (task, order, project)
and will be bundled in the eager main.js bundle.

thumb_up We should always make sure that the location of the


implementation of any give standalone is as precise as possible.
That's why if the component is only used in the layout (1st case)
the implementation should also be layout/ folder.

82 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
3. Multiple lazy - Component is used in multiple features like tasks, orders,
and projects where it displays user avatar for the user which is responsible
for handling the respective task, order or project. The component will be
extracted into a “virtual pre-bundle” by the bundler and will be then
loaded together with any of the lazy loaded features in which it was used
when the user navigates to any one of them. Navigating to another lazy
feature which uses it after it was already loaded will simply use it as it’s
already available.
4. Single lazy - Component is used in single lazy features like tasks where it
displays user avatar for the user which is responsible for handling the
respective task. In this case, the bundler will bundle this component in the
bundle of that lazy-loaded feature. In this case, it would also make sense to
consider moving its implementation from ui/ to feature/task/avatar/
and only extract it back to ui/ once there is a need to reuse it another
lazy feature, pattern or layout!

The behaviors and resulting bundling described above will depend and may
differ based on the actual
internal configuration of Angular CLI for webpack and esbuild, but in general,
the principle holds true. Even though we spoke about components, the same
behavior and rules apply to other standalones, the directives and pipes!
Each of the ui standalones will be consumed explicitly in each feature,
pattern or layout, specifically in their components and templates…
1 @Component({
2 selector: 'my-org-header',
3 imports: [AvatarComponent], // ui standalone added to the template context
4 template: `
5 <img class="mx-auto" src="..." alt="MyOrgLogo" />
6 <my-org-avatar [user]=user /> <!-- ui standalone used in the template -->
7 <!-- ... -->`,
8 })
9 export class HeaderComponent {}

83 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
How to handle types and interfaces
Let’s start with a description of one of the common challenges that will arise
in most of the projects as a result of the following set of constraints:

1. UI standalones receive all the data through inputs and notify parent about
changes through outputs
2. These inputs and outputs need corresponding types and interfaces to
make them type safe
3. Data with its corresponding interfaces and types will be managed and
provided by services or state management solution which are
implemented in core or in the respective feature
4. UI standalones can’t have direct dependency on (or import from) the core
or any lazy feature
For example, the UI standalone <avatar [user]="user" /> displays
user image, name and role. These properties are also available in the
User interface which is used to type user entity which is managed by
the UserService in the core. What would be the best way to handle this
situation to fulfill all the constraints?

There are a couple of ways to handle this situation, each with its own
individual set of tradeoffs. Let’s go through them one by one…

84 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Rework ui standalone into a pattern
The first option is to implement the avatar as a pattern, which is a more
advanced architectural type discussed in later chapters. The pattern could
then import the User interface as it is allowed to import form the core. The
downside of this approach is that the avatar would then be able to import any
other service or logic from the core, and especially, the avatar is not really a
pattern but a generic UI component.

With such a solution, we would be abusing the architectural type system


to work around constraints which we added in the first place…

Define a new architecture type (eg model) just for types and interfaces
With this approach, we would define a new type, eg model which would then
contain source files which implement all the types and interfaces. The newly
defined model type would have a very permissive set of architectural
constraints which would allow all other types to import from it without any
restrictions. Such a solution can be successfully employed in many projects.

It especially fits projects which use monorepo approach where frontend


and backend are in the same repository and share the same types and
interfaces. Such projects usually already have a dedicated concept (eg
library) for sharing types and interfaces between frontend and backend!

Define interfaces and types in the ui standalone itself


This is the preferred solution to the presented challenge as it keeps the types
and interfaces close to the place where they are used and comes with a
couple of great advantages:
85 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
UI standalone is self-contained as it describes what data it needs locally
The types and interfaces describe only the properties which are actually
used by the UI standalone instead of the more generic User entity
The types and interfaces can be more descriptive and specific to the use
case of the UI standalone, eg Avatar instead of the more generic User
As the application evolves, the Avatar maintains its own identity and
independence from the User entity which can change over time

The downside is that now we have to maintain similar types and interfaces in
multiple places, and possibly introduce some kind of mapping between the
User entity and the Avatar entity in the future as they diverge.

A very nice variation of this approach is to introduce multiple more


granular inputs like imageUrl , name and role which will be described
by a primitive type like string or number . That way the need to use
(and therefore import) the User interface is removed altogether!

Such an approach can work very well in simpler cases with a rather small
number of properties. At the same time, the issue of diverging User
interface can be solved by a simple mapping directly in the template of the
consumer component which is using the <avatar /> component.
1 <!-- original -->
2 <my-org-ui-avatar [imageUrl]="user.profileImageUrl" />
3
4 <!-- updated, can be easily adjusted when the User entity changes -->
5 <my-org-ui-avatar [imageUrl]="user.resource.imageUrl" />

86 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
How to handle complex pipes
Another common challenge is related to implementation of pipes in the UI
architecture type and happens in situations which can be described as
follows:

The pipe contains complex logic


Some components and more importantly, some services need to use this
logic as well as a part of their implementation
We never want to register Angular pipe as a provider in the injector
hierarchy as that would mix up separation between template context and
injector hierarchy

thumb_up It's always a good practice to separate pipe into a thin "wrapper-
like" pipe, which will be used in the template, and a service which
will contain the actual logic. That way, the logic will be available
to both components (can be used in the template) and services
(can be used in the business logic).

This would turn our pipe into a pattern which is a more advanced architectural
type discussed in later chapters as patterns can import from the core and
therefore can contain services and other logic.
This is not an issue and more complicated pipes can in fact be implemented
as patterns!

87 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture

But what if we wanted to use such a pipe in another ui component as


well? The pipe, now pattern, can’t be imported from the ui component as
that’s forbidden by the architecture!

The best way to solve this issue is to remove the need to use the pipe inside
the UI component altogether. This can be achieved by using the pipe in the
parent component instead (eg layout, feature or pattern) and pass the result
down to the UI component…
This usually comes in one of two forms:

1. options driven APIs — the ui component receives the data and options
through inputs, we’re using the pipe in the parent component to transform
the data before it’s passed down to the ui component
1 <my-org-ui-price [price]="product.price | myOrgPatternPricePipe" />

2. (better) template driven APIs — the ui component receives the data


through the content projection, and the pipe is used in the parent
component to transform the data before it’s passed down to the ui
component
1 <my-org-ui-dropdown />
2 @for (let price of prices) {
3 <my-org-ui-dropdown-item [value]="price.id">
4 {{ price.value | myOrgPatternPricePipe }}
5 </my-org-ui-dropdown-item>
6 }
7 </my-org-ui-dropdown>

In both cases, the need to use (and import) the pipe in the UI component is
removed. The pipe is implemented as a pattern and the clean architecture is
preserved!

88 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Feature lazy

With all the reusable types and building blocks in place, it’s time to start
implementing actual use cases and business flows that will be used directly
by our users!

Implementation in feature/<feature-name>/ folder


Complete isolation between features, features can’t import from one
another
Always lazy loaded with the help of loadChildren() (pointing to feature
route config) instead of loadComponent() as that ensures uniform API and
universal extendability
The smallest possible app is an app with single / first lazy-loaded feature,
this again provides consistency and universal extendability, easy to add
additional features, and they all work in the same way
Black box — can contain any kind of implementation and building blocks,
Even if it gets dirty, the isolation prevents the spread, and it won’t affect
the rest of the application
Throw-away nature, because of isolation, it’s easy to throw it away and
start over. On the more positive note, it also becomes easy to extract it
into a library or move around.
Large features can be further divided into lazy sub features to make them
more manageable and improve bundling (2nd, 3rd, … level navigation)
Sharing logic between features (and sub-features) follows “extract one
level up rule” (e.g. into parent lazy feature, pattern, core or ui)

89 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Isolation and the “black box” or “throw-away” nature of lazy features
Through the previous chapters, we have learned that isolation between lazy
features (features can’t import from one another) is one of the key properties
of this architecture.

This means that each lazy feature can be treated as a “black box” which
can contain any kind of implementation, building blocks and even lazy
sub features.

The consequence of this is that even if the implementation of a lazy feature


gets dirty, the isolation prevents the spread of that dirtiness, and it won’t
affect the rest of the application.

thumb_up We should always strive to keep our lazy features as clean as


possible, but the isolation guarantee allows us to optimize for
speed of delivery where we can afford to be a bit more pragmatic
and focus on delivering value to the users instead of perfect
implementation.

Another consequence of isolation is that lazy features can be treated as a


throw-away code which will be used as long as serves its purpose. Then,
because of the isolation, it’s easy to throw it away and replace with new
better implementation instead of often much slower refactoring.
Keep in mind that such an approach should be reserved only for cases when
user flow changes significantly, and it’s faster to start from scratch than to
try to rework an existing solution.

90 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Nested lazy sub features
In practice, it’s also pretty common that some features will be so large that it
will make sense to split them into additional lazy sub features. This goes often
hand in hand with additional level of navigation in the form of a submenu
commonly located in the sidebar.

info The lazy sub features be specified in children: [] array of the


parent feature which means that the view of the parent and child
are combined as in the diagram above.
The other option is that lazy sub features will be registered as
siblings of the parent lazy feature which means they will replace
parent in the view when navigated to.

91 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
The implementation of these features then should be located within the
parent feature folder, for example feature/order/dashboard/ or
feature/order/definitions/ .

Such a lazy sub feature is the lazy loaded based on the config located in the
parent feature routes config, first let’s explore the nested children lazy sub
features scenario…
1 export const routes: Routes = [
2 {
3 path: '',
4 component: OrderComponent, // <--- parent lazy feature component
5 children: [ // will always be displayed, needs <router-outlet /> ...
6 {
7 // will be displayed when navigated to and will be added to the parent view
8 path: 'dashboard',
9 loadChildren: () => import('./dashboard/dashboard.routes'),
10 },
11 {
12 path: 'definitions',
13 loadChildren: () => import('./definitions/definitions.routes'),
14 },
15 ],
16 },
17 ];

Another option is that lazy sub features replace the parent view when
navigated to…
1 export const routes: Routes = [
2 {
3 path: '',
4 component: OrderListComponent, // <--- parent lazy feature component
5 },
6 {
7 // will be displayed when navigated to and will REPLACE the parent view
8 path: 'dashboard',
9 loadChildren: () => import('./dashboard/dashboard.routes'),
10 },
11 {
12 path: 'definitions',
13 loadChildren: () => import('./definitions/definitions.routes'),
14 },
15 ];

92 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Fractal nature of the lazy sub features
In previous example, we have added one additional level of the lazy features
and corresponding navigation, but there is nothing stopping us from adding
even more levels of lazy sub features and navigation.
This is a great example of what we can call a "fractal nature" of the lazy
features and navigation! It will allow us to add as many levels of lazy sub
features and navigation as we need while following exactly the same pattern
in their implementation!

In practice, it will be pretty rare to need more than 3 - 4 levels…

translate fractal - a geometric shape or mathematical object that exhibits


a repeating pattern at different scales or levels of magnification.
This means that as you zoom in on a fractal, you will see smaller
copies of the same pattern repeated over and over, often
infinitely.

93 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
How to share logic between multiple lazy features
Throughout this book, we have learned and discussed that the isolation
between parts of the application and especially between the lazy features
plays a key role in the architecture.
But the real world use cases are not always so clear-cut to neatly fit our
idealized architecture, and it is often useful and therefore necessary to be
able to share some logic between multiple lazy features.

Now is the time to talk about what are the most common types of logic
that we might want to share between the lazy features. Let’s learn how to
handle each of these cases in a way which preserves the isolation, clean
one way dependency graph and therefore all the benefits of the
architecture itself.

info Sharing logic between lazy features can be summarized with the
“extract one level up rule”, but where exactly the “one level up”
is, depends strongly on the type of logic that we want to share.

Types of logic that we might want to share


The types of logic that we might want to share between multiple lazy features
follow the architectural types which we discussed until now, namely the
core and ui . Besides these two, there will be also a new type, the
pattern , which we will discuss in depth in the future chapters. For now, it
will be enough to know that the pattern is a type which can be described as
a pre-packaged combination of standalones and injectables which is focused
on implementing of a specific use case.
94 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Now that we know about the types of logic that we might want to share from
the technical perspective. Let’s explore what are the most examples for each
type to get a better idea of how it will play out in practice.

The "one level up" extraction rule removes dependency arrows


between sibling lazy features on the same level!

95 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
The main distinction in the sharing behavior depends on the level of the lazy
feature, the first level lazy features will extract shared logic to the core/ or
ui/ folder, while the nested lazy sub features will extract shared logic to the
parent lazy feature.

ui - standalones in the form of generic UI components, directives and


pipes, only inputs and outputs, typical examples could be Avatar ,
Popover , Dialog , Button or Menu

used in more than one lazy feature on the first level, will be extracted to
the ui/ folder
used in more than one lazy sub feature of the same parent lazy feature
will be extracted to feature/parent-feature/ folder
core - injectables, services and other headless logic like state
management library state slices or utils, typical examples could be
OrderService , OrderState (NgRx based state slice), OrderUtils

used in more than one lazy feature on the first level, will be extracted to
the core/ folder
used in more than one lazy sub feature of the same parent lazy feature
will be extracted to feature/parent-feature/ folder

thumb_up When extracting logic with multiple related files to the core/
folder, you can always use domain-based grouping, e.g.
core/product/ or core/order/ .

96 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
The last type of sharing between lazy features is the pattern . The main
distinction between pattern and the other two types is that the pattern is
a pre-packaged combination of standalones and injectables which is focused
on implementing of a specific use case.
This makes it categorically less generic and reusable than the other two
types, but it also means that it can be used to share more complex logic
between lazy features.

97 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Besides the differences, the pattern also follows the same extraction rule
as the other two types, which should prove intuitive and easy to remember.

pattern - pre-packaged combination of standalones and injectables


focused on implementing of a specific use case, typical examples could be
DocumentManager , ItemChangeHistory or ApprovalProcess

used in more than one lazy feature on the first level, will be extracted to
the pattern/ folder
used in more than one lazy sub feature of the same parent lazy feature
will be extracted to feature/parent-feature/ folder

Example of sharing logic between lazy features


To make sure that this key concept feels as familiar as possible, let’s explore a
particular example of how to share logic between lazy features, one per each
type based on experience from real life projects.

Sharing UI components between lazy features


Besides obvious examples like Card , Button or Avatar , all af which
will most likely be provided from the selected "baseline" component
library like Angular Material, PrimeNg or TaigaUI, there are are other
examples of application specific reusable UI components which can be
shared between lazy features and will be implemented as a part of the
codebase of the application itself.

98 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
For example, in an application which allows us to define and manage various
types of ad-hoc checks that we can execute against the source code
repositories in our organization and collect the results… The application will
support multiple types of checks, and we will most likely want to display
them in a nice stylized way implemented in the CheckTypeComponent .
Then, the way this can unfold could be described as follows:

1. We’ve started working on the application, and the first lazy feature we’re
working allows us to display and manage the list of checks, we will call it
feature/checks/

2. The stylized check type is currently implemented inline in the template of


the CheckListComponent and there is no need to extract it yet
3. Now we’re adding checks editor and additional checks details view, and we
realize that we need to display the same stylized check in all of them
4. It’s time to extract the CheckTypeComponent but it will be still part of the
feature/checks/ folder as it’s used only in this feature

5. With the checks feature implemented, we’re moving to the next feature
which allows us to display results of the checks per analyzed repository, we
will call it feature/results/
6. We realize that we need to display the same stylized check in the
feature/results/ feature as well

7. We’re extracting the CheckTypeComponent to the ui/ folder as it’s used


in more than one lazy feature on the first level

99 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Sharing NgRx state slices between lazy features
Another common example is the need to share some piece of the state
between multiple lazy features.

Here we’re going to assume that the application is using NgRx as the
state management library of choice. At the same time, it would play out
in more or less the same way with other libraries, or something like
services with RxJs subjects or the newest kid on the block, the Angular
Signals.

Continuing with the previous example, let’s assume that we have


implemented the feature/checks/ feature and the feature also contains the
CheckState NgRx state slice which manages the CRUD for the Check entity
together with some view state like the currently selected check or various
filters of the list of checks. The state slices will initially be implemented in the
feature/checks/state-check/ folder.

thumb_up It's a good practice to namespace NgRx state slices as state-


<entity-name> as that will help with making things consistent
and predictable in a common case when single lazy feature
manages state of multiple entities.

With the stage set like this, let’s explore how the sharing of the CheckState
in the form of the NgRx state slice could play out in practice…

100 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
1. We’ve started working on the application, and the first lazy feature we’re
working allows us to display and manage the list of checks, we will call it
feature/checks/

2. The CheckState NgRx state slice is currently implemented in the


feature/checks/state-check/ folder and manages the state of the entity
Check together with some view state like the currently selected check or
various filters of the list of checks
3. Now we’re adding checks editor and additional checks details view, and
maybe a couple of more view related state properties into the already
existing CheckState NgRx state slice
4. With the checks feature finished, we’re moving to the next feature which
allows us to display results of the checks per analyzed repository, we will
call it feature/results/
5. We realize that we need to get access to the check entity state to be able
to enrich the results with additional information about the check used to
get a particular result
6. Now we have two options, either we can duplicate the part of the check
state slice which deals with the entity into the feature/results/state-
check/ folder, or if the entity state doesn’t change too often and will be
used in the same way by both features, we can extract it to the core/
folder
7. Because it’s the entity state used in the same way by both lazy features,
we’re going to create new core/check/state-check/ folder and generate
(with schematics) new empty NgRx state slice

101 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
8. Back in the feature/checks/ feature, we’re going to move properties and
logic related to the entity state from the feature/checks/state-check/
folder and move it into the core/check/state-check/ instead, this move
will be on the level of properties (state slice), functions (reducers,
selectors and effects) and constants (actions)
9. Once the move of logic is finalized, we’re going to use selectors and
actions from core in the checks feature to make sure that it works the
same way as before
10. After than, we can also access and manage checks entity state from the
feature/results/ and any subsequent lazy feature as well

info The partial NgRx state slice sharing and extraction represents a
rather advanced example of what is possible to achieve when
following this architecture with relative ease. It should be easy to
imagine that the overall process of sharing injectables will be
much simpler and more akin to the sharing of the UI components
when we would need to extract something like a
CheckService instead of the NgRx state slice.

Sharing patterns between lazy features


As we don’t really know what patterns are yet and because we’re going to
discuss them in more depth in the chapter dedicated later on, let’s just say
that the sharing of patterns between lazy features will follow exactly the
same "extract one level up rule" as the other two types, only difference will be
the destination folder, pattern/ instead of core/ or ui/ .

102 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Sharing lazy features
In the last couple of pages we have learned how to share logic between lazy
features by extracting parts of it using the "one level up" rule, but all this logic
was in the form of standalones, injectables and combination of thereof…

But what if we wanted to share a whole sub lazy feature between


multiple lazy features as a sub route?

Imagine a following situation where we have implemented a couple of lazy


features, feature/order/ and feature/task/ and both of them have a list
component which displays a list of their respective entities.
Selecting an entity should open a details view which displays details of the
selected entity. Then, because our data model is standardized and metadata
driven, we are able to create a generic details view sub feature that we want
to reuse in both lazy features.
The way this sub feature will be integrated will be through a route config of
the parent lazy feature, for example feature/order/order.routes.ts and
feature/task/task.routes.ts …

1 export default <Routes>[


2 {
3 path: '',
4 component: OrderListComponent,
5 },
6 {
7 path: ':id',
8 loadChildren: () => import('../detail/detail.routes'),
9 // notice the "../", we're leaving the feature/order/ folder
10 // and entering sibling lazy feature feature/detail/ which is forbidden
11 },// for the actual implementation but not for the routes!
12 ];

103 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Even though we’re referencing one feature from another, it’s only the route
config, and this is allowed by the validation rules of the architecture, and the
actual implementation of the feature will still be completely isolated and
independent!

Shared feature vs lazy sub feature


Previously we have discussed the concept of the lazy sub features which are
loaded in exactly the same way as shared features, the main difference is…

lazy sub feature - located inside the same folder as the parent lazy feature,
has less strict isolation and falls under the "black box" approach
shared feature - located in the separate folder, has complete isolation of
its implementation and can only be referenced through its route config

104 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Why are there no eager features?
The keen reader might have noticed that there was no single mention of an
eager feature throughout this book, but why is that the case?
For example, what if our application was a really simple Angular SPA…

Only one feature displayed form start


No menu or navigation
Display form to capture user data
Submit form and display success message

Such application could surely be implemented fully within the


AppComponent , right? The answer is a resounding yes, it is definitely possible
to do so! With such a case, our premise is that:

requirements tend to change


the required capabilities and respective features tend to grow over time

Assuming that we received a new requirement to capture another type of


data using different form, we will predictably have to extend the
implementation by adding routing and navigation and implementing that
second form as a lazy feature…

Now we ended up in a situation where there are two different ways of


how the feature can be implemented, eager and lazy. Often with a
custom handling to hide the initial lazy feature, which is definitely a
suboptimal situation to find ourselves in!

105 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
The better way to go about this would be to follow the prescribed
architecture. This means we’re going to implement our first, and for the time
being, the only existing feature as our first lazy feature.

That way, once the new requirements arrive, we will be able to extend our
application in a predictable standardized way which can scale further
into the future!

thumb_up In general, we should always strive to minimize the amount of


concept used to achieve the same outcome.
In this case, we want all the features to be implemented in the
exactly same way which means that even if we only have one
feature yet, we're going to implement it as our first lazy feature!

106 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Pattern lazy eager (rare)

The last main architecture type that we need to discuss is the pattern which
is a type that we have mentioned briefly in the previous chapter about the
features but haven’t really explored in depth yet.

In a nutshell, the pattern is a type which consists of a pre-packaged


combination of standalones and injectables which implement a
specific reusable use case which is consumed in a lazy feature (or rarely
in layout) with a help of "drop in" component instead of a route!

Implementation in pattern/<pattern-name>/ folder


Great for implementing of cross-cutting business features like document
manager, approval process, change history (audit log), notes or
comments which could be dropped in many lazy features
One way dependency between patterns and features, only features can
consume patterns but not the other way around
Consumed through main "drop in" component used in the templates of
lazy features (or rarely, layouts)
Sharing logic between patterns follows the familiar “extract one level up
rule” (into core or ui)

Patterns are usually consumed through their main "drop in" component which
is used in the templates of lazy features…
1 <!-- feature/order/order.component.html -->
2 <my-org-order-list />
3 <!-- other feature specific components... -->
4
5 <my-org-document-manager /> <!-- pattern consumed through the "drop in" component -->

107 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Why do we call it pattern or a "drop in" feature
During some of our Angular architecture review calls as well as during the
writing process of this book, some folks out here were specifically asking
about the name pattern and what is it supposed to express.

The idea for the name came from the development process of a generic
organization specific UI library (think in direction of MyOrgMaterial)
where we wanted to differentiate between the level of individual
generic reusable UI components (e.g. button or popover) and the level
of pre-packaged combination of such components, e.g. "context
menu" (a multiple buttons in a popover) which we called patterns …

Another great name which includes information about the way most of the
patterns will be used is the "drop in" feature. In general, each pattern will have
one root component which can be dropped in anywhere in the template of a
component which belongs to a lazy-loaded feature.
From this perspective, a pattern can be also described as a "non-routed"
feature.

thumb_up Even though patterns aren't used with the help of Angular
routing, we can still lazy load them with ease with the help of the
@defer blocks, but it is always important to consider the
tradeoffs and only opt in for such an approach if patterns is heavy
(large bundle size) or is only used after some specific user
interaction performed in the consumer lazy feature.

108 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Pattern vs ui, core and feature
Now that we have some idea what patterns are, let’s drive the point home by
highlighting the most important differences between the pattern and the
concepts which we discussed until now, namely the ui , core and
feature .

pattern vs ui - Pattern is usually bound to a specific data source in


form of service or a selector from state management library, which is
forbidden for the reusable UI components which can only consume data
through inputs and outputs. Besides that, pattern is usually a combination
of multiple standalones (pattern specific and also other reusable ui
standalones) and injectables
pattern vs core - Pattern usually has its own UI which is created as a
combination of pattern specific and other reusable ui standalones, while
core is only about headless logic and therefore doesn’t have any UI
pattern vs feature - Pattern is usually similar to a feature in that it’s a
combination of multiple standalones and injectables. The main difference
between them is that pattern is consumed through its "drop in"
component (e.g. <my-org-document-manager [context]="context" ...
/> ) instead of a route config which is the case for the lazy features

109 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
When to create a pattern
Let’s now explore a hypothetical scenario that helps us to understand when it
makes sense to create a pattern versus implementing the same functionality
as a part of a lazy feature or extracting reusable parts into the core or ui .

The scenario will deal with an application which manages products and
orders and also needs to store and display various types of documents,
for example, user guides, product specifications, order confirmations
and invoices. This example also assumes that we don’t necessarily see
the final picture from the start and that the requirements evolve over
time, and we have to react to them.

First, we’re working on the feature/product/ feature which allows us to


display and manage the list of products, with additional details view which
displays details of the selected product and edit view which allows us to edit
the selected product. The implementation of this feature will be pretty
standard, and it will be implemented in the feature/product/ folder.
Now we get a requirement to add the ability to upload and display user
guides for each product. Because no other feature needs this functionality,
we’re going to implement it as a part of the feature/product/ feature itself,
for example, in the feature/product/document/ folder.
The implementation will consist of the following parts:

document.service.ts- which will be responsible for CRUD for the


document metadata together with the document files themselves

110 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
document-list.component.ts - which will be responsible for displaying
the list of documents, with information like name, file format and upload
date together with actions like download, edit metadata and delete
document-editor.component.ts - which will be responsible for displaying
a single document in the edit mode to adjust its metadata
document-item.component.ts - which will be responsible for displaying a
single document in the preview mode
... - other smaller reusable components like document-type-
icon.component.ts which will display an icon based on the file format of
the document
So far so good, but now we get another requirement to add the ability to
upload and display order confirmations and invoices!

Now it’s time to ask ourselves a couple of clarifying questions…

1. Is the behavior of the documents in the feature/product/ feature the


same as the behavior of the documents in the feature/order/ feature?
For example, do we need to support all the use cases like the full CRUD, or
is it enough just to load related receipts and invoices?
2. Is the UI of the documents in the feature/product/ feature the same as
the UI of the documents in the feature/order/ feature? For Example, do
we need a metadata editor or is it enough just to provide download links
for the receipts and invoices?

111 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Based on any possible combination of answers to these questions, we can
end up with one of the following scenarios:

1. Quite different behavior and UI - In this case, it would make sense to keep
the implementation fully isolated to preserve the ability to evolve it
independently in the future. The cost of that will be a slight duplication, in
our case, the order feature will implement its own (and much simpler)
version of the document service and related components.
2. Similar behavior but different UI - In this case, it would make sense to
extract the common behavior into the core/ for example
core/document/ folder and implement the UI separately in each feature.
The cost of that will be a slight duplication, in our case, the order feature
will implement its own (and much simpler) version of the related
components.
3. Different behavior and similar UI - In this case, it would make sense to
extract the common UI into the ui/ for example ui/document/ folder
and implement the behavior separately in each feature in form of a
document service or event a state management solution for the more
complex one. The cost of that will be a slight duplication, in our case, the
order feature will implement its own (and much simpler) version of the
document service.
4. Similar behavior and similar UI - In this case, it would make sense to
extract the common behavior and UI into the pattern/ for example
pattern/document-manager/ folder and implement the integration in
each feature in form of a "drop in" component. This will effectively
eliminate any duplication but will couple the pattern implementation to
different requirements of the features which use it which will lead to need
to parameterize the pattern and therefore make it more complex.

112 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Pattern examples
In general, patterns are great for implementing of cross-cutting business
features like document manager, approval process, change history (audit
log), notes or comments which could be dropped in multiple lazy features or
lazy sub features.

The characteristic feature of all these patterns is that they either work
the same in every consumer or receive a relatively small amount of
configuration from the consumer lazy feature to work in a slightly
different way.

Document manager
Load, display and manage documents, e.g. user guides, product
specifications, order confirmations and invoices…

will receive configuration, for example, in the form of supported document


types (e.g. user guide, product specification, order confirmation or
invoice) and supported actions (e.g. download, edit metadata and delete)

Approval process
Load, display and manage approval process, e.g. approval of the order or
approval of the invoice…

will receive configuration, for example, in the form of supported approval


constrains (e.g. how many approvals are needed) and supported actions
(e.g. approve, reject and cancel)

113 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Change history (audit log)
Load, display and manage change history (audit log), e.g. change history of
the order or change history of the invoice…

will receive configuration, for example, in the form of supported change


types (e.g. order created, order updated, order deleted), amount of items
to display and supported actions (e.g. revert, view details, …)

Notes / comments
Load, display and manage notes / comments, e.g. notes / comments for
products or interaction with specific customer…

will receive configuration, for example, in the form of supported actions


(e.g. add, edit, delete) and supported types (e.g. public, private, …)

info There are many more examples of patterns which can be


implemented, but the key takeaway is that patterns are great for
implementing of cross-cutting business features which could be
dropped in multiple lazy features or lazy sub features with no or
minimal configuration.

114 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Helper types
In the previous chapters, we have learned about the main architecture types,
namely the ui , core , feature and pattern , but most of the developers
who ever worked with an Angular application will know there are other types
of files like app.component.ts or main.ts which were not accounted for in
the architecture types we discussed until now.

The rules set, which we will focus on and implement soon, will be
responsible to ensure automated validation of our architecture!

It will work in a way that will throw an error if it discovers a file that does
not belong to a predefined type or wasn’t defined as ignored.

This is the reason why we have to define a type for all files in our application
including the app.component.ts , app.config.ts , app.routes.ts and the
main.ts , even though their implementation will stay almost as empty as
when they were first generated with the help of Angular Schematics when fist
creating the workspace with the help of ng new .

info Another option would have been to add all these files to the
ignore list, but because these files still import and consume
our implementation, for example app.config.ts will always
import and use the core.ts file, we want to make sure that
they are accounted for in the architecture as well.

115 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
App eager

The app type will consist of all files starting with app. prefix, for example
app.component.ts ( app.component.spec.ts ), app.config.ts and
app.routes.ts .

These files will mostly stay as they were generated with the help of Angular
Schematics when first creating the workspace with the help of ng new .

app.component.ts- in most cases will contain either the <router-outlet


/> in case of multiple layouts per route or <my-org-layout />
component
app.config.ts - import and call provideCore({ routes }) function
from core.ts file (possibly with other options)
app.routes.ts - top level routes config, will import and use
feature/<feature-name>/ routes configs of the first level lazy features

Main eager

The main type will consist only of the main.ts file which is used to bootstrap
the application. The file itself will stay as it was generated with the help of
Angular Schematics when first creating the workspace.
1 import { bootstrapApplication } from '@angular/platform-browser';
2 import { appConfig } from './app/app.config';
3 import { AppComponent } from './app/app.component';
4
5 bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));

The content of the file might need to be adjusted in the near future Q1 2024
to enable "zone-less" change detection scheduler for even better
performance, but that’s a topic that is out of scope of this book.
116 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Ignoring other infrastructure files
Besides the app and main files, our workspace may also contain other
TypeScript files which are not part of the application implementation itself,
but are used to configure infrastructure related tools like testing or linting,
for example karma.conf.ts or jest.setup.ts .

We don’t really need a type (and corresponding rules) for these files as
they won’t really ever be imported in the files with our implementation,
not even by accident!

Soon we’re going to learn more about how we can define architecture
validation rules, but let’s add the ignore, snippet also here to make sure that
it’s easily accessible when needed.
1 // .eslintrc.json
2 {
3 "overrides": [
4 {
5 "files": [
6 "*.ts"
7 ],
8 // ...
9 "settings": {
10 "boundaries/ignore": [
11 "**/jest(.|-)*.ts",
12 // ...
13 ]
14 }
15 }
16 ]
17 }

117 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Relationship between types
Great! Now we have learned about all architecture types, including many
examples of how to recognize them in practice, and how to combine them to
implement real life use cases.

But how do these types relate to each other? What are the rules which
govern their relationships?

In fact, we have already mentioned some of the rules and restrictions which
will govern the relationships between these types! Now it’s time to explore
them in more depth, all together, in one place!
And there is no better way to get started than with a diagram which visualizes
the big picture…

118 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Notice that in the diagram, all the arrows point only in the single direction,
from more complex types like feature or pattern towards the simpler
types like core or ui .
At the same time, the arrows are not allowed to point in the opposite
direction, which means there is no way back from simpler types to more
complex types. This prevents the formation of cycles and therefore ensures
that the architecture will stay clean and easy to reason about!

119 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Previous diagrams were simplified to make it easier to understand the main
idea of the one-way dependency graph between the individual architectural
types. Now it’s time to expand on our previous example and reintroduce the
notion of eager and lazy parts of the application.

The diagram above contains all types which we have seen previously, but now
they are arranged in a more structured way which highlights their
relationships in regard to the eager and lazy parts of the application.

info Notice and remember that both ui and pattern types can be
consumed by the eager part of the application, namely the
standalones in the layout . This will be more common for the
ui type, but rarely it can also be a case for the pattern type.

120 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Now we will zoom in one more time to reveal internal structure of some of the
types like the feature and pattern to uncover dependencies between
these important substructures and the types which consists of simpler
building blocks like standalones in case of ui and injectables from core .

warning Always keep in mind that there should never be any dependency
arrows between the sibling lazy features, sibling lazy sub features
or between individual patterns!

121 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
The most common type of dependency graph issues
This chapter will be more relevant for the developers working on an already
existing application which was not following, and especially not using
automated validation of the architecture the way it is described in this book.
The good news is that we can still learn from it and use it to our advantage!

Feature depends on feature - The most common type of dependency


graph issue encountered in the wild, and it’s usually caused by the way
requirements tend to evolve over time. For example, a requirement is first
implemented in one feature and later, the need for same functionality
arises in another feature as well. Instead of direct reuse, the solution is to
extract the shared logic into the core/ or ui/ folder!
122 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Core depends on feature - This is the second most common type of
dependency graph issue, usually caused by a newly discovered need to use
part of the state in the eager part of the application. Instead of direct
reuse, the solution is to extract the shared logic into the core/ .

thumb_down The core/ (or for that matter any other eager part of
application) depending on a feature is especially problematic
as it breaks lazy loading and the underlying bundling which has
direct measurable negative consequence for the users (startup
performance) and developers (rebuild time on every change).

UI depends on core - Another common occurrence is when the UI


component starts generic but over time gets coupled to some state, for
example, through a service or a selector from state management library.
Instead, we should always keep such component generic and create a new
pattern which prepackages that component with specific state and
behavior…
There might be other types of dependency graph issues, but the three
above have proven to be the most common ones. The great thing about
the approach in this book is that it will automatically prevent all of them
for every newly created Angular application with the help of eslint based
tooling!

123 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Automated architecture validation
Now that we learned about all the architecture types and their relationships,
it’s time to learn about how we can automate the architecture validation with
the help of eslint to make sure that our project stays clean, maintainable
and extendable for the whole duration of its lifetime.

The automated architecture validation will be implemented with the


help of eslint and especially the eslint-plugin-boundaries which
allows us to assign types to different parts of our codebase and define
rules to govern their relationships.

info The complete pre-configured setup of the eslint-plugin-


boundaries is available as a part of the provided example
repository, but it's very valuable to learn the basic ins and outs of
the plugin configuration which will allow us to extend it further to
accommodate for the organization-specific needs.

The plugin can support everything from basic single src/ based workspace
to a more advanced setup with multiple Angular applications and libraries in
the projects/ folder of the standard Angular CLI workspace.

Angular CLI can easily support moderate number of applications and


libraries, (let’s say less than 10, but that strongly depends on the
individual size) in a single workspace. Especially since the recent
(Angular 16 and above) improvements in build speed with an
introduction of the eslint based builders. Once the teams encounter
actual build performance issues, then it’s time to consider exploring
Nx.
124 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
The eslint-plugin-boundaries anatomy
The eslint-plugin-boundaries is a plugin for eslint which allows us to
assign types to different parts of our codebase and define rules to govern
their relationships.
In general the setup of the plugin will be configured in the root
.eslintrc.json file of the workspace, so that it applies to all projects and
libraries in the workspace.
The configuration itself consists of three major parts…

basic setup - Install dependencies, add the plugin itself, extend base
rule set, setup Typescript import resolver, …
types definitions - Define settings to recognize types, e.g. feature or
core based on their location within the workspace
rules definitions- Define rules to govern relationships between the
previously defined types, e.g. feature can import from core but not the
other way around

Basic setup
To use plugin, we first have to install a couple of dependencies. Assuming
that the current Angular CLI workspace already has eslint support which was
added during the first execution of ng lint , we still have to install…
1 npm i -D eslint-plugin-boundaries eslint-import-resolver-typescript

Once the dependencies were installed successfully, it’s time to have a look
into the root .eslintrc.json file of the workspace.

125 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
The plugin focuses on the relationships between the Typescript files which
can be discovered by parsing their import and dynamic import()
statements and therefore the plugin is configured for the *.ts files.
1 {
2 // most of the configuration was skipped to focus on the boundaries plugin
3 "overrides": [
4 {
5 "files": ["*.ts"],
6 "plugins": ["boundaries"],
7 "extends": [
8 // other presets...
9 "plugin:boundaries/strict" // all files have to belong to a type
10 ],
11 "settings": {
12 "import/resolver": { // recognize both static and dynamic Typescript imports
13 "typescript": {
14 "alwaysTryTypes": true
15 }
16 },
17 "boundaries/dependency-nodes": ["import", "dynamic-import"],
18 }
19 }
20 ]
21 }

info The boundaries/strict rule set is the way to go for all new
projects, but it's also possible to start with the less strict
boundaries/recommended rule set which is more suitable for
existing projects as it allows us to have parts of the project non-
compliant with the defined types, making it possible to refactor
the codebase progressively.

With the base setup in place, it’s time to define the types and rules which will
govern their relationships…

126 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Types definitions
The types definitions are used to recognize the types of the files based on
their location within the workspace. The actual definitions will differ based on
the structure of the workspace, namely if it’s a single app src/ based or
recommended projects/ based Angular CLI workspace.

thumb_up The projects/ based Angular CLI workspace is always better


because it allows us to add additional apps and libraries in a
standardized and consistent way for a very small price of two
additional folders projects/<app-name>/ . To create
workspace in that way, we have to use the --create-
application false when first running the ng new
command.

As previously discussed, on the philosophical level, it’s exactly the same


tradeoff as creating every (so even first and only) feature as a lazy feature.
With such an approach, we will be able to add other lazy features, and
they will all be implemented in the same way!

In the following example we’re going to highlight the difference between the
definitions for the src/ based workspace and the projects/ based
workspace and after that we’re going to proceed with the full configuration
example for the projects/ based workspace as that is the focus of this
book and what is used in the provided example repository.

127 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Let’s illustrate the difference between the definitions for the src/ based
workspace and the projects/ based workspace on the example of the
feature type which is used to recognize lazy features.

1 {
2 "overrides": [
3 {
4 "files": ["*.ts"],
5 "settings": {
6 "boundaries/elements": [
7 {
8 // "projects/" based workspace
9 "type": "feature",
10 "pattern": "feature/*",
11 "capture": ["feature"],
12 "basePattern": "projects/**/src/app",
13 "baseCapture": ["app"],
14
15 // "src/" based workspace
16 "type": "feature",
17 "pattern": "src/app/feature/*",
18 "capture": ["feature"],
19 }
20 ],
21 }
22 }
23 ]
24 }

projects/ based workspace


type - architecture type which we want to recognize, the feature

pattern - pattern to match the files which belong to the specific type
capture - capture groups to extract the name of the type from the
pattern, e.g. features/order will capture order , captures *
basePattern - pattern to match a path from the root of the workspace
to a specific app
baseCapture - capture groups to extract the name of the app from the
base pattern, e.g. projects/my-org-app/src/app will capture my-org-
app , it captures what is matched by **

128 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
src/ based workspace
type - architecture type which we want to recognize, the feature

pattern - pattern to match the files which belong to the specific type
from the root of the workspace
capture - capture groups to extract the name of the type from the
pattern, e.g. src/app/feature/order will capture order , captures *

warning For the projects/ based workspace, the basePattern and


baseCapture are used to recognize the application to which
the files of that type belong. This is very important because we
will never want to share logic between multiple applications in a
single workspace!
To do that, we again need to follow the extract "one level up rule"
and on this level, this translates into extracting the shared logic
into a library!

The final types definitions for the projects/ based workspace will be
provided in the next code example. The example is rather long, so it will
be split into multiple pages but in the actual .eslint.json file, it will be
a single block of the "boundaries/elements" definitions…

Notice that the value of type and pattern property in the next code
example matches the name and folder of the architecture type which we
have learned about in the previous chapters.

129 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
1 {
2 "overrides": [
3 {
4 "files": ["*.ts"],
5 "settings": {
6 "boundaries/elements": [
7 // helper types
8 {
9 "type": "main",
10 "mode": "file",
11 "pattern": "main.ts",
12 "basePattern": "projects/**/src",
13 "baseCapture": ["app"]
14 },
15 {
16 "type": "app",
17 "mode": "file",
18 "pattern": "app(-|.)*.ts", // app-routes, app.component, ...
19 "basePattern": "projects/**/src/app",
20 "baseCapture": ["app"]
21 },
22
23 // architecture types
24 {
25 "type": "core",
26 "pattern": "core",
27 "basePattern": "projects/**/src/app",
28 "baseCapture": ["app"]
29 },
30 {
31 "type": "ui",
32 "pattern": "ui",
33 "basePattern": "projects/**/src/app",
34 "baseCapture": ["app"]
35 },
36 {
37 "type": "layout",
38 "pattern": "layout",
39 "basePattern": "projects/**/src/app",
40 "baseCapture": ["app"]
41 },
42 {
43 "type": "pattern",
44 "pattern": "pattern",
45 "capture": ["pattern"],
46 "basePattern": "projects/**/src/app",
47 "baseCapture": ["app"]
48 },
49 ], // the config continues on the next page...
50 }
51 }
52 ]
53 }

130 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
1 {
2 "overrides": [
3 {
4 "files": ["*.ts"], // ...continued from the previous page
5 "settings": { // overrides -> settings -> boundaries/elements
6 "boundaries/elements": [// won't be repeated, just to show nesting...
7 {
8 "type": "feature-routes", // distinction between routes and impl
9 "mode": "file", // will be important for the rules
10 "pattern": "feature/*/*.routes.ts",
11 "capture": ["feature"],
12 "basePattern": "projects/**/src/app",
13 "baseCapture": ["app"]
14 },
15 {
16 "type": "feature", // actual impl of feature
17 "pattern": "feature/*",
18 "capture": ["feature"],
19 "basePattern": "projects/**/src/app",
20 "baseCapture": ["app"]
21 },
22
23 // library types
24 {
25 "type": "lib-api", // public API of the library
26 "mode": "file",
27 "pattern": "projects/**/src/public-api.ts",
28 "capture": ["lib"]
29 },
30 {
31 "type": "lib",
32 "pattern": "projects/**/src/lib",
33 "capture": ["lib"]
34 }
35 ],
36 }
37 }
38 ]
39 }

warning The defined rules come with some limitations, for example, we
should not use app- prefix for a file within feature or ui types as
such file would get matched by and assigned to the app type
which is undesirable!

131 Tomas Trajan


@tomastrajan
Angular
NoticeEnterprise
that someArchitecture
types will have substructure, for example, the
feature type will capture actual features by their name which stands in
contrast to core or ui where just belonging to that specific type is
enough as any standalone within ui can depend on any other
standalone within ui and the same goes for injectables in the core .

info Capturing individual features by their name is especially


important for the feature type because it will allow us to
enforce the rule that there should never be any dependency
arrows between the lazy features, sibling lazy sub features or
between individual patterns! On the other hand, the core ,
pattern and ui don't have such a substructure.

The reason for this is that we're trying to optimize the tradeoff
between the security provided by the strictness of the
architecture and ability to get things done in a reasonable
amount of time!

Rules definitions
With the types definitions in place, it’s time to focus on the last missing piece
of the puzzle, the rules definitions which will govern the relationships
between the previously defined types!
The rules themselves will be defined in the same .eslintrc.json file of the
workspace in the rules section where we’re going to specify them as the
part of the "boundaries/element-types" config.

132 Tomas Trajan


@tomastrajan
Angular
1
2
{ Enterprise Architecture
"overrides": [
3 {
4 "files": ["*.ts"],
5 "rules": {
6 "boundaries/element-types": ["error",
7 { "default": "disallow", "rules": [/*...*/] }
8 ]
9 }
10 }
11 ]
12 }

As with the type definitions there will be a slight difference between rules for
the src/ and the projects/ based workspace.
1 {
2 "overrides": [
3 {
4 "files": ["*.ts"],
5 "rules": {
6 "boundaries/element-types": [
7 "error", {
8 // without explicit rule, depending on (importing from) files is disallowed
9 "default": "disallow",
10 "rules": [
11 // "projects/" based workspace
12 {
13 "from": "core",
14 "allow": [ // the core can depend on itself (within a single app)
15 ["core", { "app": "${from.app}" }],
16 ["lib-api"] // core can depend on the public API of a library
17 ]
18 },
19 // "src/" based workspace
20 {
21 "from": "core",
22 "allow": ["core"] // the core can only depend on itself
23 }
24 ]
25 }
26 ]
27 }
28 }
29 ]
30 }

As we can see the projects/ type workspace rules are more complex
because they have to account for the fact that there can be multiple apps
and libraries in the single Angular CLI workspace.
133 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
This extra complexity, ["core", { "app": "${from.app}" }] instead of just
"core" , allows us to enforce the type depending on other types within the
same application and an explicit rule for consumption of libraries through
their public API ["lib-api"] is also necessary.
In the similar way as previously, rules definitions will be split into multiple
pages but in the actual .eslint.json file, it will be a single block of the
"boundaries/element-types" definitions…

1 {
2 "overrides": [
3 {
4 "files": ["*.ts"],
5 "rules": {
6 "boundaries/element-types": [
7 "error", {
8 "default": "disallow",
9 "rules": [
10 {
11 "from": "main",
12 "allow": [["app", { "app": "${from.app}" }]]
13 },
14 {
15 "from": "core",
16 "allow": [["lib-api"], ["core", { "app": "${from.app}" }]]
17 },
18 {
19 "from": "ui",
20 "allow": [["lib-api"], ["ui", { "app": "${from.app}" }]]
21 },
22 {
23 "from": "layout",
24 "allow": [
25 ["lib-api"],
26 ["core", { "app": "${from.app}" }],
27 ["ui", { "app": "${from.app}" }],
28 ["pattern", { "app": "${from.app}" }]
29 ]
30 },
31 ]
32 }
33 ]
34 }
35 }
36 ]
37 }

134 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
1 {
2 "overrides": [
3 {
4 "files": ["*.ts"],
5 "rules": {
6 "boundaries/element-types": [ // ...continued from the previous page
7 "error", { // overrides -> rules -> boundaries/element-types
8 "default": "disallow", // won't be repeated
9 "rules": [
10 {
11 "from": "app",
12 "allow": [
13 ["lib-api"],
14 ["app", { "app": "${from.app}" }],
15 ["core", { "app": "${from.app}" }],
16 ["layout", { "app": "${from.app}" }],
17 ["feature-routes", { "app": "${from.app}" }]
18 ] // routes only, nothing from actual lazy features!
19 },
20 {
21 "from": ["feature-routes"],
22 "allow": [
23 ["lib-api"],
24 ["env", { "app": "${from.app}" }],
25 ["core", { "app": "${from.app}" }],
26 ["pattern", { "app": "${from.app}" }],
27 ["feature", {
28 "app": "${from.app}",
29 "feature": "${from.feature}" // from the same feature
30 }], // use local impl from the same feature (service, guard, ...)
31 ["feature-routes", {
32 "app": "${from.app}",
33 "feature": "!${from.feature}" // from other features (notice !)
34 }] // feature can reference routes to lazy load sub features
35 ] // but not the actual implementation of other features
36 },
37 {
38 "from": ["feature"], // fully isolated from other features
39 "allow": [
40 ["lib-api"],
41 ["env", { "app": "${from.app}" }],
42 ["core", { "app": "${from.app}" }],
43 ["ui", { "app": "${from.app}" }],
44 ["pattern", { "app": "${from.app}" }]
45 ]
46 },
47 ]
48 }
49 ]
50 }
51 }
52 ]
53 }

135 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
1 {
2 "overrides": [
3 {
4 "files": ["*.ts"],
5 "rules": {
6 "boundaries/element-types": [ // ...continued from the previous page
7 "error", { // overrides -> rules -> boundaries/element-types
8 "default": "disallow", // won't be repeated
9 "rules": [
10 {
11 "from": ["pattern"],
12 "allow": [
13 ["lib-api"],
14 ["core", { "app": "${from.app}" }],
15 ["ui", { "app": "${from.app}" }],
16 ["pattern", { "app": "${from.app}" }]
17 ]
18 },
19
20 // library rules
21 {
22 "from": ["lib-api"],
23 "allow": [["lib", { "app": "${from.lib}" }]]
24 },
25 {
26 "from": ["lib"],
27 "allow": [["lib", { "app": "${from.lib}" }]]
28 }
29 ]
30 }
31 ]
32 }
33 }
34 ]
35 }

thumb_up Notice that feature implementation (the feature type) is


separated from the feature routes (the feature-routes type)
and only the feature-routes an be referenced from the app
(routing) or other lazy feature feature-routes but not the
actual implementation of other features themselves. This ensures
that the most important part of isolation is preserved at all times!

136 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Configuration of rules per project
Everything that we have seen until now was an approach focused on one
global architecture validation system defined in the root .eslintrc.json
file which applies to all projects and libraries in the workspace.

This tends to work really well for the vast majority of Angular projects,
but it’s reasonable to acknowledge that there might be some projects
which have specific needs which are not covered by proposed global
setup.

In such cases, it’s possible to move this configuration to the nested


.eslintrc.json files which are located in the individual projects and
libraries in the projects/ folder of the standard Angular CLI workspace.
Furthermore, if the majority of applications and libraries in the workspace
have the same needs, it’s possible to create a shared configuration which can
be extended by the individual .eslintrc.json of these projects to make
sure that the configuration stays DRY and easy to maintain.

New project vs an existing project (strictness)


The proposed configuration is very strict in the sense that it will throw an
error if it discovers a file that does not belong to a predefined type or wasn’t
defined as ignored.
This is the correct approach for every new project, but for the existing
projects, it’s not realistic to expect to clean the whole architecture and
dependency graph in a single step. Therefore, it’s better to start with the less
strict boundaries/recommended and switch to the boundaries/strict once
the architecture is cleaned and the team is ready to enforce the strictness.
137 Tomas Trajan
@tomastrajan
Angular Enterprise Architecture
Architecture vs implementations
The book and the provided example repository focus solely on architecture
and related concerns. Therefore, it is not opinionated about many topics that
need to be handled by most projects but are at the same time quite unique to
every project and organization.

State management - depending on the amount of state, the need to share


state between multiple features and the complexity of how state has to
change based on user interaction, the app will land somewhere on the
spectrum from component based state management to full-blown state
management library like @ngrx/store
Component library - the example comes with @angular/material out of the
box, but there are many other 3rd party component libraries like primeng,
Taiga UI, or even custom…
SSR/ SSG - is the app a public website that needs to be indexed by the
search engines? In that case, SSR or SSG can dramatically improve SEO,
Lighthouse score and performance.
I18n - build time ( $localize ), runtime (3rd party libs like ngx-translate),
or none
Authentication & Authorization - does app even need to manage it?, SSO,
OAuth, 3rd party…
Other - every project is unique with a different set of requirements and
constrains…

138 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Limitations and alternatives
The described approach works really well for the majority of Angular projects
and environments! This is because at the end of the day, no matter how many
applications there are, or how large they become, we will always need to
manage the relationships between the individual parts of the codebase.

Because of this, it doesn’t really matter if we apply these principles on


the level of the folders within a single Angular application in a standard
Angular CLI workspace or on the level of libraries inside NX monorepo
which supports very similar concepts and ideas with its enforce-
module-boundaries rule.

Therefore, philosophically, there is little difference between:

a single application with many fully isolated lazy features


multiple smaller applications with less lazy features in a monorepo

The key idea to realize that it’s all about the isolation and that the isolation
can be achieved on any technological level!
And then there are technical limitations of the tooling, which may result in
very slow builds, testing or linting that lead to painful friction in the
development process!

thumb_up With today's hardware and Angular CLI improvements, especially


the introduction of the esbuild based builder, the vast
majority of projects can easily be handled without NX!

139 Tomas Trajan


@tomastrajan
Angular Enterprise Architecture
Of course, there are other things to consider like the…

general organization structure


number of teams and their sizes
skill level
amount of legacy code
number of existing projects
deployment strategy

… but the architecture itself and the underlying principles are more or less the
same and should be applicable, even if in an adjusted form, to the vast
majority of Angular projects!

Ideal fit
The approach in this book is ideal for the new projects in a standard Angular
CLI workspace. Such a workspace can easily support moderate number of
applications and libraries which can be worked on by multiple teams in
parallel because of the strong focus on the isolation and the clear boundaries!

Even with a single large application, the guaranteed isolation of each


feature and pattern will make it much easier to *distribute the work
between multiple teams. This allows for scaling of the overall
development process as the application grows in size and complexity.

140 Tomas Trajan


@tomastrajan
Hands on architecture
The last part of the book is a hands-on step-by-step guide how to recreate
the provided example repository from scratch to see how all the discussed
concepts and ideas come together in practice.

info The provided example repo can serve as a great starting point,
but it can be still very useful to go through the process of
creating the architecture from scratch to understand the
concepts and ideas in more depth.
This should empower us to be able to adjust the architecture to
the specific needs of the organization as well as to get a better
idea how such architecture can be back-ported to the existing
projects in your organization.

Setup and project creation


The first step is to set up our development environment and create a new
workspace with the help of Angular CLI. As of September 2024, the latest
version of Angular CLI is 18.2.5 but depending on when you’re reading this,
the version might be different, so it’s better to check the latest version on the
official Angular.dev homepage.

Setup environment
First, we have to make sure that we are using a supported version of Node.js
and NPM which can be determined by checking actively supported versions
on the official Angular.dev documentation.

141 Tomas Trajan


@tomastrajan
Hands on architecture
The current locally available version of Node.js can be checked by running the
following command in the terminal…
1 node --version # v20.14.0
2 npm --version # 10.8.0

Once we have the correct version of Node.js and NPM installed, it’s time to
install the latest version of Angular CLI…
1 npm install -g @angular/cli@latest

The installation can be validated by running


1 ng version

which should output the version of the Angular CLI and the underlying
Angular framework (if run in an existing Angular workspace)…
1
2 _ _ ____ _ ___
3 / \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
4 / \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
5 / ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
6 /_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
7 |___/
8
9
10 Angular CLI: 17.1.2
11 Node: 20.9.0
12 Package Manager: npm 10.1.0
13 OS: linux x64
14
15 Angular:
16 ...
17
18 Package Version
19 ------------------------------------------------------
20 @angular-devkit/architect 0.1701.2 (cli-only)
21 @angular-devkit/core 17.1.2 (cli-only)
22 @angular-devkit/schematics 17.1.2 (cli-only)
23 @schematics/angular 17.1.2 (cli-only)

142 Tomas Trajan


@tomastrajan
Hands on architecture
Create a new workspace
Once the Angular CLI is installed, it’s time to create new workspace with the
help of the ng new command…
1 ng new my-org-workspace --create-application false --prefix my-org

and enter the newly created workspace…


1 cd my-org-workspace

Set up IDE (Webstorm, VS code) and generate application


Now that our workspace is created, it’s time to set up the IDE and open the
newly created workspace. Then we can have a first look at the structure of the
workspace and to make sure that everything is set up correctly.

Personally, I prefer to use Webstorm (or IDEA), as we have found it to


provide by far the best available developer experience for the Angular
projects on the market, but it’s also possible to use VS code (free) with
the Angular Language Service extension (and some additional
extensions) which provides a very similar experience.

143 Tomas Trajan


@tomastrajan
Hands on architecture
The created workspace is pretty much empty, only containing a couple of
configuration files and the package.json with the base set of dependencies
and NPM scripts.
Because there is no app or lib yet, the content of the angular.json file is
also pretty much empty as well…
1 {
2 "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 "version": 1,
4 "newProjectRoot": "projects",
5 "projects": {
6 }
7 }

Let’s add out first app to the workspace…


1 ng g application example-app --prefix my-org --routing --style=scss --strict --ssr false

1 CREATE projects/example-app/src/app/app.component.scss (0 bytes)


2 CREATE projects/example-app/src/app/app.component.html (19903 bytes)
3 CREATE projects/example-app/src/app/app.component.spec.ts (931 bytes)
4 CREATE projects/example-app/src/app/app.component.ts (311 bytes)
5 CREATE projects/example-app/src/main.ts (250 bytes)
6 CREATE projects/example-app/src/app/app.config.ts (227 bytes)
7 CREATE projects/example-app/src/app/app.routes.ts (77 bytes)
8 CREATE projects/example-app/tsconfig.app.json (271 bytes)
9 CREATE projects/example-app/tsconfig.spec.json (281 bytes)
10 CREATE projects/example-app/src/favicon.ico (15086 bytes)
11 CREATE projects/example-app/src/index.html (302 bytes)
12 CREATE projects/example-app/src/styles.scss (80 bytes)
13 CREATE projects/example-app/src/assets/.gitkeep (0 bytes)
14 UPDATE angular.json (3045 bytes)
15 UPDATE package.json (1047 bytes)
16 ✔ Packages installed successfully.

info The flags may differ in the future. It is always possible to list
supported flags with ng g application --help and the --
help flag can be used with any command to get more info!

144 Tomas Trajan


@tomastrajan
Hands on architecture
With the application created, the angular.json file will be updated to
include information about it including schematics property which we’re
going to use later to preconfigure default flags to make use of schematics
even more convenient…

For the schematics, it always makes sense to preconfigure the following


defaults so that the components are generated consistently out of the box!
1 {
2 //...
3 "schematics": {
4 "@schematics/angular:component": {
5 "standalone": true,
6 "changeDetection": "OnPush",
7 "style": "scss",
8 "displayBlock": true // will add :host { display: block; } to the component styles
9 }
10 }
11 }

145 Tomas Trajan


@tomastrajan
Hands on architecture
Let’s try to run the generated application to see if everything works so far.
Let’s add --open (or -o ) flag to the start script in the package.json to
open the app in the default browser…
1 {
2 "scripts": {
3 "start": "ng serve --open"
4 }
5 }

… and then run the start script in the terminal…


1 npm start

The running application should open up automatically in the browser, and


(depending on the version) it could look like this…

146 Tomas Trajan


@tomastrajan
Hands on architecture
Prettier support
Prettier is an amazing opinionated code formatter which can be used to
format all the code in the workspace in a consistent way, which allows us to
completely forget about the code style and focus on the actual code itself.
Let’s start by installing the Prettier…
1 npm install -D prettier

and then create a .prettierrc file in the root of the workspace with the
following content…
1 {
2 "singleQuote": true
3 }

The specific configuration is subject to the personal preference, but the


singleQuote seems to make sense as on most keyboards, it’s possible to
type the single quote with a single key press compared to the double quote
which requires the shift key to be pressed as well…
Read more about Prettier configuration in the official documentation and see
the list of all available options in the options documentation.

It’s usually a good idea to bind Prettier to a specific key combination in the
IDE to be able to format the whole file (or selected code block) on demand.
147 Tomas Trajan
@tomastrajan
Hands on architecture
With Prettier in place and configured for the used IDE, let’s also add
formatting related scripts to the package.json to make it possible to
format or check if it’s properly formatted in whole workspace!
1 {
2 "scripts": {
3 "format:test": "prettier --list-different \"./projects/**/*.{ts,html,scss,json}\"",
4 "format:write": "prettier --write \"./projects/**/*.{ts,html,scss,json}\""
5 }
6 }

Let’s test the formatting by running the format:test script in the terminal
and out of the box there are going to be some files that are not properly
formatted…
1  npm run format:test
2
3 > [email protected] format:test
4 > prettier --list-different "./projects/**/*.{ts,html,scss,json}"
5
6 projects/example-app/src/app/app.component.html
7 projects/example-app/src/app/app.component.spec.ts
8 projects/example-app/src/app/app.component.ts
9 projects/example-app/src/app/app.config.ts
10 projects/example-app/src/index.html
11 projects/example-app/src/main.ts
12 projects/example-app/tsconfig.app.json
13 projects/example-app/tsconfig.spec.json

let’s fix the formatting by running the format:write script…


1 npm run format:write

Follow up, run of the format:test script should not output anything, which
means that the whole workspace is properly formatted!
The formatting setup can be further enhanced by its integration with the
version control system in form of the commit hooks, for example with lint-
staged or husky , learn more…

148 Tomas Trajan


@tomastrajan
Hands on architecture
Bundle size analysis (and budgets)
In the theoretical part of the book, we have discussed the importance of the
bundle size. We also focused on the need to keep it under control to make
sure that the application startup is fast enough for the users as well as its
impact on the developer experience (build and rebuild times).

The use of the automated architecture validation will help us to keep the
bundle size under control, but at the same time, it still can be very useful
to have a tool in place which will allow us to analyze and get the full
understanding of where our application stands when the need arises.

There are three main tools that can be used to analyze the bundle size…

esbuild-visualizer - can be used with the esbuild based builders, but


produces files, so it needs additional setup in form of http-server or
similar to be able to visualize the results. It also supports multiple
visualization modes, like treemap (default, similar to webpack-bundle-
analyzer ), sunburst , network , or flamegraph

source-map-explorer - can be used with any builder that produces


source maps, the disadvantage is that it’s not as visual and has fewer
features compared to the webpack-bundle-analyzer
webpack-bundle-analyzer - currently still the most used tool overall, but
it can’t be used with esbuild based builders so new projects or existing
projects which already use esbuild won’t be able to use it, this means
that the tool has its place only in the existing projects which use the
webpack based builders

149 Tomas Trajan


@tomastrajan
Hands on architecture
Let’s add the esbuild-visualizer and the source-map-explorer to the
workspace…
1 npm install -D esbuild-visualizer source-map-explorer http-server

After that add the following scripts to the package.json …


1 {
2 "scripts": {
3 // line breaks added for better readability (actual scripts is on a single line)
4
5 "analyze": "ng build --stats-json --output-hashing none --named-chunks
6 && esbuild-visualizer --template treemap --metadata dist/example-app/stats.json
7 --filename dist/example-app/analyse/index.html
8 && http-server -o -c-1 ./dist/example-app/analyse/",,
9
10 "analyze:sme": "ng build --source-map --output-hashing none --named-chunks
11 && source-map-explorer dist/example-app/browser/*.js
12 --html dist/example-app/sme/index.html
13 && http-server -o -c-1 ./dist/example-app/sme/",
14 }
15 }

For the script, the --template flag value can be changed to


analyze
sunburst , network , or flamegraph based on the preference…

The -o and -c-1 flags for http-server are used to automatically open the
browser and disable the cache respectively to make sure we’re always seeing
the latest result.

info The --output-hashing none and --named-chunks flags


are used to make sure that the chunks are named based on the
file name and their names are shorter which makes it easier to
understand the results.

150 Tomas Trajan


@tomastrajan
Hands on architecture
Running the analyze script in the terminal will build the application and
then open the browser with the visualization of the bundles and their sizes
which depending on the chosen template can look like this…

Another option to get better visualization of the esbuild based builders is


to upload the stats.json file (that can be found in the dist/ folder per
application) to the esbuild Bundle Size Analyzer website

151 Tomas Trajan


@tomastrajan
Hands on architecture
Another concept strongly related to the bundle size is the Angular CLI based
bundle size budgets which can be used to specify at which size the build
process provides a warning or an error.

As such, we can think about the budgets as a unit test for the bundle size
and a great way to enforce the desired bundle size limits!

The budgets are specified in the angular.json file per application in the
configurations section…

1 {
2 "projects": {
3 "example-app": { // app name (will be configured per app)...
4 "architect": {
5 "build": {
6 "configurations": {
7 "production": {
8 "budgets": [
9 {
10 "type": "initial",
11 "maximumWarning": "750kb", // reasonable initial targets
12 "maximumError": "1mb" // depends on used libraries
13 } // and the amount of eager impl
14 ]
15 }
16 }
17 }
18 }
19 }
20 }
21 }

info Sticking to a specific budget should not become a goal in itself


and instead the better way to think about budgets is as a tool to
detect accidental large changes in the bundle size which are
usually caused by incorrectly importing the whole 3rd party
libraries instead of just a parts which we're going to use!

152 Tomas Trajan


@tomastrajan
Hands on architecture
Dependency graph analysis
Another key topic which we have discussed in the theoretical part of the
book is the importance of the clean one way dependency graph and most of
the approaches and efforts in the book are in place for exactly this reason, to
make sure that the dependency graph is clean, and we can reap all the
derived benefits like enhanced maintainability, extensibility, rebuild times,
and more…

The automated architecture validation guarantees us that the


dependency graph will stay clean on the level of the individual
architectural types of the codebase.
On the other hand, within the individual architectural types, it’s still
possible to introduce circular dependencies and other issues which can
be detected with the help of the dedicated dependency graph analysis
tools like madge.

Let’s add the madge to the workspace…


1 npm install -D madge npm-run-all

Additional dependency, the Graphviz is required in order to generate


visual graphs which is the best way to read madge output…

1 brew install graphviz || port install graphviz # macOS


2
3 sudo apt-get install graphviz # Ubuntu (Windows WSL, Windows WSL2, ...)

Once done, let’s create a deps/example-app/ folder, after that we are going
to add new analyze:deps scripts to the package.json …

153 Tomas Trajan


@tomastrajan
Hands
1
2
{ on architecture
"scripts": {
3 "analyze:deps": "npm-run-all analyze:deps:*", // run all analyze:deps:* scripts
4
5 // line breaks added for better readability (actual scripts is on a single line)
6 "analyze:deps:all": "madge projects/example-app/src/main.ts --ts-config tsconfig.json
7 --image ./deps/example-app/_all.png",
8
9 // per building block type dep graph analysis
10 "analyze:deps:ui": "madge --extensions ts --ts-config tsconfig.json
11 --image ./deps/example-app/ui.png projects/example-app/src/app/ui/",
12 // add same for core, layout, pattern, feature, ...
13 }
14 }

info Notice that we're namespacing the output file path with the
name of the application to make sure that it will be easy to
extend the script to analyze multiple applications in the future!
Once more apps are added, we would also namespace the script
itself, for example analyze:deps:example-app

Running the analyze:deps script in the terminal will generate the


dependency graph for the example-app which will be very minimal as we
haven’t provided any implementation yet…

It might be a good idea to re-run this script at some time interval (e.g.
once per month) and especially when merging large PRs to make sure
that we’re not introducing any circular dependencies! We can easily
recognize them as they are highlighted in red in the generated graph!

154 Tomas Trajan


@tomastrajan
Hands on architecture
Eslint support
The eslint is one of the most popular and powerful linters for the
JavaScript and TypeScript!

The eslint is the actual tool on which the automated architecture


validation is built upon and is therefore mandatory to have it in place
before we can proceed to define the architecture rules.

Currently, it is not added as a part of the Angular CLI workspace out of the
box, but it’s very easy to add it afterward with the help of Angular
schematics…
1 ng lint

Running this will detect that there is no eslint support in the workspace
yet, and will offer to add it…
1 Cannot find "lint" target for the specified project.
2 You can add a package that implements these capabilities.
3
4 For example:
5 ESLint: ng add @angular-eslint/schematics
6
7 Would you like to add ESLint now? (Y/n)

After we’ve answered this and all following prompts with Y the process will
install all relevant dependencies and add the necessary configuration to the
workspace. Afterward we can validate that everything worked out by re-
running the ng lint again…
1 ng lint
2
3 Linting "example-app"...
4
5 All files pass linting.

155 Tomas Trajan


@tomastrajan
Hands on architecture
Sometimes, it might be necessary to manually configure the eslint in the
used IDE to make sure that the linting errors and warnings are visible directly
in the editor.
For example, in case of Webstorm, it might be necessary to go to the
Settings -> Languages & Frameworks -> JavaScript -> Code Quality
Tools -> ESLint and to enable the Automatic ESLint configuration and
to set the path to the node_modules/@angular-eslint folder…

The setup can be verified by changing of the app.component.ts file, for


example by adjusting selector from my-org-root to app-root . The
problem should be highlighted directly in the editor…

156 Tomas Trajan


@tomastrajan
Hands on architecture
Schematics support
Angular Schematics is a powerful tool that can be used to generate and
modify the code in the workspace in a consistent way, reducing the amount
of repetitive work and making it easier to follow the best practices!

Schematics are hands down one of the best and the most powerful
features of the Angular!

thumb_up Angular Schematics completely remove the pain of large rarely


executed tasks like creation of new workspace or application as
well as day to day repetitive tasks like creation of a new
component (including testing setup) or service!

Another consequence of having Angular Schematics is that they greatly


contribute to the stability of Angular over time because they remove need
to micro-optimize framework boilerplate (like @Component ).
It’s hard to feel the pain of boilerplate which we did NOT need to produce in
the first place because everything is generated. This makes stability and
consistency much more important and valuable in comparison to a need to
type a bit fewer characters during the component or service creation!
The schematics can be run from the terminal with the ng g command ( g
stands for generate )…
1 ng g --help # list all available schematics
2
3 ng g c features/home/example # will generate a new component in the home feature

157 Tomas Trajan


@tomastrajan
Hands on architecture

thumb_down The issue with running the schematics from the terminal is that
in large projects, the paths can become very long and tedious or
even downright hard to type correctly, which would predictably
lead to the errors and frustration.

Luckily, most of the IDEs have built-in (or extension based) support for the
Angular Schematics which allows us to execute them on a selected folder in
the IDE file explorer instead of in the terminal.
This completely removes the need to type the paths as we only need to
provide final name of the component or service together with a couple of
flags (only the ones for which it doesn’t make sense to preconfigure them in
the angular.json file). The IDE will then take care of the rest and the use of
the schematics will become a walk in the park!

158 Tomas Trajan


@tomastrajan
Hands on architecture
Once we configured our IDE to run schematics with a key shortcut, let’s
select the folder again and hit the key…

info The list of available schematics can be extended by the 3rd party
libraries and the custom schematics which can be created in the
organization to automate the repetitive tasks and to enforce the
best practices! A common addition schematics collection are
@ngrx/schematics or @angular-eslint/schematics ...

Now that we have selected the desired schematic to run, in our case the
component , there will be a last step where we have to provide the
component name and other flags. When generating component, all flags can
and should be preconfigured in the angular.json file to make sure our
codebase follows consistent code style!
159 Tomas Trajan
@tomastrajan
Hands on architecture

Once we hit Ok button, the component will be generated and the IDE will
print out the output of the schematics run in the IDE-based terminal…
1 /@angular/cli/bin/ng.js generate component avatar
2 CREATE projects/example-app/src/app/shared/avatar/avatar.component.scss (0 bytes)
3 CREATE projects/example-app/src/app/shared/avatar/avatar.component.html (21 bytes)
4 CREATE projects/example-app/src/app/shared/avatar/avatar.component.spec.ts (596 bytes)
5 CREATE projects/example-app/src/app/shared/avatar/avatar.component.ts (314 bytes)
6 Done

Our component is ready and waiting, so we can jump straight to work,


without a need to worry about the boilerplate! We didn’t need to copy,
rename, move, or anything like that!

160 Tomas Trajan


@tomastrajan
Hands on architecture
Schematics pre-configuration
We should always make sure to preconfigure the schematics in the
angular.json file to reduce number of flags which we have to provide
manually when running the schematics in the IDE almost to zero!
1 {
2 "newProjectRoot": "projects",
3 "projects": {
4 "example-app": {
5 "schematics": {
6 "@schematics/angular:component": {
7 "standalone": true,
8 "changeDetection": "OnPush",
9 "style": "scss"
10 }
11 }
12 }
13 }
14 }

The only type of flags which can’t be preconfigured, and instead, we need to
pass them when we invoke schematics in our IDE are the ones that are used to
describe some kind of relationship between the generated code and the rest
of the codebase!

For example, previously when generating NgModule based lazy feature, it


was possible to specify which module should serve as a parent lazy
feature and the route config would be added to the parent module
instead of the app-routing.module.ts file.
This is currently not really useful as we’ve fully embraced standalone
approach , but a great member of the Angular community Chau Tran is
looking into how to make it work with the standalone approach as well!

161 Tomas Trajan


@tomastrajan
Hands on architecture
3rd party schematics
Besides official Angular Schematics which are implemented in the
@schematics/angular package, many other popular Angular libraries like
Angular Material, NgRx, or Angular ESLint provide their own schematics
collections.
These collections usually bring 3 main types of schematics:

- to automate the installation and the initial setup of the library in


ng-add
the target Angular workspace, for example, Angular Material will guide us
through the setup of themes, typography, and animations
- to automate the update of the library to the latest version,
ng-update
including custom library specific migrations will be run as a part of the
main Angular CLI update process
library-specific building blocks - to automate the creation of the library-
specific building blocks like components, services, or modules, for
example, NgRx schematics will automate the creation of the NgRx actions,
selectors, reducers, effects or even whole integrated state features

When using multiple schematics packages (also called collections), we


should list them in the angular.json file to make sure they are picked up by
the tooling, especially in terminal…
1 {
2 "cli": {
3 "schematicCollections": [
4 "@ngrx/schematics",
5 "@schematics/angular"
6 ],
7 }
8 }

162 Tomas Trajan


@tomastrajan
Hands on architecture
Automatic migrations with ng update

Another amazing thing about Angular Schematics is the ng update


command which can be used to automate the update of the Angular
workspace to the latest version of the Angular framework and related
libraries, including the custom library specific migrations which are run as a
part of the main Angular CLI update process.

Our workspace is starting at Angular v18.2 which means we get most of


the new features like standalone, new control flow syntax or some of the
signals out of the box.

All these recent changes proved the invaluable position of the ng update
which enables Angular and hence also our projects to evolve and embrace
ever improving best practices and approaches without prohibitive cost of
manual migration.

info Recent (Angular 17) release of new control flow represents a


perfect example of the power of the ng update and related
Angular Schematics based migrations.
Personally, I was able to migrate a medium (on the larger side)
sized project to the new control flow in less than two hours with
the help of the ng g @angular/core:control-flow where
the initial schematics run updated approximately 95% of the
codebase and the rest was easy to fix manually!

Besides the ng update itself, the official Angular Update Guide is an


additional great resource to help us to keep our projects up to date!
163 Tomas Trajan
@tomastrajan
Hands on architecture
Component library and styles utils
Our workspace has been set up, and we have the basic tooling in place.
Before we start working on the application itself, it’s a good idea to add a
component library and styles utils library which sometimes come pre-
combined in a single library…

In general, most Angular projects will use one of the 3rd party
component libraries like the official Angular Material, PrimeNG, Taiga UI
or others, but it’s also quite common that large enterprise organizations
will have enough resources to create and maintain their own in-house
component library which will be used across multiple projects.

warning It's exceedingly rare (and very inefficient) to build an equivalent


of base set of components provided by the 3rd party component
libraries as a part of the codebase of a single application. It;s
much better to pick and customize one of the open source
solutions and focus on the application-specific features which
deliver value to the users instead!

Add Angular Material


Let’s add an official Angular Material component library. We’re going to add it
with the help of ng add schematics which will guide us through the setup of
themes, typography, and animations as well as demonstrate the power of the
ng add schematics in general…

1 ng add @angular/components

164 Tomas Trajan


@tomastrajan
Hands on architecture
The schematics will download the necessary dependencies and start the
process which will prompt us about a couple of choices which we have to
make to set up the Angular Material in the workspace…
1  ng add @angular/material
2 ℹ Using package manager: npm
3 ✔ Found compatible package version: @angular/[email protected].
4 ✔ Package information loaded.
5
6 The package @angular/[email protected] will be installed and executed.
7 Would you like to proceed? Yes
8 ✔ Packages successfully installed.
9 ? Choose a prebuilt theme name, or "custom" for a custom theme: Custom
10 ? Set up global Angular Material typography styles? Yes
11 ? Include the Angular animations module? Include and enable animations
12
13 UPDATE package.json (1839 bytes)
14 ✔ Packages installed successfully.
15 UPDATE projects/example-app/src/app/app.config.ts (338 bytes)
16 UPDATE projects/example-app/src/styles.scss (1644 bytes)
17 UPDATE projects/example-app/src/index.html (525 bytes)

The chances are high that we’re going to answer Yes to all the questions,
and Custom to the theme prompt, which will allow us to customize the
theme to match colors of our organization.

The UPDATE files are the files which contain base setup required for
Angular Material to work properly. Instead of reading the
documentation, the ng add schematics did all the heavy lifting for us!

info The Angular Material custom theme allows us to even define


custom color palette and typography and use these to create
custom theme which allows for next level customization
compared to just creating custom theme by combining the out of
the box available Material palettes!

165 Tomas Trajan


@tomastrajan
Hands on architecture
The custom Material palette and theme can be defined in the styles.scss
after the @include mat-core(); line…
1 $my-org-blue-palette: ( // should be same as in tailwind.config.js
2 50: #eff6ff, // more on that in the next section...
3 100: #dbeafe,
4 200: #bfdbfe,
5 300: #93c5fd,
6 400: #60a5fa,
7 500: #3b82f6,
8 600: #4c73db,
9 700: #2852c8,
10 800: #2144a6,
11 900: #1d3c91,
12 contrast: ( // text on background
13 50: #000,
14 100: #000,
15 200: #000,
16 300: #000,
17 400: #000,
18 500: #fff,
19 600: #fff,
20 700: #fff,
21 800: #fff,
22 900: #fff,
23 )
24 );
25
26 $example-app-primary: mat.define-palette(
27 $my-org-blue-palette, // use the custom palette
28 700, // default shade
29 600, // lighter shade
30 900 // darker shade
31 );
32
33 // Angular Material palettes (out of the box)
34 $example-app-accent: mat.define-palette(mat.$blue-palette, A400, A100, A700);
35 $example-app-warn: mat.define-palette(mat.$red-palette);

In the example above, were adding one custom organization specific


color palette and using it to define the primary color of the theme while
using Material palettes for the accent and warn colors. In the same way,
we could have also used custom organization specific palettes for the
accent and warn colors for our theme!

166 Tomas Trajan


@tomastrajan
Hands on architecture
Add Tailwind
Another good addition to our setup is a CSS based utility library like Tailwind
CSS or Bootstrap which can be used to speed up the development of the UI,
especially because they provide us with grid and a lot of responsive utility
classes which are almost universally useful and can be used to build the UI in
a very consistent way!
Let’s start by installing the Tailwind CSS and its dependencies and execute
the initialization script as described in the official Tailwind CSS
documentation…
1 npm install -D tailwindcss postcss autoprefixer
2 npx tailwindcss init

Once finished, we will add a new configuration in the generated


tailwind.config.js file to define where it should look for the usage of the
Tailwind CSS classes in our Angular source…
1
2 module.exports = {
3 content: [
4 // will look in every project in the workspace
5 './projects/**/*.{html,ts,scss}',
6 ],
7 //...
8 }

Another thing we should do is to add the Tailwind related setup to the


styles.scss file

1
2 // Angular Material setup...
3
4 @tailwind base;
5 @tailwind components;
6 @tailwind utilities;

167 Tomas Trajan


@tomastrajan
Hands on architecture
With all this setup in place, we should be able to validate that everything
works by running the npm start script in the terminal and updating the
content of the app.component.html template (or inline template if you
prefer) by including the following snipped at the top of the file…
1 <div class="bg-blue-300 p-8">
2 <span class="text-blue-900 font-bold text-3xl">Hello world!</span>
3 </div>

When successful, it should look like this…

Hello world!

In case we have defined custom Angular Material color palette that is


specific to our organization, we should also make sure to replicate this
configuration in the tailwind.config.js file and keep them in sync
whenever we perform a change!

For example, assuming our previously defined custom color palette in the
styles.scss file, we should add it to the tailwind.config.js file…

1 module.exports = {
2 // ...
3 theme: {
4 extend: {
5 colors: {
6 blue: {
7 // make sure to keep in sync with custom palette styles.scss
8 50: '#eff6ff',
9 100: '#dbeafe',
10 200: '#bfdbfe',
11 // ...
12 },
13 },
14 },
15 },
16 };

168 Tomas Trajan


@tomastrajan
Hands on architecture
Final cleanup
With all this setup in place, let’s do a final cleanup of the workspace and
remove all placeholder implementation generated by the Angular CLI and our
experiments to validate setup of Angular Material and Tailwind CSS…
Let’s delete following files…

projects/example-app/src/app/app.component.html

projects/example-app/src/app/app.component.scss

projects/example-app/src/app/app.component.spec.ts

… and update the app.component.ts file to remove the placeholder


implementation…
1 import { Component } from '@angular/core';
2
3 @Component({
4 selector: 'my-org-root',
5 standalone: true,
6 imports: [],
7 template: ``,
8 })
9 export class AppComponent {}

After this changes, running the npm start script in the terminal should yield
an empty page in the browser, which is a good sign that we’re ready to start!

169 Tomas Trajan


@tomastrajan
Hands on architecture
Scaffold architecture skeleton
Our workspace is now prepared, and now it’s time to start scaffolding the
folder structure which will match the architecture rules and the best
practices which we have discussed in the theoretical part of the book.

Preparing folder structure


The first step is to prepare the folder structure which will match the
architecture rules. For this, we’re going to navigate in the file explorer of our
IDE to the projects/example-app/src/app folder and create the following
folders…

core/

ui/

layout/

feature/

pattern/

Remember that you can find in-depth explanation of the purpose of


each of these folders and related architectural types in the theoretical
part of the book!

Core
The core folder will contain the injectables and other headless logic which
can be used in the eager part of the application and re-used between
multiple lazy features and patterns.

170 Tomas Trajan


@tomastrajan
Hands on architecture
The core will also contain the root core.ts file with the base setup for our
application which would usually be defined in the app.config.ts but we
want to keep app.*.ts files as empty as possible and implement everything
in its dedicated type instead…
1 import {
2 Routes,
3 provideRouter,
4 withComponentInputBinding,
5 withEnabledBlockingInitialNavigation,
6 withInMemoryScrolling,
7 withRouterConfig,
8 } from '@angular/router';
9 import { ENVIRONMENT_INITIALIZER } from '@angular/core';
10 import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
11
12 export interface CoreOptions {
13 routes: Routes; // possible to extend options with more props in the future
14 }
15
16 export function provideCore({ routes }: CoreOptions) {
17 return [
18 // reasonable default for most applications
19 provideAnimationsAsync(),
20 provideRouter(
21 routes,
22 withRouterConfig({ onSameUrlNavigation: 'reload' }),
23 withComponentInputBinding(),
24 withEnabledBlockingInitialNavigation(),
25 withInMemoryScrolling({
26 anchorScrolling: 'enabled',
27 scrollPositionRestoration: 'enabled',
28 }),
29 ),
30
31 // other 3rd party libraries providers like NgRx, provideStore()
32
33 // other application specific providers and setup
34
35 // perform initialization, has to be last
36 {
37 provide: ENVIRONMENT_INITIALIZER,
38 multi: true,
39 useValue() {
40 // add init logic here...
41 // kickstart processes, trigger initial requests or actions, ...
42 },
43 },
44 ];
45 }

171 Tomas Trajan


@tomastrajan
Hands on architecture
Once the core setup is in place, we can update the content of the
app.config.ts file to remove everything and use the newly created
provideCore instead…

1 import { ApplicationConfig } from '@angular/core';


2
3 import { routes } from './app.routes';
4 import { provideCore } from './core/core';
5
6 export const appConfig: ApplicationConfig = {
7 providers: [provideCore({ routes })]
8 // all other gloval providers should be defined in the core.ts
9 };

Layout
Now that our core is integrated and will be used to provide the base setup for
the application, we can start working on the layout where we’re going to
create the initial main layout!

There are many different approaches to managing of layouts which we’re


discussed in the Layout chapter of the theoretical part of the book
(check it out to make an informed decision about the best layout
approach for the application), but for the purpose of this example, we’re
going to assume there is only one main layout.

Let’s use Angular Schematics to generate a new main layout component. In


our IDE we’re going to click on the layout folder and hit the previously
configured schematics key shortcut and then select component
schematics…
In the follow-up dialog, we can provide main-layout as the name of the
component and confirm the creation by clicking the Ok button.

172 Tomas Trajan


@tomastrajan
Hands on architecture
Let’s add the following content into main-layout.component.html file…
1 <mat-toolbar class="fixed shadow-lg !bg-white">
2 <div class="container mx-auto px-10">
3 <div class="flex justify-between">
4 <a
5 href="https://angularexperts.io"
6 target="_blank"
7 class="hidden md:inline"
8 >
9 <img
10 height="60"
11 width="168"
12 src="https://angularexperts.io/assets/images/logo/angular-experts.svg"
13 alt="Angular Experts Logo"
14 />
15 </a>
16 <a href="https://angularexperts.io" target="_blank" class="md:hidden">
17 <img
18 height="48"
19 width="48"
20 src="https://angularexperts.io/assets/images/logo/angular-experts-icon.svg"
21 alt="Angular Experts Logo"
22 />
23 </a>
24
25 <div class="flex items-center gap-4">
26 @for (route of ['home']; track $index) { <!-- add more routes here -->
27 <a
28 [routerLink]="['/', route]"
29 routerLinkActive
30 #rla="routerLinkActive"
31 [color]="rla.isActive ? 'accent' : 'primary'"
32 mat-flat-button
33 >{{ route | titlecase }}</a
34 >
35 }
36 </div>
37 </div>
38 </div>
39 </mat-toolbar>
40
41 <main class="container mx-auto mt-16 p-10">
42 <router-outlet></router-outlet>
43 </main>
44
45 <footer class="mt-auto p-10 bg-white">
46 <div class="container mx-auto text-center">
47 My Org
48 </div>
49 </footer>

173 Tomas Trajan


@tomastrajan
Hands on architecture
Then, in the main-layout.component.scss file, we can add following
content…
1 :host {
2 @apply flex flex-col min-h-screen bg-gray-100;
3 }

And finally, in the main-layout.component.ts file, we should add following


entities into the imports: [ ] array of the @Component decorator (if not
already there)…
1 @Component({
2 // ...
3 imports: [
4 // template context (everything we have used in the template...)
5 // Angular
6 TitleCasePipe,
7 RouterLink,
8 RouterOutlet,
9 RouterLinkActive,
10
11 // material
12 MatToolbarModule,
13 MatButtonModule,
14 ]
15 })
16 export class MainLayoutComponent { /* ... */ }

Once we have pasted the imports content into the file, we need to
make sure that they are properly imported at the top of the file…

thumb_up It's always a good idea to let the IDE do the heavy lifting for us
and use its built-in functionality to automatically import the
missing entities which will add the necessary imports at the top
of the file...

174 Tomas Trajan


@tomastrajan
Hands on architecture
With the main layout in place, we can update the app.component.ts file and
especially the imports and template property in the @Component
decorator to use the newly created main layout…
1 @Component({
2 selector: 'my-org-root',
3 standalone: true,
4 imports: [MainLayoutComponent], // add main layout to the imports
5 template: `<my-org-main-layout />`, // Angular self-closing tags are amazing!
6 })
7 export class AppComponent {}

Now it’s a great time to check out running application in the browser and see
how the main layout looks like and if everything is working as expected!

Of course, we’re going to customize it with our own organization


specific styles and assets like logo, colors, and typography, but for now,
it’s a great start!

175 Tomas Trajan


@tomastrajan
Hands on architecture
Feature
In the layout we’ve just created, we have already added button with a link to
the home route which will navigate to our first lazy feature called home . If
we tried to click on this button in a running application, we would get an error
in the console because the lazy route (and corresponding feature) were not
implemented yet!
Let’s create a new lazy feature called home in the feature/ folder by
creating a home/ folder and creating the home.routes.ts file inside it…
1 import { Routes } from '@angular/router';
2
3 export default <Routes>[]

Because of currently missing Angular Schematics support, we’re trying


to optimize for the least amount of boilerplate and are therefore using
the export default statement which will allow us to skip the .then(m
=> m.routes) part of the route definition in the parent route
configuration!

info The "routes" based lazy features are relatively new and don't have
a dedicated Angular Schematics to generate them yet. Because
of this, the proposed syntax might change in the future with
release of such schematics.

Let’s generate a first component of our newly created lazy feature by clicking
on the home/ folder, hitting the previously configured schematics key
shortcut, then selecting component schematics and generating a home
component…
176 Tomas Trajan
@tomastrajan
Hands on architecture
With the component in place, let’s update the home.routes.ts file to
include the route for the home component…
1 import { Routes } from '@angular/router';
2 import { HomeComponent } from './home/home.component';
3
4 export default <Routes>[
5 {
6 path: '',
7 component: HomeComponent
8 }
9 ]

The last piece of the puzzle is to integrate our lazy feature routes config in the
parent (in this specific case root) route configuration which is defined in the
app.routes.ts file…

1 import { Routes } from '@angular/router';


2
3 export const routes: Routes = [
4 {
5 path: '',
6 pathMatch: 'full',
7 redirectTo: 'home',
8 },
9 {
10 path: 'home', // shorter import() because of use of "export default"
11 loadChildren: () => import('./feature/home/home.routes')
12 },
13 {
14 path: '**', // fallback route (can be used to display dedicated 404 lazy feature)
15 redirectTo: '',
16 }
17 ];

The running application should display "home works!" in the middle part of
the screen!

The reason for that is that even if we were at the empty '' route, the
newly added root route configuration will redirect us to the home route
which will load the home lazy feature

177 Tomas Trajan


@tomastrajan
Hands on architecture
In the past, all this manual setup was not necessary because the NgModule
based lazy features had their own dedicated schematic which would
generate the whole feature including the route configuration, the initial
component and integration in the parent route configuration in one go!

Let’s hope that the route-based lazy features will get their own
dedicated schematics soon as well, but for now, we’re going to stick
with the proposed approach!

178 Tomas Trajan


@tomastrajan
Hands on architecture
What about "Shared Feature"?
With our first lazy feature in place, we can take a quick detour and discuss
how we would implement shared features. These are the features that can be
navigated to from multiple other lazy features and are therefore shared
between them as a sub route.

Every lazy feature can be shared out of the box and reused in multiple
other lazy features by integrating it as a sub-route within the route
configuration of the parent lazy feature!

This stands in contrast to the lazy sub features which are part of the parent
lazy feature and are implemented in parent lazy feature folder.
In that sense, a lazy sub feature can be extracted to become a standard lazy
feature in case when we need to share it with other lazy features besides the
parent lazy feature where it was implemented initially.

UI & Pattern
For now, we’re going to keep them empty and ready to implement shared
reusable UI components and patterns which can be used across multiple lazy
features when the need arises.

info Check out provided example repo to see an extended version of


this example project with multiple lazy features, shared UI
components and patterns, and more!

179 Tomas Trajan


@tomastrajan
Hands on architecture
Setup automated architecture validation
With the initial architecture skeleton in place, it’s time to set up the
automated architecture validation with the help of eslint-plugin-
boundaries which will ensure that our architecture stays clean over the
lifetime of the project!

Here we’re just going to add the rules, please check the Automated
architecture validation in the theoretical part of the book to see an in-
depth explanation of the rules, their purpose and overall setup of the
automated architecture validation!

First, let’s install the required dependencies…


1 npm i -D eslint-plugin-boundaries eslint-import-resolver-typescript

After the successful installation, we’re going to continue in the root


.eslintrc.json file and provide basic setup…

1 { // most of the configuration was skipped to focus on the boundaries plugin


2 "overrides": [
3 {
4 "files": ["*.ts"],
5 "plugins": ["boundaries"],
6 "extends": [
7 // other presets...
8 "plugin:boundaries/strict" // all files have to belong to a type
9 ],
10 "settings": {
11 "import/resolver": {
12 // recognize both static and dynamic Typescript imports
13 "typescript": {
14 "alwaysTryTypes": true
15 }
16 },
17 "boundaries/dependency-nodes": ["import", "dynamic-import"],
18 }
19 }
20 ]
21 }

180 Tomas Trajan


@tomastrajan
Hands on architecture
Let’s run ng lint to validate that everything is working as expected so far…
1 Linting "example-app"...
2 [boundaries]: Please provide element types using the 'boundaries/elements' setting
3
4 /home/tomastrajan/projects/github/my-org-workspace/projects/example-app/src/app/app.componen
5 1:1 error File is not of any known element type boundaries/no-unknown-files

If the plugin was set up correctly, the linting should run and print output
similar to the one provided above. The error is expected because we haven’t
provided the element types configuration yet and the plugin is therefore not
able to recognize the files in the workspace!
Next step is to add the boundaries/elements settings which define
architecture types and which folders and files belong to them…
1 {
2 "overrides": [
3 {
4 "files": ["*.ts"],
5 "settings": {
6 // most configuration was skipped to focus on the "boundaries/elements"
7 "boundaries/elements": [
8 {
9 "type": "main",
10 "mode": "file",
11 "pattern": "main.ts",
12 "basePattern": "projects/**/src",
13 "baseCapture": ["app"]
14 },
15 {
16 "type": "app",
17 "mode": "file",
18 "pattern": "app(-|.)*.ts", // app-routes, app.component, ...
19 "basePattern": "projects/**/src/app",
20 "baseCapture": ["app"]
21 },
22 {
23 "type": "core",
24 "pattern": "core",
25 "basePattern": "projects/**/src/app",
26 "baseCapture": ["app"]
27 },
28 // continues on the other page...

181 Tomas Trajan


@tomastrajan
Hands
1
2
on architecture
{
"type": "ui",
3 "pattern": "ui",
4 "basePattern": "projects/**/src/app",
5 "baseCapture": ["app"]
6 },
7 {
8 "type": "layout",
9 "pattern": "layout",
10 "basePattern": "projects/**/src/app",
11 "baseCapture": ["app"]
12 },
13 {
14 "type": "pattern",
15 "pattern": "pattern",
16 "basePattern": "projects/**/src/app",
17 "baseCapture": ["app"]
18 },
19 {
20 "type": "feature-routes",
21 "mode": "file",
22 "pattern": "feature/*/*.routes.ts",
23 "capture": ["feature"],
24 "basePattern": "projects/**/src/app",
25 "baseCapture": ["app"]
26 }
27 {
28 "type": "feature",
29 "pattern": "feature/*",
30 "capture": ["feature"],
31 "basePattern": "projects/**/src/app",
32 "baseCapture": ["app"]
33 },
34
35 {
36 "type": "lib-api",
37 "mode": "file",
38 "pattern": "projects/**/src/public-api.ts",
39 "capture": ["lib"]
40 },
41 {
42 "type": "lib",
43 "pattern": "projects/**/src/lib",
44 "capture": ["lib"]
45 }
46 ]
47 }
48 }
49 ]
50 }

182 Tomas Trajan


@tomastrajan
Hands on architecture
With the boundaries/elements settings in place, re-running the ng lint
command should yield a clean output without any errors or warnings!
1 ng lint
2
3 Linting "example-app"...
4
5 All files pass linting.

Great, all the files in the application are now recognized and assigned to their
respective architecture types.

info The plugin:boundaries/strict preset requires that every


file within a validated application has to belong to a
corresponding defined architecture type.

This is the reason why we have defined also helper types like main and app
which are used to capture the files which are part of the base setup of every
Angular application.
These helper types are consuming the files from the other architecture types,
for example core is consumed in app.config.ts or layout is consumed
in app.component.ts and therefore their relationship has to be defined as
well!
The application folder often contains also files which are not part of the
implementation itself but are used to configure additional tooling, especially
unit and end-to-end testing.
Because of the plugin:boundaries/strict preset, these files have to be
accounted for as well, and we have to either create a new architecture type
for them, e.g. testing or add them to the "boundaries/ignore": [] array.
183 Tomas Trajan
@tomastrajan
Hands on architecture
Example of boundaries/ignore configuration for application which uses
Jest testing framework and especially Typescript-based Jest configuration…
1 {
2 "overrides": [
3 {
4 "files": ["*.ts"],
5 "settings": {
6 "boundaries/ignore": [
7 "**/jest(.|-)*.ts",
8 ]
9 }
10 }
11 ]
12 }

Now that we know how we could account also for additional files that can
become part of the application, we can continue with the last step of
automated architecture validation, which is the setup of the rules…
1 {
2 "overrides": [
3 {
4 "files": ["*.ts"],
5 "rules": {
6 "boundaries/element-types": [
7 "error",
8 {
9 "default": "disallow",
10 "rules": [
11 {
12 "from": "main",
13 "allow": [["app", { "app": "${from.app}" }]]
14 },
15 {
16 "from": "core",
17 "allow": [["lib-api"], ["core", { "app": "${from.app}" }]]
18 },
19 {
20 "from": "ui",
21 "allow": [["lib-api"], ["ui", { "app": "${from.app}" }]]
22 },
23 // continues on the other page...

184 Tomas Trajan


@tomastrajan
Hands on architecture
1 {
2 "from": "layout",
3 "allow": [
4 ["lib-api"],
5 ["core", { "app": "${from.app}" }],
6 ["ui", { "app": "${from.app}" }],
7 ["pattern", { "app": "${from.app}" }]
8 ]
9 },
10 {
11 "from": "app",
12 "allow": [
13 ["lib-api"],
14 ["app", { "app": "${from.app}" }],
15 ["core", { "app": "${from.app}" }],
16 ["layout", { "app": "${from.app}" }],
17 ["feature-routes", { "app": "${from.app}" }]
18 ]
19 },
20 {
21 "from": ["pattern"],
22 "allow": [
23 ["lib-api"],
24 ["core", { "app": "${from.app}" }],
25 ["ui", { "app": "${from.app}" }],
26 ["pattern", { "app": "${from.app}" }]
27 ]
28 },
29 {
30 "from": ["feature"],
31 "allow": [
32 ["lib-api"],
33 ["core", { "app": "${from.app}" }],
34 ["ui", { "app": "${from.app}" }],
35 ["pattern", { "app": "${from.app}" }],
36 ]
37 },
38 {
39 "from": ["feature-routes"],
40 "allow": [
41 ["lib-api"],
42 ["core", { "app": "${from.app}" }],
43 ["pattern", { "app": "${from.app}" }],
44 ["feature", {
45 "app": "${from.app}", "feature": "${from.feature}"
46 }],
47 ["feature-routes", {
48 "app": "${from.app}", "feature": "!${from.feature}"
49 }]
50 ]
51 },

185 Tomas Trajan


@tomastrajan
Hands on architecture
1
2 {
3 "from": ["lib-api"],
4 "allow": [["lib", { "app": "${from.lib}" }]]
5 },
6 {
7 "from": ["lib"],
8 "allow": [["lib", { "app": "${from.lib}" }]]
9 }
10 ]
11 }
12 ]
13 }
14 }
15 ]
16 }

With the rules in place, the ng lint command should run without any errors
or warnings.
1 ng lint
2
3 Linting "example-app"...
4
5 All files pass linting.

thumb_up The automated architecture validation is now fully set up and


ready to ensure that the architecture of the application stays
clean and consistent over the lifetime of the project!

As with every test, and yes, the automated architecture validation is a test of
the architecture, it’s always a good idea to try to break it and see if it catches
the issues. That way we can have full confidence in its capabilities!
To do that, let’s import the HomeComponent in the main-
layout.component.ts by adding it to the imports: [ ] array of the
@Component decorator…

186 Tomas Trajan


@tomastrajan
Hands
1
2
on architecture
import { HomeComponent } from '../../feature/home/home/home.component';

3 @Component({
4 selector: 'my-org-main-layout',
5 standalone: true,
6 imports: [
7 // other imports...
8 HomeComponent // <-- add to the imports
9 ],
10 templateUrl: './main-layout.component.html',
11 styleUrl: './main-layout.component.scss',
12 changeDetection: ChangeDetectionStrategy.OnPush
13 })
14 export class MainLayoutComponent {}

Now running the ng lint command should yield an error similar to the one
provided below…
1 ng lint
2
3 Linting "example-app"...
4
5 /home/tomastrajan/projects/github/my-org-workspace/projects/example-app/src/app/layout/main
6 7:31 error No rule allowing this dependency was found. File is of type 'layout'
7 with app 'example-app'. Dependency is of type 'feature' with app 'example-app'
8 and feature 'home' boundaries/element-types

More so, the error should be visible in the IDE as well, to provide immediate
feedback!

187 Tomas Trajan


@tomastrajan
Hands on architecture
Great, the automated architecture validation is working as expected and
will from now on ensure that the architecture of the application stays
clean and consistent!!

Adding more types and rules


Previously, we’ve already mentioned that the plugin:boundaries/strict
preset requires that every file within a validated application has to belong to
a corresponding defined architecture type or be added to the
boundaries/ignore array.

Let’s illustrate a process of adding a new architecture type and related rules
with an example of adding a new env type which will be used to capture the
Angular environments files which were previously generated out of the box
with every new Angular CLI workspace, but nowadays are only added if we run
the ng g environments schematics…
1 ng g environments
2
3 CREATE projects/example-app/src/environments/environment.ts (31 bytes)
4 CREATE projects/example-app/src/environments/environment.development.ts (31 bytes)
5 UPDATE angular.json (3726 bytes)

Once the environments are generated, we can re-run the ng lint command
to see that the environments files are not recognized and are therefore not
part of the automated architecture validation…
1 ng lint
2
3 Linting "example-app"...
4
5 /home/tomastrajan/projects/github/my-org-workspace/projects/example-app/src/environments/env
6 1:1 error File is not of any known element type boundaries/no-unknown-files
7
8 /home/tomastrajan/projects/github/my-org-workspace/projects/example-app/src/environments/env
9 1:1 error File is not of any known element type boundaries/no-unknown-files

188 Tomas Trajan


@tomastrajan
Hands on architecture
Let’s add the env type to the boundaries/elements settings in the
.eslintrc.json file…
1 {
2 "overrides": [
3 {
4 "files": ["*.ts"],
5 "settings": {
6 "boundaries/elements": [
7 {
8 "type": "env",
9 "pattern": "environments",
10 "basePattern": "projects/**/src",
11 "baseCapture": ["app"]
12 }
13 // other elements...
14 ]
15 }
16 }
17 ]
18 }

Re-running the ng lint command should yield a clean output without any
errors or warnings!
1 ng lint
2
3 Linting "example-app"...
4
5 All files pass linting.

Now we still need to define the rules for the env type in the
.eslintrc.json file…

We don’t want env to be able to consume any other architecture type,


so we won’t define any rules for the env type itself, but at the same
time, every other type should be able to consume the env type so we
have to add it to the allow array of every other type!

189 Tomas Trajan


@tomastrajan
Hands
1
2
{ on architecture
"overrides": [
3 {
4 "files": ["*.ts"],
5 "rules": {
6 "boundaries/element-types": [
7 "error",
8 {
9 "default": "disallow",
10 "rules": [
11 {
12 "from": "core",
13 "allow": [
14 ["lib-api"],
15 ["env", { "app": "${from.app}" }], // <-- add here
16 ["core", { "app": "${from.app}" }]
17 ]
18 },
19 // same for every other rule
20 // which should be able to consume the "env" type
21 ]
22 }
23 ]
24 }
25 }
26 ]
27 }

thumb_up Adding of additional architecture types and related rules is


relatively simple task once we have the overall setup in place.
This makes it easy to extend the architecture validation system to
the specific needs of any given organization or team!

Great, now we have built a solid understanding and foundation for the
architecture of the application and have set up the automated
architecture validation to ensure that it stays clean!

With all this in place, we’re now ready to start building the actual application
and implementing the features and patterns which will make it unique and
valuable to our users!
190 Tomas Trajan
@tomastrajan
Hands on architecture
Example project
The book comes with an example project repository which contains the full
implementation of the architecture skeleton and the automated architecture
validation setup together with a couple of feature and pattern examples and
stubs for UI components.

info Example project is a great starting point for many new Angular
projects, and especially those which were planning to use
Angular Material and Tailwind CSS as the UI and utility libraries in
the first place!

Both eBook and example project repository are living documents and
will be updated over time, see release history. Everyone who purchased
the eBook will then receive email notification about future releases and
will be able to download the latest version!

191 Tomas Trajan


@tomastrajan
Growing workspace and collaboration
Adding other apps and libs
Additional applications and libraries can and should be added in the same way
as the example-app with the help of Angular Schematics!

When using schematics to generate new apps and libs, the linting setup
will be automatically added to the new projects and will be ready to use
without any additional effort. This is because the architecture validation
is set up on the root level and is therefore inherited by all the projects in
the workspace!

It is important to acknowledge that there will always be exceptions and edge


cases that will require additional setup and configuration. At the same time,
the content of this book represents a great baseline for building for the
most of new Angular applications today.

info The proposed approach and architecture will work really well in a
small-to-medium size workspaces with up to 10 apps and libs (but
this is highly dependent on their individual size).

In general, as long as the build and test don’t represent a noticeable


bottleneck, there is no need to switch to a more powerful but at the same
time more complex tooling like NX monorepos.

192 Tomas Trajan


@tomastrajan
Growing workspace and collaboration
Collaborating with multiple teams
Proposed architecture can support an environment where multiple teams are
working on the same workspace and are building multiple apps and libs.

The isolation between all main architecture types and applications


themselves will allow for a great level of independence and autonomy
between the teams who can focus on their respective part of the
application without stepping on each other’s toes!

info When working with large (or multiple) teams, it might be very
beneficial to use CODEOWNERS file to define the owners of the
different parts of the workspace and to ensure that when
developers need to make changes in the parts of the workspace
that are shared between multiple teams (this can be on level of
application but also on a level of individual feature) the resulting
pull request will be reviewed by the right people!

193 Tomas Trajan


@tomastrajan
Wrap up
We have reached the end of the book and the journey of building a new
Angular application! I hope that you have enjoyed the ride and that you
have learned a lot about the architecture and will be able to apply the
knowledge to your own projects!

If you enjoyed this book, please consider recommending it to your friends


and colleagues who might also benefit from the knowledge and the example
project repository!
It would be very appreciated, and I’ll be extremely grateful for every
recommendation in form of…

tweet (or newly post on X) - a specific quote from the book, an individual
page with content you found valuable or just a general recommendation
testimonial - using this super short form, please provide a single paragraph
or a couple of sentences about what you enjoyed about the book
(preferably with your profile picture), name, role and your location (e.g.
USA or Thailand) and a consent to post it to the book website

Besides, do not hesitate to reach out to me with any questions, feedback or


suggestions for additional content or improvements to the book or the
example project repository! You can reach me at @tomastrajan or via email at
[email protected]

194 Tomas Trajan


@tomastrajan
Wrap up
Make sure to check out and subscribe to our free blog at AngularExperts.io
where we’re regularly publishing new content about Angular, NgRx, RxJs and
other related topics!
Also, check out our offer of Angular, NgRx and RxJs trainings and consulting
services to empower your team with the right knowledge to deliver
exceptional experiences to your customers.

195 Tomas Trajan


@tomastrajan
Links
The eBook and example project
eBook & Example project — https://angularexperts.io/products/ebook-
angular-enterprise-architecture/purchase-success — the links can be
accessed by providing the email used for the purchase
Release history — https://angularexperts.io/products/ebook-angular-
enterprise-architecture#releases
Submit testimonial — https://angexp.link/testimonial

References from the content


JavaScript Module Syntax — https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Guide/Modules
Angular Template Context (blog) —
https://angularexperts.io/blog/angular-template-context
Angular @defer Guide (blog) — https://angularexperts.io/blog/angular-
defer-lazy-loading-total-guide
Angular Named Outlets (docs) — https://angular.io/guide/router-tutorial-
toh#displaying-multiple-routes-in-named-outlets
Angular CoreModule Standalone Migration (blog) —
https://angularexperts.io/blog/angular-core-module-standalone-
migration
Angular Actively Supported Versions (docs) —
https://angular.dev/reference/versions#actively-supported-versions
Angular Update Guide — https://update.angular.io/
196 Tomas Trajan
@tomastrajan
Links
References from the content
Tailwind CSS for Angular — https://tailwindcss.com/docs/guides/angular
Prettier configuration (docs) —
https://prettier.io/docs/en/configuration.html
Prettier options (docs) — https://prettier.io/docs/en/options.html
Prettier pre-commit (docs) — https://prettier.io/docs/en/precommit.html

Tools and libraries


Esbuild Bundle Size Analyzer — https://esbuild.github.io/analyze/
madge — https://www.npmjs.com/package/madge
Graphviz — https://www.graphviz.org
VS code — https://code.visualstudio.com
Webstorm — https://www.jetbrains.com/webstorm
Omniboard — https://omniboard.dev

197 Tomas Trajan


@tomastrajan
Links
Contact and social
Email — [email protected]
Angular Experts — https://angularexperts.io
Angular Experts Blog — https://angularexperts.io/blog
X (previously known as twitter) — https://x.com/tomastrajan
LinkedIn — https://linkedin.com/in/tomastrajan
GitHub — https://github.com/tomastrajan
YouTube — https://www.youtube.com/@tomastrajan
GDE — https://g.dev/tomastrajan

Offered products, workshops and services


Angular Project Review — https://angularexperts.io/services/angular-
project-review
Getting Reactive with RxJs Workshop —
https://angularexperts.io/workshop-rxjs
Angular State Management with NgRx Workshop —
https://angularexperts.io/workshop-ngrx-state-management
Mastering Angular Signals (eBook) —
https://angularexperts.io/products/ebook-signals
WebStorm Skol Theme — https://angularexperts.io/products/skol

198 Tomas Trajan


@tomastrajan
Resources
Following pages contain easy to print diagrams which can be used as a
reference for the architecture described in this book.
This can prove useful when first familiarizing yourself with the described
architecture and can serve as a reference when presenting the architecture
to your team or planing a new feature for your project!

199 Tomas Trajan


@tomastrajan
200
najartsamot@
najarT samoT
201
najartsamot@
najarT samoT
202
najartsamot@
najarT samoT
203
najartsamot@
najarT samoT
204
najartsamot@
najarT samoT
205
najartsamot@
najarT samoT
206
najartsamot@
najarT samoT
207
najartsamot@
najarT samoT

You might also like