Skip to main content

Overview

Facades provide a clean abstraction layer between components and the underlying state management and services. They orchestrate complex workflows, manage side effects, and expose simple APIs for components to consume.

Facade Pattern Benefits

Encapsulation

Hide complexity from components

Reusability

Share business logic across features

Testability

Easy to mock and test in isolation

Type Safety

Full TypeScript type inference

Available Facades

AuthFacade

Authentication workflows and token management

TripPlannerFacade

Trip planning and route calculation

TripPassengerFacade

Trip requests and passenger operations

TripActiveFacade

Active trip state and real-time updates

SessionsFacade

Session lifecycle management

UsersFacade

User registration and management

AuthFacade

Orchestrates authentication flows including login, logout, token refresh, and session restoration.

Public API

login
(payload: LoginPayload) => Observable<User>
Authenticates user and initializes session
  • Calls AuthService.login()
  • Stores tokens (secure storage for mobile, cookie for web)
  • Fetches user profile with AuthService.me()
  • Schedules automatic token refresh
  • Returns user object on success
logout
() => Observable<void>
Logs out user and clears all session data
  • Calls AuthService.logoutWeb() or logoutMobile()
  • Clears tokens from storage
  • Resets AuthStore state
  • Cancels auto-refresh timer
  • Navigates to login page
refreshSession
() => Observable<string>
Refreshes access token using refresh token or cookie
  • Determines platform (mobile vs web)
  • Calls AuthService.refresh()
  • Updates tokens in store and storage
  • Reschedules next auto-refresh
  • Returns new access token
restoreSession
() => Promise<boolean>
Attempts to restore session on app startup
  • Loads session type from storage
  • Retrieves stored refresh token (mobile) or cookie (web)
  • Calls refreshSession() to get new access token
  • Fetches user profile if refresh succeeds
  • Returns true if session restored, false otherwise

Usage Example

src/app/features/auth/login/login.component.ts
import { inject } from '@angular/core';
import { AuthFacade } from '@/app/store/auth/auth.facade';
import { LoginStore } from '@/app/store/auth/login.store';

export class LoginComponent {
  private authFacade = inject(AuthFacade);
  private loginStore = inject(LoginStore);

  // Access loading state
  loading = this.loginStore.loading;
  error = this.loginStore.error;

  onSubmit(credentials: LoginPayload) {
    this.authFacade.login(credentials).subscribe({
      next: (user) => {
        console.log('Logged in as:', user.email);
        // Navigation handled by facade
      },
      error: (err) => {
        console.error('Login failed:', err.message);
      }
    });
  }
}

Token Refresh Scheduling

AuthFacade automatically schedules token refresh 30 seconds before expiration:
src/app/store/auth/auth.facade.ts
private readonly AUTO_REFRESH_OFFSET = 30_000; // 30s

private scheduleAutoRefresh(): void {
  const expiresIn = this.authStore.accessTokenExpiresIn();
  if (!expiresIn || expiresIn <= 0) return;

  const delay = Math.max(0, expiresIn - this.AUTO_REFRESH_OFFSET);
  
  this.refreshTimerId = setTimeout(() => {
    this.refreshSession().subscribe({
      next: () => console.log('Token refreshed'),
      error: (err) => this.handleRefreshError(err)
    });
  }, delay);
}

TripPlannerFacade

Manages trip planning workflow from destination search to fare estimation.

Public API

init
() => Promise<void>
Initializes trip planning with user’s current location
  • Gets current GPS location
  • Sets origin point in store
  • Loads cached destination if available
  • Recalculates route if both origin and destination exist
searchDestination
(query: string) => Observable<PlaceSuggestion[]>
Searches for destinations using Mapbox Places API
  • Calls MapboxPlacesService.search()
  • Filters results within Santiago de Cuba bounds
  • Returns array of place suggestions
selectDestination
(suggestion: PlaceSuggestion) => Promise<void>
Sets destination and calculates route
  • Updates destination in store
  • Calls MapboxDirectionsService.getRoute()
  • Fetches fare estimate
  • Caches for 24 hours
