This article continues our ongoing Angular Architecture series, which tackles recurring design problems and their solutions at both the View and Service layers. Here is the complete list of posts in this 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
Have you ever considered what you stand to gain by building an application around a centralized store, whether in Angular or any other framework? It's common to jump straight into Actions, Reducers, and the rest of the store-related vocabulary, but those concepts — while necessary — are really just tools to achieve a larger goal.
The centralized store approach is fundamentally a collection of application design patterns. So the logical entry point is to understand the design intention itself: what problems does this architecture aim to solve, and what mechanisms does it use? By getting a firm grasp on that first, we reduce the chances of either misusing the architecture or failing to fully exploit its strengths.
The store architectural shift
Adopting a store architecture marks a significant departure from older paradigms. The arrival of single-page applications relocated the Model-to-View transformation from the server to the client. Store architectures take that transition one step further: they also shift the Model-to-View-Model mapping to the client side.
But what exactly is gained by this shift, how does it function, and how do the various pieces — Ngrx Store, Actions, Reducers, Selectors, and Effects — fit together? That's the subject we'll explore in this series.
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
- Conclusions
What is the best place to get started with Store Solutions?
Let's start from the very beginning of store architectures. We will revisit the original Facebook counter problem that sparked their development, examining both the nature of the issue and the remedy that was applied.
The Original Facebook Chat Bug that originated Flux
The initial Flux talk is an excellent starting point for understanding the Flux architecture and store solutions, as it walks through the original issue in detail. It's a compelling story: the entire architecture traces back to a single, highly visible bug affecting Facebook users — a problem with the unread messages counter. Long-time Facebook users may even remember it.
Technically, it wasn't so much a random bug as a fundamental limitation of the architecture that preceded Flux, as we're about to see.
What was the Facebook counter bug?
The core symptom was that the unread messages counter consistently showed incorrect values. Users might see, for instance, that they had one unread message, but upon clicking the counter, they would find that all messages had already been read:

Just a simple bug?
From a non-developer's perspective, this might sound like a simple issue to fix. But most of us have encountered this kind of problem: you keep fixing what seems like a small bug, only to see it resurface in various forms.
That was precisely the experience of the Facebook team. Over the course of months, they repeatedly patched the counter, yet the same bug kept finding its way back. New variations appeared, or newly introduced features would cause the defect to reappear.
How did they fix the problem?
To truly resolve the issue, the Facebook team realized they needed to overhaul the application's architecture substantially. 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 covers stores, dispatchers, actions, and all the other elements of the Flux architecture. However, it's also interesting to note that the presentation focuses more on the solution than on the root causes of the initial problem.
We know there was a recurring counter bug, that Flux resolved it, and that Flux is built on several core concepts. But what exactly was causing the problem in the first place?
So what was the problem?
Was it simply a counter bug? If so, why was it so difficult to fix? The talk provides several clues. A key sentence is that the solution needed to:
..."brings more real data to the client-side, less derived data".
The introduction of Flux solved the issue, and the counter problem did not return. So what does this mean: what was the original problem, and how did Flux address it?
The Facebook counter problem is common to many early single page applications
At its core, the Facebook counter problem is closely tied to a fundamental distinction present in virtually any user interface we might build. Every UI application has, either implicitly or explicitly, two types of data:
- a Model, or Domain Model
- A View Model
For example, here's a simplified version of the Facebook chat application, with two users on separate machines:

Looking at the user interface, we can see that this application deals with three main types of data:
- Messages
- Threads
- Participants
If we're building our program in Typescript, these data types correspond to the definition of the following three custom types:
Notice that Message and Participant are plain POJOs, while Thread includes 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.
We could also choose to build the program in plain Javascript, without strong types. But even then, the three domain model notions would still exist:
- at least in the terminology used by the development team to discuss the program
- in the written functional specification documents of the project
It's a significant advantage to be able to define these three types using Typescript at the language level and use them to construct the program.
But these types are not what we see on the screen, or are they?
These types closely resemble what we'd typically use to define the database schema of our application. If we were using a SQL database, these could almost be the definitions of three database tables.
Even if we used a NoSQL database like Firebase and chose to store the denormalized View Model directly in the database, those three domain model notions would still exist, either in the program directly or implicitly in the project documentation.
It's very convenient to write our program around the same domain model notions we use to discuss it. There's just one problem: those types don't correspond directly to what is displayed on the screen.
Model vs View Model
What we see on the screen is not the Model; it's actually the View Model — this is a crucial distinction. Sometimes the View Model maps directly to the Model, but most of the time they are different models.
In the application above, for example, we could define a couple of View Models like these:
(Note the VM suffix, which stands for View Model)
The ThreadSummaryVM is a good illustration of how the View Model is closely related to the Model, but isn't quite the same thing. This View Model corresponds to the list of threads on the left side of the screen:

