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
- 107
 
   
   
  