recalcRouteAfterAdjust
(newDest: LatLng) => Promise<void>
Recalculates route after user adjusts destination pin on map
  • Performs reverse geocoding for new coordinates
  • Updates route and fare estimate
  • Updates store with new values
resetPlanning
() => void
Clears trip planning state (soft reset, preserves origin)
hardResetPlanning
() => void
Clears all trip planning state including origin

Usage Example

src/app/features/tabs/map/map.component.ts
import { inject } from '@angular/core';
import { TripPlannerFacade } from '@/app/store/trips/trip-planner.facade';
import { TripPlannerStore } from '@/app/store/trips/trip-planner.store';

export class MapComponent {
  private facade = inject(TripPlannerFacade);
  private store = inject(TripPlannerStore);

  // Reactive signals from store
  routeSummary = this.store.routeSummary;
  fareQuote = this.store.fareQuote;
  isReady = this.store.isReadyToRequest;

  async ngOnInit() {
    // Initialize with current location
    await this.facade.init();
  }

  onMapClick(lngLat: { lng: number; lat: number }) {
    // User tapped map to select destination
    this.facade.recalcRouteAfterAdjust(lngLat);
  }
}

TripPassengerFacade

Handles trip requests and real-time trip tracking for passengers.

Public API

requestTrip
(payload: CreateTripRequest) => Observable<TripResponseDto>
Creates a new trip request
  • Validates origin, destination, and fare quote
  • Calls backend API to create trip
  • Starts real-time Socket.io connection
  • Returns trip details
cancelTrip
(tripId: string) => Observable<void>
Cancels an active trip
  • Calls backend cancellation API
  • Disconnects Socket.io listeners
  • Clears trip state
subscribeToTripUpdates
(tripId: string) => void
Subscribes to real-time trip events via Socket.io
  • Listens to driver assignment events
  • Tracks driver location updates
  • Handles trip status changes
  • Updates trip store reactively

SessionsFacade

Manages session lifecycle and device tracking.

Public API

getCurrentSession
() => Observable<Session>
Fetches the current active session
updateSessionType
(type: SessionType) => void
Updates session type in store and storage
listSessions
() => Observable<Session[]>
Lists all active sessions for the user

Error Handling

Facades handle errors gracefully and update store state:
login(payload: LoginPayload): Observable<User> {
  this.loginStore.start();

  return this.authService.login(payload).pipe(
    // ... business logic ...
    catchError((err: ApiError) => {
      this.loginStore.fail(err);
      this.authStore.setError(err);
      return throwError(() => err);
    }),
    finalize(() => this.loginStore.finish())
  );
}

Best Practices

Single Responsibility: Each facade should focus on a specific domain (auth, trips, users, etc.)
Observable APIs: Facades return Observables for async operations, allowing components to handle loading/error states reactively
Don’t Bypass Facades: Components should always use facades, never directly calling services or mutating stores

Testing Facades

Facades are easy to test by mocking dependencies:
describe('AuthFacade', () => {
  let facade: AuthFacade;
  let authService: jasmine.SpyObj<AuthService>;
  let authStore: jasmine.SpyObj<AuthStore>;

  beforeEach(() => {
    authService = jasmine.createSpyObj('AuthService', ['login', 'logout']);
    authStore = jasmine.createSpyObj('AuthStore', ['setAuth', 'clear']);

    facade = new AuthFacade(
      authStore,
      authService,
      // ... other mocks
    );
  });

  it('should login successfully', (done) => {
    authService.login.and.returnValue(of(mockLoginResponse));

    facade.login(mockCredentials).subscribe(user => {
      expect(authStore.setAuth).toHaveBeenCalled();
      expect(user.email).toBe('[email protected]');
      done();
    });
  });
});

TripActiveFacade

Provides read-only access to active trip state with computed selectors for UI consumption.

Public API

