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