Angular Architecture & RxJS
Practical senior topics: feature boundaries, DI, standalone, RxJS patterns, leaks, change detection, and state.
1. Feature Architecture (How to organize a real app)
Interviewers want structure that scales: separation by feature, clear public APIs, and minimal coupling.
✅ Recommended
- Organize by feature (not by type).
- Expose a public API (index.ts) per feature.
- Keep shared limited (UI kit, utils, primitives).
- Put domain-ish logic in services/facades (not in components).
🚨 Traps
- God shared module that imports everything.
- “components/services/models” by type (hard to scale).
- Business rules inside templates/components.
- Cross-feature imports without a clear boundary.
// src/app
// core/ (singletons: auth, interceptors, config)
// shared/ (ui, pipes, utils - no feature dependencies)
// features/
// orders/
// data-access/ (api, repositories)
// ui/ (presentational components)
// feature/ (smart/container components)
// orders.routes.ts
2. Smart vs Dumb Components (Container/Presentational)
This is how you keep templates simple and logic reusable.
Smart (Container)
- Fetches data / talks to facades.
- Orchestrates side effects.
- Passes data down via Inputs.
Dumb (Presentational)
- Pure UI: Inputs + Outputs.
- No API calls, no routing.
- Easy to test + reuse.
export class OrderListComponent {
orders = input<Order[]>([]);
select = output<string>();
}
export class OrdersPageComponent {
orders$ = this.facade.orders$;
onSelect(id: string) { this.facade.select(id); }
}
3. Dependency Injection (DI) & Providers
Senior Angular is about controlling scope: singletons, per-feature instances, and testable abstractions.
✅ Best practices
- Put singletons in core only (auth, interceptors).
- Provide feature-specific services at route/feature level.
- Prefer facades for features (UI doesn't talk to HttpClient directly).
🚨 Traps
- Service provided in root but used as feature state (shared across pages unexpectedly).
- Injecting too much in components (hard to test).
export const routes = [
{ path: 'orders', loadComponent: () => import('./orders.page'),
providers: [OrdersFacade, OrdersApi] }
];
4. Modules vs Standalone (Modern Angular)
Standalone simplifies composition and tree-shaking. Modules still exist, but don’t need to dominate new codebases.
Standalone shines when
- You want routes + lazy loading with fewer files.
- You want explicit imports per component.
- You want feature isolation (providers at route level).
Modules are ok when
- You maintain legacy apps or library packaging.
- You have shared UI libraries already built around NgModules.
5. RxJS Fundamentals (What interviewers really test)
✅ Key ideas
- Observable is lazy (executes on subscription).
- Operators build a pipeline (map/filter/switchMap).
- Subscription is a resource (must be cleaned up).
- Cold vs Hot streams matters for duplication.
🚨 Classic trap
Subscribing inside subscribe (nested subscriptions) → leaks + race conditions. Prefer higher-order mapping operators.
user$.subscribe(u => {
http.get(`/api/orders?user=${u.id}`).subscribe(orders => {});
});
// ✅ GOOD: switchMap
orders$ = user$.pipe(
switchMap(u => http.get(`/api/orders?user=${u.id}`))
);
6. Subjects (BehaviorSubject / ReplaySubject)
BehaviorSubject
Holds the latest value. New subscribers immediately receive the current value.
ReplaySubject
Replays N previous values. Useful for caching streams (careful with memory).
🚨 Trap: Subject everywhere
Subjects are powerful but can become “event spaghetti”. Prefer derived streams (pipe) and keep Subjects private inside facades.
7. Operators & Patterns (switchMap vs mergeMap vs concatMap)
switchMap
Cancels previous request. Perfect for typeahead / latest wins.
mergeMap
Runs in parallel. Use for independent work (limit concurrency if needed).
concatMap
Queues requests sequentially. Use when order matters.
results$ = query$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(q => http.get(`/api/search?q=${q}`)),
shareReplay({ bufferSize: 1, refCount: true })
);
8. HTTP Streams, Caching & shareReplay
🚨 Trap: multiple async pipes = multiple HTTP calls
If you bind the same cold HTTP observable multiple times, you may trigger multiple requests. Use caching (shareReplay) or a facade with a single shared stream.
orders$ = http.get<Order[]>('/api/orders').pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
⚠️ Note
shareReplay can keep data in memory. Prefer refCount=true and be intentional about caching boundaries.
9. Subscriptions & Memory Leaks
✅ Good patterns
- Prefer async pipe in templates.
- Use takeUntilDestroyed for manual subscriptions.
- Keep subscriptions in containers, not in dumb components.
🚨 Traps
- Subscribing in services without lifecycle control.
- Forgetting to unsubscribe in long-lived components.
- Subjects never completed + global event listeners.
import { DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export class OrdersPageComponent {
private destroyRef = inject(DestroyRef);
ngOnInit() {
events$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
}
}
10. Change Detection (CD) & Zones
A senior Angular dev understands when the UI re-renders and how to control it.
💡 Key interview point
Default strategy checks many components frequently. If your data is immutable and streams-driven, OnPush reduces work.
11. OnPush Strategy (The performance default)
✅ When OnPush updates
- Input reference changes (immutability).
- Event in the component (click, input).
- Async pipe emits a new value.
🚨 Trap: mutating arrays/objects
If you mutate in place, OnPush may not detect changes. Prefer new references (spread, map, etc.).
12. Signals & RxJS Interop
Signals are great for local UI state; RxJS remains excellent for async workflows and complex stream composition.
// (interview-friendly statement)
13. State Management (Facade-first)
Facade pattern
Components consume readonly streams + call methods. Implementation (signals/RxJS/store) is hidden behind the facade.
Store choice (NgRx/Akita/Signals)
Pick based on team maturity and app complexity. Don’t choose a store “because trendy”.
export class OrdersFacade {
orders$ = this.api.orders$;
select(id: string) { /* update state */ }
}
🎯 Interview Advice
If you say “feature boundaries + OnPush + async pipe + switchMap + takeUntilDestroyed + facade-first state” — you’ll sound like a real senior Angular engineer.