Each entry in that list, however, does not correspond to a single Thread Model instance. Instead, it maps to a combination of:
- one
Threadinstance - several
Participantinstances, since we can see their names as a comma-separated list - the current participant, and information about whether they've read the thread (unread threads appear in red)
- one
Messageinstance (the thread's last message)
There's also an extra bit of state that is purely UI-related: which thread is currently selected. In this case, the first one, highlighted in blue. This UI-only state (the currently selected thread Id) is also used by the messages list component on the right to determine which messages to display. We'll get back to this UI state later; for now, let's focus on the Domain Data.
What is the View Model similar to?
Continuing with the SQL analogy, the three custom types we defined as interfaces above would correspond to the result of a SQL query that joins three tables. We don't have to use Typescript custom types to define the View Model, but doing so makes it very clear that it's distinct from the Model.
An Example of Model to View Model distinction
To better grasp this distinction, here is the Model Data for the first thread shown on the screen:
This is all the data associated with the first thread, including all its messages and participants.
What does a View Model look like?
What we actually see in the threads list on the left 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 identical: the View Model is a transformation of the model — a query executed on the model.
For example, we know the read flag is set to true because we queried the Model data dbMessagesQueuePerUser and found that there were no pending messages for the currently logged-in user with id 1.
So what does this have to do with the Facebook counter?
It may not be immediately obvious, but one of the main challenges in the Facebook chat scenario is that multiple View Models of the same Model are displayed on screen at the same time, including:
- the list of thread summaries
- the list of messages for the currently selected thread
- And the unread messages counter!
The counter is itself a View Model — another transformation of the Model. It's an aggregated view reflecting information derived from the Participant, as well as all the threads and messages they have access to.
The counter is a summarized view of the Model that must stay in sync with the other View Models in the application. This brings us to the first problem that is hard to solve without a store.
Facebook Chat Problem 1 - Multiple view models for the same business data
In user interfaces that need to display multiple View Models of the same domain data (the Model), how do we keep everything in sync? Each top-level component in the View Layer (the thread list, the message list, or the counter) needs to display a particular View Model, and these multiple views must remain consistent at all times.
Not every UI will face this problem, and not every screen in an application will necessarily trigger it. But the Facebook home page with the chat window open and the unread messages counter at the top definitely does.
But is this the only fundamental design constraint behind the Facebook counter bug situation?
What if this was a read-only application?
In that case, we'd simply load the data, build its multiple View Models, and display them — that would solve the issue.
However, Facebook chat is an application that constantly modifies its data, through operations like:
- the user sending a new message, which gets added to the list
- the UI continuously receiving new messages from the backend via server push
- new messages being reflected both in the unread counter and on the thread list (the last message is updated, unread threads are highlighted in red)
- when the user clicks on a thread, all its messages are marked as read, which affects the message counter
So, multiple parts of the application are:
- constantly modifying the same domain Model data
- displaying different View Models of that data simultaneously
This leads us to the second problem inherent in the Facebook counter situation.
Facebook Chat Problem 2 - the shared data ownership/data encapsulation problem
When multiple parts of the application need to modify the same data, which part actually owns it? Is it a case of shared ownership, or do the various components maintain local copies of the data and inform each other of every change?
Can they share references to the same data and mutate it directly? Let's examine these options one by one.
Keeping local copies of the data?
Keeping copies of the data at the component level wherever a view model is needed doesn't work; it doesn't scale well in terms of complexity. We would quickly end up with event spaghetti, where every part of the application has to notify every other part of every modification.
Sharing references to the same data and mutating it?
This approach also fails because it introduces indirect couplings between different parts of the application. We can no longer reason about a component by just looking at it and its template — we lose locality.
That's because the component's data is being directly modified by an unrelated part of the application. As more features that mutate the data are added, we'll accumulate edge cases that don't quite work correctly.
This is definitely one of the dynamics at play in the Facebook counter problem.
An ancient problem going all the way back to OO programming
The core question is: who owns modifiable data? How do we encapsulate modifiable data, keeping it private to one part of the program, while still making it accessible to other parts?
Alternatively, should we expose the data but make it Immutable? That would also prevent shared ownership errors. But there's also a third problem, one that's not just about maintainability, but about user experience.
Facebook Chat Problem 3 - Avoiding Ajax "spinner" applications
A simple way to sidestep the problems above would be to get the View Model directly from the backend, which is, in fact, how most applications are built today.
So the Thread Section, Message List, and Counter components could each obtain their View Models via separate calls, or through one large, shared backend request. And when a data modification occurred, we'd just send a request to the backend and refresh everything, thereby ensuring consistency.
What is the problem with this approach?
This would lead to the kind of early Ajax "Spinner" single-page applications where data is perpetually reloaded with every user action. This results in a poor user experience, particularly on mobile devices. This is especially critical 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 clear picture of the original issue in the Facebook application: the home screen plus chat needed to display multiple View Models of the same Model data all at once on the same page. That data didn't belong to any single part of the application and was being modified constantly by several independent actors (the server via push, and the user). All View Models needed to stay synchronized without constantly refreshing everything.
The main problem with the Facebook counter bug situation
The central challenge was that traditional, pre-store solutions — transforming the Model into View Models on the server and having the backend return those ready-made View Models — don't work well in this scenario. Let's now introduce the Store application architecture pattern as a way to address all these issues — and it works very effectively. The store provides an elegant, maintainable solution for every problem encountered in the Facebook counter scenario.
The Centralized Store Design Pattern In a Nutshell
Let's go back to the Flux talk and its key phrase, "bring more real data to the frontend." What does this actually mean? This is the very essence of the store solution.
It means that instead of converting the Model into View Models on the server, as we usually do, we should copy the Model to the client side. We can then store it as an in-memory client database and derive View Models on the fly at the client side, rather than doing so upfront on the server. Let's break down this design in detail and explore code for each component.
How is the data handled?
- We create a client-side in-memory database for the application data.
- We bring the real domain data — the Model — to the frontend, not the pre-built View Models the UI needs.
- We keep that Model data (which is a user-specific slice of the database, typically paginated) in that in-memory database.
- We place that in-memory database inside a centralized service known as a Store.
- We make sure the centralized service owns the data, either by encapsulating it or exposing it as immutable.
- We give this centralized service reactive properties, so we can subscribe to it and be notified when the Model data changes.
How are View Models produced?
This outlines the structure of the view layer under this store design:
- Each component that needs to generate a View Model subscribes to receive new versions of the in-memory database data.
- Upon receiving the Model data, each component creates its own View Model at the very last moment, right at the component level.
- This guarantees that all View Models are always in sync with the centralized data Model.
- The Model-to-View-Model transformation happens locally on the View Root at the last possible moment, not on the server.
- This transformation is carried out by a function called a Selector — its input is the Model, and its output is the View Model.
How is the data modified?
Here's how we can change the data within this store design while preserving view synchronization:
- Data can only be updated within the centralized service, by the store itself.
- If a component decides to trigger a data change, it dispatches a message to the centralized service in the form of a command payload called an Action object.
- The sender of the action doesn't know which parts of the app will be affected; there's no tight coupling among the View components involved.
- The Action contains all the information needed to launch a sequence of operations that will modify the Model data.
- A new version of the application Model state is generated by applying a pure function, called a Reducer, to the previous state and the action.
- The Reducer function can be split into multiple smaller functions, each responsible for modifying a part of the state.
- Once the new state Model is ready, it is broadcast to all subscribed components, which then convert it into their View models.
- The new state model is frozen before being broadcast, so subscribers can't modify it.
What data is kept inside the Store?
-
The store holds not only the Model data but also any UI-specific global state that exists purely client-side, such as:
- the currently selected thread ID
- the data of the current user
Was this the exact solution used by Facebook?
Facebook's initial solution employed multiple stores that could wait for each other to be notified and each contained a subset of the data. The design described above utilizes a single centralized store (this approach was popularized by Redux), which represents a step forward from the Facebook solution. However, the fundamental idea is identical.
So let's move through this design step by step and see what each piece looks like in practice.
Installing a store solution
Let's begin by adding ngrx store to our application:
npm install --save @ngrx/store @ngrx/core
This allows us to define a client-side in-memory database for our application. The first thing we need to decide is what type of data the store should hold.
Defining the shape of the Application State
After installing the store, it's wise to define a custom type called ApplicationState:
As we can see, the contents of our in-memory database can be split into two categories. First, some state pertains purely to the UI, which we define using a custom type named UiState:
This state contains the currently logged-in user in our chat application, the currently selected thread in the thread list, and the current error message (if any).
It's clear that this state is entirely distinct from the thread data itself, which lives in the StoreData custom type:
Note that the data is organized in maps, not arrays. For these maps, the key is a number corresponding to the Id field of the element in the map. For instance, here's what the participant's data might look like:
At first glance, this might appear redundant, as we're storing the Id twice: once as the map key and once inside the object. This format is designed to keep the data as shallow as possible, avoiding deep nesting, and is optimized for "find by Id" operations. This structure closely mimics a database table in SQL, where the primary key is the object's Id field.
These custom types define the shape of the data inside the store, but what about its initial value?
Defining the initial value of the store state
In addition to defining the structure, we can establish the initial state for each data type like this:
This will be the init…al value of the in-memory data while we haven't yet loaded the store with backend information. For the chat app, we plan to populate the store with data from an initial backend request.
How to populate the in-memory database?
Our aim now is to retrieve the data via a standard HTTP request from the backend and store it in the store. We interact with the store by sending it a command to modify its internal data in a specific predefined way. This command object is called an Action. The action will trigger a modification of the store state synchronously and immediately.
How do we handle asynchronous actions? That's covered in part 2. For now, we already have the data ready to be loaded into the store, so how do we go about it? We start by defining an Action:
The action contains both a type and a payload. In this case, the payload is a transfer object that matches the data fetched from the backend in an initial request:
If we have a backend service that can retrieve the data from the server, here's how we'd dispatch it to the store:
As you can see, we're sending a command object with all the necessary data to the store's centralized service. But we don't know which other parts of the app will be affected by this change, or how. So what will the store do with this action, and how will it save the data?
How does the store process each action?
Remember, the action's purpose is to alter the state inside the in-memory database in a particular way. Each time the store receives a new action, it takes the current state, combines it with the action, and produces a new version of the application state.
This new state is generated using a reducer function. The reducer is a pure function — meaning it has no side effects — with a signature similar to this:
It's called a reducer because it shares the same signature as the reduce functional programming operation. Simply put, it's a way of creating a new store state in response to an action being dispatched. What does this function actually look like?
Reducer functions
Reducers are only one small element of the larger design we're implementing. The overall goal here is to bring the Model data to the frontend and keep it in memory, and the reducer is the mechanism we use to modify that data in a maintainable way.
There are many ways to structure reducer functions. A common practice is to divide them into smaller functions, each handling a specific piece of the state. For instance, a valid store reducer might delegate state creation to smaller helpers like this:
As expected, the output is an instance of ApplicationState, building its uiState and storeData properties by delegating calculations to smaller functions.
Let's look at a quick example of one of these smaller reducer functions:
A typical reducer function includes a switch statement on the action type. Inside it, we add the specific processing logic for a given action: in this case, we're loading all the data into the store by saving it in maps keyed by Id.
To store the backend data by Id, we use the Lodash keyBy utility function. This function takes the backend data and uses it to initialize the in-memory database. But how do the components in the View layer consume that data?
How to consume the data inside the in-memory database?
If a component wants to be notified whenever new data becomes available, the simplest approach is to inject the store's centralized service through its constructor. The store is viewed by the View layer as an observable of application state. Let's review the implementation of a component that consumes the store data reactively:
What's happening in this ThreadSection component? Let's break it down:
- It's a top-level component that gets a service injected via its constructor.
- This type is known as a smart component because it is aware of the service layer and how to use it.
- The only injected service is the store itself; no additional service needs to be provided to the View layer.
- The component's goal is to take the store and create a set of data streams derived from the application state.
- All member variables are observables, created using the select operator from the store.
- Observable variables like
threadSummaries$emit View Models, not raw Model data. - This component never directly accesses the application data; it holds no direct references to it.
- It communicates with the rest of the application by dispatching store actions; the store acts like a View layer facade, ensuring the
ThreadSectioncomponent stays decoupled from the rest of the app. - The streams configured as member variables are fed to the template and consumed via the
asyncpipe.
As you can see, the store is treated as an observable of application state and is used to derive streams of View Models. These View Models are derived from the Model data in the store at the final moment, just before being injected into the View.
How to produce View Models from the application state?
The store Model data (stored under the storeData property within ApplicationState) is transformed into a View Model using a Selector function. The Selector maps the application state to a specific View Model. For instance, this function returns the unread messages counter:
As we can see, the function simply takes the application state, finds the current user, and iterates over all threads in the in-memory database. It then sums up all the threads with unread messages for that participant, producing the total count. The Selector is a pure transformation: it takes the store state, queries it, and returns the View Model.
Presentational components
The ThreadSection component is also a solid example of a container component: it configures a set of data streams and passes the actual data to a tree of local components via the async pipe.
One component in that local tree is thread-list. Let's examine it:
This component is constructed very differently from the smart component. It's referred to as a presentational component. Let's outline how it works:
- Its primary goal is to display some data.
- It receives data synchronously via an
@Input(). - It can emit events to its parent using
Output(). - It's more reusable because it could be used in other places to display different lists of threads.
- It is unaware of how to fetch data from the backend; it simply receives a list of threads and displays it.
These are the foundational elements of the centralized store design and how they all connect to achieve the overall goal.
How does this solution solve the Facebook counter problem?
A good reminder: it's all about creating an in-memory, client-side database that holds a user-specific slice of the backend data, and then deriving View Models from it on the client. This thoroughly resolves the Facebook counter problem:
- Multiple views of the data are synchronized by design.
- Ownership of the shared data is consolidated in a centralized service.
- The user experience can be improved, since we won't constantly be refreshing the application.
This covers the key components of the centralized store pattern.
Conclusions
The Ngrx ecosystem is much more than a set of libraries; it's a set of application design patterns. Stores, Actions, Reducers, and Selectors are all tools for implementing these patterns. To understand these concepts fully, it's best to start by learning the centralized store pattern itself, then drill down into the specific implementation details. All the store concepts, like Actions and Reducers, become simpler once we see them as a means to implement an overall design.
The centralized store design is very effective at solving many problems that are otherwise difficult or impossible to handle. It accomplishes this by introducing certain architectural trade-offs. To see a complete implementation of this pattern using Ngrx and other libraries, take a look at the Ngrx sample application.
Stores and backend design
Stores aren't only relevant to the frontend. Using an in-memory database that holds real Domain data opens up new possibilities for building more reusable backends that emit Model data instead of pre-built View Models. For example, a store is a great fit for GraphQL backends, since you can define the exact Model data you need in each request. The endpoint doesn't return a predefined structure, unlike traditional REST endpoints.
In this approach, we could construct just one backend for our entire system, used by both the UI and any third-party services. That's not the most common situation today, where we often end up with a UI-specific backend separately from a REST backend for third-party integrations.
By moving Model-to-View-Model mapping to the client and giving our backend query capabilities, we could build a single, reusable API consumed by both our UI and third parties.
If you found this post valuable, take a moment to browse the list below for related posts and resources on Angular.
Feel free to subscribe to our newsletter to get notified whenever new posts are published:
For an in-depth dive into NgRx, we recommend exploring the NgRx with NgRx Data - The Complete Guide course, where we cover the entire NgRx ecosystem in far greater detail.
If you're new to Angular, check out the Angular for Beginners Course:
Other posts on Angular
If you liked this post, you might also enjoy these other popular articles:
