Resiliency patterns in Angular, NestJS and RxJS


Resiliency in real life


Not so resilient






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.'));
}),
);
}


Nikola Mitrović
Freelance Developer
📍Novi Sad, Serbia



Career Development Coach
Web Consultant
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(() => new Error('Recipes could not be fetched.'));
}),
retry(3)
);
}
Retry pattern

RxJS retry operator
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(
catchError((error) => {
this.showFeedbackMessage('Recipes could not be fetched.');
return throwError(() => new Error('Recipes could not be fetched.'));
}),
retry({
count: 3,
delay: (error, retryCount) => {
console.log(
`Attempt ${retryCount}:
Error occurred during network request,
retrying in ${Math.pow(2, retryCount)} seconds...
`
);
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 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(
catchError((error) => {
this.showMessage();
return throwError(
() => new Error('Recipes could not be fetched.'));
}),
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(2000);
},
}),
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(
catchError((error) => {
this.showMessage();
return throwError(
() => new Error('Recipes could not be fetched.'));
}),
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(2000);
},
}),
tap(() => {
// Successful retry, close the circuit
this.closeCircuit();
this._snackBar.dismiss();
}),
);
}
CLOSED -> OPEN
OPEN
OPEN -> HALF_OPEN
HALF_OPEN -> OPEN
HALF_OPEN -> CLOSED
Optimistic update


Helen Keller
Optimism is the faith that leads to achievement.

Optimistic update
postRecipe(): void {
this.http
.post<Recipe>('https://super-recipes.com/api/recipes', this.recipe)
.pipe(
optimisticUpdate(this.recipe, (originalItem: Recipe, error: Error) => {
// Rollback UI changes here
console.error('Error updating item:', originalItem);
this.recipes$.next(error);
})
)
.subscribe((recipe: Recipe) => this.recipes$.next(recipe));
}
Optimistic update
import { Observable, of, throwError, catchError, retry, ignoreElements } from 'rxjs';
export function optimisticUpdate<T, E = any>(
originalValue: T,
rollback: (value: T, error: E) => void,
retryConfig: { count: number; delay: number } = { count: 1, delay: 20000 }
): (source: Observable<T>) => Observable<T> {
return (source: Observable<T>) => {
return concat(
of(originalValue),
source.pipe(
ignoreElements(),
retry(retryConfig),
catchError((error) => {
rollback(originalValue, error);
return throwError(() => error);
})
)
);
};
}
Optimistic update
postRecipe(): void {
this.http
.post<Recipe>('https://super-recipes.com/api/recipes', this.recipe)
.pipe(
optimisticUpdate(this.recipe, (originalItem: Recipe, error: Error) => {
// Rollback UI changes here
console.error('Error updating item:', originalItem);
this.recipes$.next(error);
})
)
.subscribe((recipe: Recipe) => this.recipes$.next(recipe));
}
NestJS

Retry
Exponential backoff
Circuit breaker
Bulkhead
getRecipes$(): Observable<Recipe[]> {
return this.httpClient.get<Recipe[]>('/api/recipes').pipe(
catchError((error) => {
this.logError(error);
return throwError(
() => new Error('Recipes could not be fetched.'));
}),
retry(3)
);
}
getRecipesWithBackoffStrategy$(): Observable<Recipe[]> {
return this.httpClient.get<Recipe[]>('/api/recipes').pipe(
catchError((error) => {
this.logError(error);
return throwError(
() => new Error('Recipes could not be fetched.'));
}),
retry({
count: 3,
delay: (error, retryCount) => {
return timer(Math.pow(2, retryCount) * 1000);
},
}),
);
}
getRecipesWithCircuitBreakerStrategy$(): Observable<Recipe[]> {
return defer(() => this.sendRequestIfCircuitNotOpen()).pipe(
catchError((error) => {
this.logError(error);
return throwError(
() => new Error('Recipes could not be fetched.'));
}),
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(2000);
},
}),
tap(() => {
// Successful retry, close the circuit
this.closeCircuit();
}),
);
}
import { Observable, Subject, mergeMap } from 'rxjs';
@Injectable()
export class BulkheadInterceptor implements NestInterceptor {
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();
}
}
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
By nmitrovic
Resiliency patterns
- 78