This post is part of the ongoing Angular Architecture series, where we cover common design problems and solutions at the level of the View Layer and the Service layer. Here is the full series:
- View Layer Architecture - Smart Components vs Presentational Components
- View Layer Architecture - Container vs Presentational Components Common Pitfalls
- Service Layer Architecture - How to build Angular apps using Observable Data Services
- Service Layer Architecture - Redux and Ngrx Store - When to Use a Store And Why?
- Service Layer Architecture - Ngrx Store - An Architecture Guide
Angular Store Architectures
Did you ever wonder what are the benefits of building an application using a centralized store solution, either in Angular or in any framework in general?
Often we dive straight into Actions, Reducers and the associated terminology of store architectures, but those concepts (although essential) are simply means to an end.
The centralized store architecture is really a set of application design patterns, so probably the best starting point is to be aware of the design intention itself, and what problems the architecture helps solve and how.
By understanding that first, we are much less likely to run the risk of misusing the architecture or using it without making the most out of it.
The store architectural shift
Using a store architecture represents a big architectural shift: with the arrival of single page applications, we moved the Model to View transformation from the server to the client.
Store architectures have taken that one logical step further: we are now also moving the Model to View Model Mapping from the server to the client.
But what benefits does that architectural shift bring, how does it work and how do we tie all these pieces together: Ngrx Store, Actions, Reducers, Selectors, Effects ?
That's what we will be covering in this series of posts.
Table of Contents
This post will cover the following topics:
- The origins of store solutions, the original Flux Facebook chat problem
- The root of the Facebook counter problem - Model vs View Model
- Facebook Chat Problem 1 - Multiple view models for the same business data
- Facebook Chat Problem 2 - the shared data ownership/data encapsulation problem
- Facebook Chat Problem 3 - Avoiding Ajax "spinner" applications
- Solving the Facebook Counter Problem - The Store Pattern In A Nutshell
- The Store, Actions, Reducers and Selectors and how they solve the Facebook counter problem
What is the best place to get started with Store Solutions?
Let's go back to the beginning and to the origins of store architectures: We will go over the original Facebook counter problem that originated store architectures and have a look at what was the problem and how it was fixed.
The Original Facebook Chat Bug that originated Flux
A great place to start getting into the Flux Architecture and store solutions is the original talk where Flux was initially presented because it goes over the original problem itself.
There is a great story behind it: it turns out that everything originated in one big bug that was very visible to Facebook users: a problem with the unread messages counter. Actually, you might remember about it, if you have been using Facebook for a while.
It wasn't so much a bug as it was a limitation in the architecture that was in place before Flux, as we are about to see.
What was the Facebook counter bug?
The problem was that the unread messages counter was systematically displaying incorrect results: users would see for example one unread message, then they would click on the counter and all the messages had already been read:
Just a simple bug?
To anyone other than a developer, this would look like a simple problem to fix. But we have probably all run at some point into this kind of issue: where we keep fixing an apparently small bug but the problem keeps coming back in multiple variants.
And this is what happened to the Facebook team: they kept fixing the problem over months and somehow the same bug kept coming back. Either new variations of the bug were found, or the introduction of new features would cause the bug to reappear.
How did they fix the problem?
It turns out that to solve this problem the Facebook team had to change significantly the architecture of the application. And they shared their new design in an early Flux talk.
The introduction of the Flux architecture starts at 10:19 and it really is a must watch for anyone looking for information on store architectures.
The original talk goes over stores, dispatchers, actions and all the elements of the Flux architecture. But it's also very interesting to see that the talk covers mostly how the problem was solved, but not as much the causes of the actual problem itself.
We know that there was a recurring bug with a counter, and that Flux solved it and that Flux is based on a few main concepts. But what was causing the problem in the first place?
So what was the problem?
Was it just a bug in the counter right? so why was it so hard to implement? The talk gives several hints. One of the key sentences is that to fix the problem they needed a solution that:
..."brings more real data to the client-side, less derived data".
The introduction of Flux fixed the issue and the problem with the counter did not return. So what does this mean: What was the original problem, and in which way did Flux solve it?
The Facebook counter problem is common to many early single page applications
At its root, the Facebook counter problem is closely linked to a fundamental distinction that exists in any user interface that we will ever build.
Every UI application has either implicitly or explicitly two types of data:
- a Model, or Domain Model
- A View Model
For example, this is a simplified version of the Facebook chat application (two users are using the application in separate machines):
By looking at the user interface, we can see that there are 3 types of data in this application:
If we are building our program in Typescript, these data types correspond to the definition of the following 3 custom types:
Participant are plain POJOs, while
Thread contains a property
participants which is a map: the key is a participant Id and the value is the number of unread messages for that participant in that thread.
- if nowhere else in the terminology that the development team uses to talk about the program
- in the written functional specification documents of the project
It really helps to be able to use Typescript to define these 3 types at the level of the language itself and use them to build the program.
But these types are not what we see on the screen, or are they?
These types are very closely related to what we usually use to define the database data in our application. If we would be using a SQL database, this could almost be the definition of 3 database tables.
Also if we would be using a NoSQL database like Firebase and chose to store the denormalized View Model on the database, even then these 3 domain model notions would still exist either directly in the program or implicitly in the project documentation.
It's very convenient to write our program around the very domain model notions that we use to talk about it. There is just one problem: those types do not correspond directly to what we see on the screen.
Model vs View Model
What we see on the screen is not the Model, its actually the View Model - this is an important difference. Sometimes the View Model has a direct correspondence with the model - but most often they are two different models.
For example, in the application above this would be an example of a couple of View Models:
(Notice the VM suffix that stands for View Model)
ThreadSummaryVM is a good example of how the View Model is very close to the Model itself, but it's not exactly the same thing. This View Model corresponds to the list of threads on the left side of the screen:
But each entry in that list does not correspond to a
Thread Model instance. Actually, it corresponds to a combination of:
Participantinstances, because we can see their list of names in a comma separated list
- the current participant, and the information about if he read or not a thread (unread threads show up in red in the list)
Messageinstance (the last message of the thread)
There is also one extra bit of state which is purely UI-only: which thread is currently selected. In this case, the first one on the list which is highlighted in blue.
This UI-only state (the currently selected thread Id) is also used by the messages list component to the right, to know which messages to display. More on this UI state later, for now, let's focus on the Domain Data.
What is the View Model similar to?
Continuing the analogy to a SQL database, the 3 custom types above defined as interfaces would correspond to the output of a SQL query that joins 3 tables.
We don't need to use Typescript custom types to define the View Model, but doing so really makes it very clear that its different than the Model.
An Example of Model to View Model distinction
To better understand the distinction, here is the Model Data for the first thread that we see on the screen:
This is all the data that relates to the first thread - including all its messages and participants.
What does a View Model look like?
But what we actually see on the threads list on the left of the screen is a View Model (the
ThreadSummaryVM), which is a particular view of the thread:
As we can see, although the two data structures are similar, they are not exactly the same: The View Model is a transformation of the model, its a query done on the model.
For example, we now that the
read flag is set to true because we queried the Model data
dbMessagesQueuePerUser, and found that there were no messages to be read by the currently logged in user with id 1.
So what does this have to do with the Facebook counter?
It might not be immediately apparent, but in the Facebook chat situation one of the main problems that we face while implementing it is that there are multiple View Models of the same Model displayed on the screen at the same time, including:
- the list of thread summaries
- the list of messages of the currently selected thread
- And the unread messages counter!
The counter is itself a View Model, it's also a transformation of the Model. The counter is an aggregated view that reflects information that is obtained from the Participant plus all the multiple threads and messages that it has access to.
The counter is a summarized view of the Model that needs to be kept in sync with the rest of the view models in the application, and this leads us to the first problem that it's hard to solve without introducing a store.
Facebook Chat Problem 1 - Multiple view models for the same business data
In user interfaces where we need to display multiple View Models of the same domain data (the Model), how are we going to keep everything in sync?
Each View Layer top-level component (the list of threads, the list of messages or the counter) needs to display a given View Model of the data, and these multiple views need to be kept consistent at all time.
Not every user interface will have this problem, also not all the screens of an application will likely have this problem. But the Facebook home page with the chat window opened and the unread messages counter on top definitively has that issue.
But is this the only fundamental design constraint of the Facebook counter bug situation?
What if this was a read-only application?
In that case, we would just need to load the data, build its multiple View Models and display them and that would also solve the issue.
But another problem is that the Facebook chat is an application that modifies the data constantly, via for example the following operations:
- If the user sends a new message, the message gets added to the current list
- the UI is continuously receiving new messages from the backend, via server push
- the new messages arriving are reflected both in the unread messages counter and on the thread list ( the last message is updated, the unread thread is marked in red)
- when the user clicks on a thread, all messages in the thread are marked as red which affects the message counter
So this means that multiple parts of the applications are:
- constantly modifying the same domain Model data
- at the same time displaying different View Models of the data on the screen
Which leads us to the second problem that the Facebook counter situation has.
Facebook Chat Problem 2 - the shared data ownership/data encapsulation problem
If multiple parts of the application need to modify the same data, then which part of the application owns the data? Is it a situation of shared ownership or do the multiple components keep local copies of the data, and inform each other on each modification?
Can they share references to the same data and mutate it directly? Let's see these options one by one.
Keeping local copies of the data?
Keeping copies of the data at the level of each component that needs to generate a view model wouldn't work, it does not scale well in complexity.
We would soon end up with event spaghetti where all the multiple parts of the application need to notify each other that they modified something.
Sharing references to the same data and mutating it?
This does not work either because it introduces indirect couplings between the multiple parts of the application.
We can no longer reason about one component by just looking at the component and its template - we loose locality.
This is because the component data is being modified directly by a completely unrelated part of the application, and as we add more features that mutate the data we will end up with all sorts of edge cases that don't quite work.
And that is definitively also a problem that is happening in the situation of the Facebook counter problem.
An ancient problem going all the way back to OO programming
The problem is: who owns the modifiable data, how to encapsulate modifiable data and keep it private to only one part of the program and still make it visible to other parts of the application?
Or should we expose it and make it Immutable? That would also prevent shared ownership error situations. But there is one-third problem at stake that does not relate to building a maintainable program, but to the user experience.
Facebook Chat Problem 3 - Avoiding Ajax "spinner" applications
There would be one simple solution to the problems presented above: we could simply get the View Model directly from the backend, which is actually the case for most applications written today.
So the Thread Section, the Message List, and the Counter components could get their View Models via separate calls or one large common call to the backend.
And then if some modification of the data occurs: send a request to the backend, and simply refresh everything - this way all the data would be consistent.
What is the problem with this approach?
This would lead to those very early types of Ajax "Spinner" single page applications where the data is constantly being reloaded all the time with each user action, which is not a good user experience, especially on mobile.
This is especially true for a large consumer facing application like Facebook, where user experience is at a premium.
Summary of the Facebook counter problem
At this point, we have a good view on what the problem was in the Facebook application: the Facebook home screen + chat needed to display multiple View Models of the Same Model data at the same time on the same screen.
That data was not owned by any particular part of the application and was being modified constantly by multiple independent actors (the server via server push and the user).
Every View Model needed to be kept in sync and consistent without refreshing everything all the time.
The main problem with the Facebook counter bug situation
The main problem was that the usual pre-store solutions of transforming the Model into the View Model on the server and having the backend return View Models to the client do not work well in this situation.
Let's now present the Store application architecture pattern as a solution to these multiple problems - and it works really well. The store provides an elegant and maintainable solution for all the problems faced in the Facebook counter problem.
The Centralized Store Design Pattern In a Nutshell
If we go back to the Flux talk, let's take the key sentence "bring more real data to the frontend". What does this mean? This is actually the core of the store solution.
It means that instead of transforming the Model into the View Model on the server as its usually done, let's instead copy the Model to the client-side, keep it an in-memory client database and derive View Models last second on the client side instead of doing so upfront on the server.
Let's break this design down in more detail, and show some code for each part.
How is the data handled?
- we are going to create a client side in-memory database for the application data
- we are going to bring the real domain data, the Model to the frontend and not the multiple View Models that the UI needs
- we are going to keep that Model data which is really a user-specific slice of the database (usually paginated) in that in-memory database
- we are going to put that client-side in-memory database inside a centralized service that we will call a Store
- we are going to ensure that the centralized service owns the data, by either ensuring its encapsulation or exposing it as immutable
- this centralized service will have reactive properties, we can subscribe to it to get notified when the Model data changes
How are View Models produced?
This is how the view layer will be structured in this store design:
- each component that needs to produce a View Model of the data will subscribe to get new versions of the in-memory database data
- each component, will upon reception of the Model data produce its own View Model, last second at the level of the component itself
- this will ensure that all View Models are always in sync with the centralized data Model
- The transformation from Model to View Model is done last second on the View Root itself, and not on the server
- The transformation from view to view Model is done via a function called a Selector - the input of a Selector is the Model, and the output is the View Model
How is the data modified?
This is how we can modify the data in this store design while keeping all views in sync:
- the data can only be modified inside the centralized service by the Store itself
- if a component wants to trigger modifications of the data, it sends a message to the centralized service, under the form of a command payload which is known as an Action object
- The emitter of the action does not know which parts of the application will be affected by the action, there is no tight coupling between the multiple View components involved
- The Action contains all the information necessary to trigger a sequence of operations that will result in modification of the Model data
- A new version of the application Model state is produced by taking the previous state and the action and applying it a pure function called a Reducer
- The Reducer function can be split into multiple smaller functions, each one modifying a part of the state
- When the new state Model is available, it gets broadcasted to all interested components, which will then transform it into their View model
- the new state model is frozen before getting broadcasted so that the subscribers cannot modify it
What data is kept inside the Store?
The store contains not only the Model data, it also contains any UI specific global state that only exists on the client-side, such as:
- the currently selected thread ID
- the data of the current user
Was this the exact solution used by Facebook?
Facebook originally used a solution where multiple stores were used that could wait for each other to get notified and contained one subset of the data.
The design above only uses one centralized store (this design was made popular by Redux) so it's already an evolution towards the solution used by Facebook. But the essence of the solution is the same.
So let's break this design down step by step and see how each design element looks like.
Installing a store solution
Let's start by installing ngrx store in our application:
npm install --save @ngrx/store @ngrx/core
This will allow us to define a client-side in-memory database for our application. The first thing that we want to define is, what type of data will the store keep.
Defining the shape of the Application State
After installing the store, its a good idea to define a custom type called
As we can see, the contents of our in-memory database can be divided into two parts: some state is purely related to the UI, and we define it as a custom type named
This state consists of the user currently logged-in in our chat application, the currently selected thread on the thread list and the current error message displayed if any.
As we can see this state is completely different than the data of the threads, that is defined in the
StoreData custom type:
Notice that the data is not organized in arrays, but in maps. For these maps, the key is a number that corresponds to the Id field of the element stored in the map. For example, this is what the participant's data would look like:
On a first look, this way of storing the data might look like repetition, because we are repeating the Id twice: once in the key of the map and the other in the object itself.
This way of storing the data is optimized to make the data as shallow as possible and avoid deep nesting and is optimized for "find by Id" queries. This way of storing the data closely resembles a database table in a SQL database, where the primary key is the Id field of the object.
These data types define what the data contained inside the store will look like, but what about the initial value of the data?
Defining the initial value of the store state
Besides defining the structure of the in-memory data, we can also define the initial state of each data type in the following way:
This will be the initial value of the in-memory data, while we don't load the store with backend data. In the case of the chat application, we are going to load the store with the content of an initial request to the backend.
How to populate the in-memory database?
At this point what we want to do is to get the data via a plain HTTP request from the backend and save it inside the store.
The way that we interact with the store is via sending it a command for modifying its internal data in a certain pre-defined way. That command object is called an
Action. The action will trigger a modification of the store state in a synchronous and immediate way.
What about asynchronous actions? That will also be covered in part 2. Right now we already have the data ready to be loaded in the store, so how do we load it?
We start by defining an
The action contains both a type and a payload. The payload is in this case a transfer object data structure that corresponds to the data that was fetched from the backend in an initial backend request:
If we have a backend service that allows us to return the data from the backend, here is how we dispatch it to the store:
As we can see, we are sending a command object with all the data needed to the store centralized service. But we don't know what other parts of the application will be affected by this change and how.
But what will the store do with this action, how will it save the data?
How does the store process each action?
Remember, the goal of the action is to modify the state inside the in-memory database in a certain way.
Each time that the store receives a new action, it will take the current state plus the action and use both these inputs to produce a new version of the application state.
This new state is produced via a reducer function. The reducer function is a pure function (meaning it has no side effects) with a signature similar to this one:
This function is called a reducer because it shares an identical signature to the
reduce functional programming operation, and it's simply a way of producing a new store state in response to the dispatching of an action.
So what does this function look like?
Remember, reducer functions are only a small piece of the puzzle of the design that we are trying to implement. What we are doing here is bringing the Model data to the frontend and keeping it in memory, and the reducer is a way to modify that data in a maintainable way.
There are many ways to write reducer functions, one common way is to split it up into smaller functions, one affecting each type of state that is stored in the store. For example, this could be a valid store reducer function, that would delegate the creation of new state to smaller functions:
The output of this object is as expected an instance of
ApplicationState, that builds its
storeData properties by delegating their calculation to smaller functions.
Let's have a look at what one of these smaller reducer functions would look like:
The reducer function usually has a switch statement, that can be on the action type. Inside it, we are going to add processing logic which is specific of a given action type: in this case we are loading all the data into the store by saving in the corresponding maps per Id.
In order to save the data that we received from the backend per Id, we are using the Lodash
keyBy utility function.
This function would take the data from the backend and initialize the in-memory database. But how do the multiple View layer components consume this data?
How to consume the data inside the in-memory database?
If a given component in the application wants to be notified whenever new data is available, the simplest way is to have the store centralized service injected via the constructor.
The store is seen by the View layer as an observable of application state. Let's review the implementation of a component that consumes the store data in reactive style:
So what is going on in this
ThreadSection component? Let's break this down:
- this is a top-level component that gets a service injected via its constructor
- this type of component is also known as a smart component because it's aware of the service layer of the application and how to use it
- the only service that this smart component gets injected is the store service itself, no other service needs to be injected into the View layer
- the goal of the component is to take the store and define a series of data streams that are derived from the application state
- all the member variables are observables, that were derived from the store itself using the select operator
- All observable member variables like
threadSummaries$emit View Models, and not Model data
- this component does not access the application data directly, it does not have direct references to the application data
- the component interacts with the remainder of the application by dispatching store actions, so the store acts essentially as a View layer facade service that ensures that the
ThreadSectioncomponent stays decoupled from the rest of the application
- the streams of data that were configured as members variables are passed to the template and consumed using the
As you can see, the store is handled as an observable of application state and is used to derive streams of View Models. View Models are being derived from the Model data in the store at the very last second before injecting the data into the view layer.
How to produce View Models from the application state?
The store Model data (saved under the
storeData property inside
ApplicationState) is transformed into a View Model by applying a Selector function.
The Selector function takes as input the application state and returns from it a specific View Model. For example this functions returns the unread messages counter:
As we can see, the function is simply taking the application state, retrieving the current user and going through all threads in the in-memory database.
It's then summing all the threads that have unread messages for the current participant, and it's producing a total number of unread messages.
ThreadSection component is also a good example of a container component: it sets up a series of data streams and passes the actual data to a tree of local components using the
One of the components of the local tree of components is the
thread-list component, let's have a look at it:
As we can see, this component is built in a very different way as a smart component. This type of component is known as a presentational component, let's break down here how this component works:
- The main design goal of this component is to display some data
- the data is received synchronously via an
- the component can emit events to its parent component using
- the component is more reusable because it can be used in other places of the application to display different lists of threads
- the component only knows that it receives a list of threads and how to display it, it does not know how to retrieve the data from the backend
And so these are the essential elements of the centralized store design, and how they fit together in the overall design goal.
How does this solution solve the Facebook counter problem?
Let's remember that its all about creating an in-memory client side database which is a user-specific slice of the database, and use that data to derive View Models from it on the client.
This elegantly solves the Facebook counter problem:
- the multiple views of the data are always in sync by design
- the ownership of the shared data is moved to a centralized service
- the user experience is improved because we don't have to refresh the application constantly
And with this, we have covered the essential parts of the centralized store pattern.
Much more than a set of libraries, the Ngrx ecosystem is really a set of application design patterns: Stores, Actions, Reducers, Selectors those are all means to implement those patterns.
To better understand those concepts and know how they all fit together we propose to start by learning the centralized store pattern itself first, and then drill down from there into the specific implementation details.
All the store related concepts like Actions, Reducers, etc. are simpler to get into if we approach them as means to implement an overall application design.
The centralized store design is very effective at solving many problems that are hard to impossible to solve otherwise, and it does so by introducing some architectural trade-offs.
To see a sample application of the store pattern in action using ngrx store and many other libraries of the Ngrx ecosystem, have a look at the latest Ngrx sample application.
Stores and backend design
Stores are not only about the client though, using an in-memory database that stores real Domain data also opens up the possibility of building more reusable backends, that emit Model data instead of View models.
A store is a great combination for GraphQL backends for example, where we can define with each request what exact Model data we need - the endpoint does not return a pre-defined data structure, unlike traditional REST endpoints.
That would mean that we could build only one single backend for our system, used by both the UI and also third-party services, which is currently not the most common situation.
Usually we end up building a UI-specific backend and a separate REST backend for third-party integrations.
By moving the mapping from Model to View Model from the server to the client and giving query language capabilities to our backend we could now instead build one single reusable backend used by both the UI and third parties.
I hope you enjoyed the post, I invite you to have a look at the list below for other similar posts and resources on Angular.
I invite you to subscribe to our newsletter to get notified when more posts like this come out:
And if you would like to learn a lot more about NgRx, we recommend checking the NgRx with NgRx Data - The Complete Guide course, where we will cover the whole NgRx ecosystem in much more detail.
If you are just getting started learning Angular, have a look at the Angular for Beginners Course:
Other posts on Angular
If you enjoyed this post, have also a look also at other popular posts that you might find interesting:
- Angular Router - How To Build a Navigation Menu with Bootstrap 4 and Nested Routes
- Angular Router - Extended Guided Tour, Avoid Common Pitfalls
- Angular Components - The Fundamentals
- How to build Angular apps using Observable Data Services - Pitfalls to avoid
- Introduction to Angular Forms - Template Driven vs Model Driven
- Angular ngFor - Learn all Features including trackBy, why is it not only for Arrays ?
- Angular Universal In Practice - How to build SEO Friendly Single Page Apps with Angular
- How does Angular Change Detection Really Work ?