Resiliency patterns in Angular & NestJS using RxJS


Resiliency in real life




Not so resilient
getRecipes$(): Observable<Recipe[]> {
return this.httpClient.get<Recipe[]>('https://super-recipes.com/api/recipes').pipe(
catchError((error) => {
this._snackBar.open('Recipes could not be fetched.', 'Close', {
verticalPosition: 'top',
horizontalPosition: 'right',
panelClass: ['mat-error'],
});
return throwError(() => new Error('Recipes could not be fetched.'));
}),
);
}


Promises v.s. Observables
Feature | Promise | Observable |
---|---|---|
Nature | Emits a single future value. | Emits zero, one, or multiple values over time. |
Completion | Resolves (success) or rejects (error) once. | Can emit multiple values, then complete or error. |
Cancellation | Not directly cancellable once initiated. | Cancellable (e.g., via unsubscribe() ). |
Operators | No built-in operators for transformation. | Rich set of operators (map, filter, merge, etc.). |
RxJS operators

Nikola Mitrović
Senior Full Stack Engineer @ AxiomQ
📍Novi Sad, Serbia


Career Development Coach
Tech Speaker
Published Author




Retry pattern
Why do we fall?
So that we can learn to
pick ourselves up.

Alfred (Batman Begins)


import { Observable, catchError, retry } from 'rxjs';
getRecipes$(): Observable<Recipe[]> {
return this.httpClient.get<Recipe[]>('https://super-recipes.com/api/recipes').pipe(
catchError((error) => {
this.showFeedbackMessage('Recipes could not be fetched.');
return throwError(() => error);
}),
retry(3)
);
}
Retry pattern
RxJS retry operator



import { Observable, catchError, retry } from 'rxjs';
getRecipes$(): Observable<Recipe[]> {
return this.httpClient.get<Recipe[]>('https://super-recipes.com/api/recipes').pipe(
catchError((error) => {
this.logger('Recipes could not be fetched.');
return throwError(() => new Error('Recipes could not be fetched.'));
}),
retry(3)
);
}
Retry pattern



Retry pattern

import { Observable, throwError, catchError } from 'rxjs';
export function catchErrorWithAction<T, E = any>(
actionCallback: (value: T, error: E) => void,
): (source: Observable<T>) => Observable<T> {
return (source: Observable<T>) => {
return source.pipe(
catchError((error) => {
actionCallback(error);
return throwError(() => error);
})
);
};
}


import { Observable, catchError, retry } from 'rxjs';
import { catchErrorWithAction } from './operators';
getRecipes$(): Observable<Recipe[]> {
return this.httpClient.get<Recipe[]>('https://super-recipes.com/api/recipes').pipe(
catchErrorWithAction((error) => this.showFeedbackMessage('Recipes could not be fetched.')),
retry(3)
);
}
Exponential backoff pattern


Tim S. Grover
After you lose or when you get knocked down. Stay down there for a minute. Understand why you lost.
Because if you just jump right back up, you’re gonna lose again and again.
And you continue to lose the same way.

Exponential backoff pattern

import { Observable, catchError, repeat, retry, timer } from 'rxjs';
getRecipesWithBackoffStrategy$(): Observable<Recipe[]> {
return this.httpClient.get<Recipe[]>('https://super-recipes.com/api/recipes').pipe(
catchErrorWithAction((error) => this.showFeedbackMessage('Recipes could not be fetched.')),
retry({
count: 3,
delay: (error, retryCount) => {
return timer(Math.pow(2, retryCount) * 1000);
},
}),
);
}
RxJS timer function

Bulkhead pattern



import { Subject, Observable, mergeMap } from 'rxjs';
export const BulkheadInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) {
private readonly MAX_CONCURRENT_REQUESTS = 3;
private requestQueue = new Subject<Observable<any>>();
constructor() {
this.requestQueue
.pipe(
mergeMap((task) => task, this.MAX_CONCURRENT_REQUESTS)
)
.subscribe();
}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
this.requestQueue.next(next.handle());
return this.requestQueue.asObservable();
}
}
Bulkhead pattern
Queues
Semaphores
Thread Pools
Processes
RxJS mergeMap operator

Circuit breaker pattern



Circuit breaker - scenarios

CLOSED -> OPEN - Consecutive failures reach threshold
OPEN - Automatic reject
OPEN -> HALF_OPEN - After timeout period expires
HALF_OPEN -> OPEN - If next request fails
HALF_OPEN -> CLOSED - If next request pass


