Designing an Angular State Architecture That Scales
The Problem with Naive State Management
Store state in services Use BehaviorSubject everywhere Mutate data directly
State becomes inconsistent Debugging becomes painful Components become tightly coupled
Principles of Scalable State Architecture
Single source of truth → Avoid duplicating state Immutability → Never mutate state directly Separation of concerns → UI ≠ Business logic ≠ Data layer Predictability → State changes should be traceable
Recommended Architecture Layers
Components (UI Layer) Only responsible for rendering No business logic
Facade Layer (Optional but powerful) Acts as a bridge between UI and state Simplifies component logic
State Layer (NgRx / Signals / Services) Stores and manages state Handles updates via actions/events
Data Layer (API services) Handles HTTP calls No state logic
Example: Simple Scalable State with RxJS
@Injectable({ providedIn: 'root' })
export class UserState {
private readonly _users = new BehaviorSubject<User[]>([]);
readonly users$ = this._users.asObservable();
constructor(private api: UserApiService) {}
loadUsers() {
this.api.getUsers().subscribe(users => {
this._users.next(users);
});
}
addUser(user: User) {
const current = this._users.value;
this._users.next([...current, user]); // immutable update
}
}
Why this works:
State is centralized Updates are controlled Components stay clean
Scaling Further with Facade Pattern
@Component({...})
export class UserComponent {
users$ = this.userFacade.users$;
constructor(private userFacade: UserFacade) {}
ngOnInit() {
this.userFacade.loadUsers();
}
}
@Injectable({ providedIn: 'root' })
export class UserFacade {
users$ = this.userState.users$;
constructor(private userState: UserState) {}
loadUsers() {
this.userState.loadUsers();
}
}
Benefits:
Components become dumb & reusable Easy to refactor state implementation later Cleaner testing
When to Use NgRx (or Not)
App is large and complex Multiple teams are working You need strict state control
Small apps → Stick to services + RxJS Medium apps → Add facades Large apps → Introduce NgRx or Signals
Best Practices Checklist
Use readonly observables (asObservable()) Avoid exposing Subject directly Keep state updates pure and immutable Split state by feature modules Use facade pattern for cleaner components Avoid putting logic in components
