Choosing an Architecture for Angular Applications
Angular applications can be organized in a number of ways. A popular approach in recent years is the Flux-inspired design, which relies on a single central store for all application state, much like Redux. For those looking to adopt this style, a few options exist:
- For reactive applications that want a centralized, predictable state container, the standard choice is
@ngrx/store, with a full guide available here. - To use Redux more directly, this post walks through the details and includes a working sample application.
- If you prefer to stick with RxJs but still want the single-state-atom concept that Redux popularized, take a look at this other post, which demonstrates the approach with an example app.
There is, however, an alternative strategy that does away with a singular global state. This article explores that route in detail: the use of observable data services. If you are still familiarizing yourself with Observables and Angular, you may find this companion post helpful, as it covers several frequent trouble spots.
Defining an Observable Data Service
At its core, an observable data service is a regular Angular injectable service whose purpose is to supply data to various parts of the application. Such a service—often referred to as a store—can be injected wherever that data is required. In a typical setup, you might inject two distinct services: one holding the primary application data (such as a list of todos), and another that tracks some aspect of the UI state (for example, an error message currently on display).
Using an Observable Data Service
The service is built around an observable it exposes. Consider TodoStore, which makes available the todos observable. Each emission from this observable represents a fresh list of todo items.
Templates can consume this observable directly, thanks to the async pipe. This pipe handles the subscription for you and extracts the most recent emitted value:
Modifying the Service Data
To alter the data held within the service, you invoke its action methods. For instance:
These methods modify the internal state and trigger a new emission from the store. All active subscribers receive this updated value and the view adjusts itself accordingly.
Noteworthy Aspects of the Pattern
A key feature is that consumers of the store are blissfully unaware of the source of change. Whether a todo was added, removed, or toggled, subscribers simply notice a new value has arrived and re-render. This creates a clean separation, as data consumers remain decoupled from the action modifiers.
Another benefit is that the smart components where the store is injected hold virtually no state variables of their own. Since such local state is a frequent source of bugs, removing it aligns with better architectural practices.
It is also worth pointing out that Http backend services are not invoked directly from within these smart components. Any data modification is requested solely through the store itself.
Having seen how the pattern works, let's turn to the mechanics of building one with RxJs.
Understanding the RxJs Subject
The engine of an observable data service is the RxJs Subject. This type straddles both worlds: it implements the Observer interface (so it can emit values) and the Observable interface (so others can subscribe to it).
In essence, a Subject operates much like a traditional event bus, but it is far more capable because it comes bundled with the full suite of RxJs operators. A basic subscription looks identical to that of a regular observable:
The interesting distinction is that a Subject can also be used to push values out to its subscribers:
However, there is a particular trait of the Subject that prevents it from being the direct foundation for our data services: when you subscribe to it, you will not be handed the most recent value. You must wait for some code to call next() before the subscriber receives anything. This is a real problem, particularly during application bootstrap. At that early stage, certain subscribers have not yet registered. For instance, template async pipes may not have been set up because the views they belong to have not finished initializing.
Introducing the BehaviorSubject
The BehaviorSubject solves this bootstrap dilemma. Upon subscription, it immediately emits the latest value from the stream. If no value has been issued yet, it emits an initial state that was provided at creation:
Another beneficial property of the BehaviorSubject is its snapshoting capability: you can retrieve the current stream value at any point in time:
Thanks to these features, the BehaviorSubject forms the cornerstone of an observable data service. In fact, with this single construct, you have almost everything you need. Let's look at a tangible example.
Constructing an Observable Data Service
A fully working store can be found here, but let's examine the essential part:
The store maintains a single private member called _todos. This member is a BehaviorSubject whose initial state is simply an empty array of todos. The constructor receives the Http backend service via dependency injection. Notably, this is the sole location within the application where the backend service is used directly. Everywhere else, the TodoStore is injected instead.
Because the store's data is initialized within the constructor, the choice of BehaviorSubject is crucial—it guarantees that any subscriber will always have access to a value immediately. Which brings us to the question: why bother with the extra public todos property?
Pitfall #1: Exposing Subjects is a Bad Idea
Notice that the store does not hand out the raw subject to its clients. Instead, it exposes an observable. The reasoning is straightforward: you want to prevent consumers from emitting values on their own. If they could, they could bypass the store's action methods entirely, leading to a mess.
Avoiding Event Soup
Directly exposing the subject is a gateway to what is commonly referred to as an "event soup" application. In such an architecture, events become tangled together and the overall flow becomes impossible to trace.
Exposing the subject is analogous to handing out a reference to an internal data structure of a class. By doing so, you relinquish control over that state and open the door for any party to push values into it. There could be a pragmatic reason for this, but more often than not, it is not what you actually want to happen.
Writing an Action Method
Within this pattern, actions are just methods exposed by the store. Let's dissect an implementation of addTodo:
This is one approach out of many. The method invokes the backend service, which returns an observable. We subscribe to that observable directly; on a successful response, we compose the next list of todos by appending the newly created one to the current list.
Pitfall #2: Beware of Duplicate HTTP Requests
A subtle but important detail in this example is that the observable returned from the Http call has two subscribers: the internal subscription inside addTodo and the external subscriber that invoked the method itself. Due to the default mechanics of observables (which are cold), this leads to two independent processing chains and, consequently, two separate HTTP requests. This is a classic surprise that observables can spring on you, detailed with other examples in this post.
There are straightforward remedies, such as the following, that guarantee a single network call:
However, be mindful of what you give up when you return a shared observable instead of the raw HTTP one:
-
You count on no duplicate requests being made.
-
But the calling code loses fine-grained control over the source observable (for instance, it can no longer implement retry logic directly).
For typical CRUD-style operations, this balance is entirely reasonable. When altering data, we should not expect a new backend call to fire each time a subscription is made to the returned observable. Instead, our focus is creating a view layer that behaves predictably, without the threat of duplicate modifications. Returning a shared observable via shareReplay() generally checks all the boxes for CRUD operations. It protects against accidental duplicates caused by multiple view subscriptions. If, in rare cases, you do need to access the underlying plain HTTP observable, you can always expose it through a separate dedicated method.
Final Thoughts
Observable data services, often dubbed store services, represent a straightforward and intuitive approach to harnessing functional reactive programming within Angular without needing to grasp a large number of novel concepts. The foundation lies in familiar ideas—such as the Subject, which is essentially a glorified event bus—making this pattern easier to absorb than others that rely on several different RxJs abstractions at once.
Taking simple precautions, like keeping the subject private, is often enough to retain app sanity. That being said, the right balance depends on the use case at hand. As the pitfalls demonstrate, a working familiarity with RxJs is practically a prerequisite. For a deeper dive into observables, revisit the previous post linked earlier.
Taking the Next Step with Angular
To continue strengthening your Angular knowledge, check out the Angular for Beginners Course:
References
Managing State in Angular Applications by Victor Savkin (@victorsavkin)
Managing state in Angular using RxJs by Loïc Marcos Pacheco (@marcosloic)
Further Reading on Angular
If this article was useful, you might also want to check out these other popular guides from our blog:
- Angular Router - How To Build a Navigation Menu with Bootstrap 4 and Nested Routes
- Angular Router - Extended Guided Tour, Avoid Common Pitfalls
- How to run Angular in Production Today
- How to build Angular apps using Observable Data Services - Pitfalls to avoid
- Introduction to Angular Forms - Template Driven, Model Driven or In-Between
- 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 ?