Circuit breaker
private state:
'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private openCircuit() {
if (this.state === 'OPEN') return;
this.state = 'OPEN';
timer(5000)
.subscribe(() => this.halfOpenCircuit());
}
private halfOpenCircuit() {
this.state = 'HALF_OPEN';
}
private closeCircuit() {
this.state = 'CLOSED';
}
getRecipesWithCircuitBreakerStrategy$(): Observable<Recipe[]> {
return defer(() => this.sendRequestIfCircuitNotOpen()).pipe(
catchErrorWithAction((error) => this.showMessage()),
retry({
count: this.state === 'HALF_OPEN' ? 1 : 3,
delay: (error, retryCount) => {
if (this.state === 'HALF_OPEN' || retryCount === 3) {
this.openCircuit();
return throwError(
() => new Error('Circuit is open'));
}
return timer(Math.pow(2, retryCount) * 1000);
},
}),
tap(() => {
// Successful retry, close the circuit
this.closeCircuit();
this._snackBar.dismiss();
}),
);
RxJS defer operator
OPEN -> HALF_OPEN - After timeout period expires

Circuit breaker pattern
sendRequestIfCircuitNotOpen() {
if (this.state === 'OPEN') {
return throwError(() => new Error('Circuit is open'));
}
return this.httpClient.get<Recipe[]>('https://super-recipes.com/api/recipes');
}
OPEN - Automatic reject


Circuit breaker
getRecipesWithCircuitBreakerStrategy$(): Observable<Recipe[]> {
return defer(() => this.sendRequestIfCircuitNotOpen()).pipe(
catchErrorWithAction((error) => this.showMessage()),
retry({
count: this.state === 'HALF_OPEN' ? 1 : 3,
delay: (error, retryCount) => {
if (this.state === 'HALF_OPEN' || retryCount === 3) {
this.openCircuit();
return throwError(() => new Error('Circuit is open'));
}
return timer(Math.pow(2, retryCount) * 1000);
},
}),
tap(() => {
// Successful retry, close the circuit
this.closeCircuit();
this._snackBar.dismiss();
}),
);
}
CLOSED -> OPEN
OPEN
OPEN -> HALF_OPEN
HALF_OPEN -> OPEN
HALF_OPEN -> CLOSED


Circuit breaker
getRecipesWithCircuitBreakerStrategy$(): Observable<Recipe[]> {
return defer(() => this.sendRequestIfCircuitNotOpen()).pipe(
catchErrorWithAction((error) => this.showMessage()),
retry({
count: this.state === 'HALF_OPEN' ? 1 : 3,
delay: (error, retryCount) => {
if (this.state === 'HALF_OPEN' || retryCount === 3) {
this.openCircuit();
return throwError(() => new Error('Circuit is open'));
}
return timer(Math.pow(2, retryCount) * 1000);
},
}),
tap(() => {
// Successful retry, close the circuit
this.closeCircuit();
this._snackBar.dismiss();
}),
);
}


Circuit breaker
getRecipesWithCircuitBreakerStrategy$(): Observable<Recipe[]> {
return defer(this.sendRequestIfCircuitNotOpen).pipe(
catchErrorWithAction(this.showMessage),
retry(this.retryConfig),
tap(this.successfullRequest),
);
}


Circuit breaker
getRecipesWithCircuitBreakerStrategy$(): Observable<Recipe[]> {
return this.httpClient.get<Recipe[]>('https://super-recipes.com/api/recipes').pipe(
resiliencyWithCircuitBreaker()
);
}


import { Observable, of, throwError, catchError, retry, ignoreElements } from 'rxjs';
sendRequestIfCircuitNotOpen(source: T) {
if (this.state === 'OPEN') {
return throwError(() => new Error('Circuit is open'));
}
return source;
}
export function resiliencyWithCircuitBreaker<T>(
retryConfig?: { count: number; delay: number } = { count: 1, delay: 20000 }
): (source: Observable<T>) => Observable<T> {
return (source: Observable<T>) => {
return defer(() => this.sendRequestIfCircuitNotOpen(source)).pipe(
catchErrorWithAction(this.showMessage),
retry(this.retryConfig),
tap(this.successfullRequest),
);
};
}
Thank you!
nikola.mitrovic1313@gmail.com
You can find me at
Link to the slides

https://github.com/PacktPublishing/RxJS-Cookbook-for-Reactive-Developers

Resiliency patterns - Angular & NestJS
By nmitrovic
Resiliency patterns - Angular & NestJS
- 4