hasTrip
Signal<boolean>
Whether there is an active trip
status
Signal<ActivePhase | null>
Current trip phase (pending, assigning, accepted, arriving, in_progress, completed, cancelled)
tripId
Signal<string | null>
Active trip ID
driver
Signal<DriverSlimForPassenger | null>
Driver information (name, rating, phone)
vehicle
Signal<VehicleSlimForPassenger | null>
Vehicle information (plate, make, model, color, year)
driverName
Signal<string>
Driver’s name or ’—’ if unavailable
driverRating
Signal<number | null>
Driver’s average rating
plate
Signal<string>
Vehicle plate number or ’—’
vehicleTitle
Signal<string>
Formatted vehicle title (e.g., “Toyota Corolla · 2020”)
etaMinutes
Signal<number | null>
Estimated time of arrival in minutes
priceAmount
Signal<number | null>
Live fare amount with waiting penalties
waitingSeconds
Signal<number | null>
Total waiting time in seconds
waitingPenaltyApplied
Signal<boolean>
Whether waiting penalty has been applied
waitingPenaltyText
Signal<string | null>
Human-readable penalty message

Usage Example

src/app/features/tabs/trips/active/active.component.ts
import { inject, computed } from '@angular/core';
import { TripActiveFacade } from '@/app/store/trips/trip-active.facade';

export class ActiveComponent {
  private facade = inject(TripActiveFacade);

  // Computed view model for template
  vm = computed(() => {
    const hasTrip = this.facade.hasTrip();
    const status = this.facade.status();
    
    const waitingSeconds = this.facade.waitingSeconds() ?? 0;
    const mm = Math.floor(waitingSeconds / 60).toString().padStart(2, '0');
    const ss = (waitingSeconds % 60).toString().padStart(2, '0');
    const waitingTime = `${mm}:${ss}`;

    return {
      hasTrip,
      status,
      driverName: this.facade.driverName(),
      driverRating: this.facade.driverRating(),
      plate: this.facade.plate(),
      vehicleTitle: this.facade.vehicleTitle(),
      etaMinutes: this.facade.etaMinutes(),
      price: this.facade.priceAmount(),
      currency: this.facade.priceCurrency(),
      waitingTime,
      penaltyApplied: this.facade.waitingPenaltyApplied(),
      penaltyText: this.facade.waitingPenaltyText(),
    };
  });
}
Location: src/app/store/trips/trip-active.facade.ts

UsersFacade

Handles user registration and management operations.

Public API

getAllUsers
() => Signal<User[]>
Returns signal with all users in the store
getRegisterStatus
() => RegisterStatus
Returns registration status including loading, error, and form errors
interface RegisterStatus {
  status: Signal<'idle' | 'loading' | 'success' | 'error'>;
  loading: Signal<boolean>;
  error: Signal<ApiError | null>;
  formErrors: Signal<Record<string, string[]> | null>;
  lastRegisteredUserId: Signal<string | null>;
}
register
(formUser: CreateUserPayload, formCreds: CreateAuthCredentialsPayload) => Observable<User>
Registers a new user with credentials
  • Validates and builds registration payload
  • Calls UserService.register()
  • Updates users store on success
  • Shows success/error notifications
  • Returns created user

Usage Example

src/app/features/auth/register/register.component.ts
import { inject } from '@angular/core';
import { UserFacade } from '@/app/store/users/users.facade';
import { CreateUserPayload, CreateAuthCredentialsPayload } from '@/app/core/models/user/user.payload';

export class RegisterComponent {
  private usersFacade = inject(UserFacade);

  // Access registration state
  registerStatus = this.usersFacade.getRegisterStatus();

  onSubmit(userData: CreateUserPayload, credentials: CreateAuthCredentialsPayload) {
    this.usersFacade.register(userData, credentials).subscribe({
      next: (user) => {
        console.log('User registered:', user.id);
        // Navigate to login or auto-login
      },
      error: (err: ApiError) => {
        console.error('Registration failed:', err.message);
        // Error already shown via notification
      }
    });
  }
}
Location: src/app/store/users/users.facade.ts

Stores

Signal-based state stores

Effects

Side effect management

Services

HTTP and WebSocket services

State Management

Architecture overview

Build docs developers (and LLMs) love