60 minlesson

Phase 3: Rate Calculation Engine with Design Patterns

Phase 3: Rate Calculation Engine with Design Patterns

Overview

This is the core of your application where design patterns solve real engineering challenges. You'll build a flexible rate calculation engine that integrates with multiple carrier APIs, applies various fees, and gracefully handles API failures. This phase demonstrates how to use the right patterns for the right problems.

Learning Objectives

By the end of this phase, you will:

  • Implement 4 design patterns in a cohesive architecture
  • Build a type-safe API adapter layer for multiple carriers
  • Apply decorators to stack fees dynamically
  • Handle multiple async operations with error recovery
  • Write unit tests for each pattern implementation

Requirements

1. Adapter Pattern - External API Integration

Pattern Purpose: Normalize different carrier API response formats into a consistent internal format. Each carrier (USPS, FedEx, UPS) returns rates in completely different structures—the Adapter pattern unifies them.

Review: TypeScript Design Patterns course, Topic 7 - Adapter Pattern

File to Create: src/adapters/carrier-adapters/adapter.ts

Target Interface:

Define CarrierAdapter interface that represents what your application expects:

  • fetchRates(request: RateRequest): Promise<ShippingRate[]> - Fetches and normalizes rates
  • trackPackage(trackingNumber: string): Promise<TrackingInfo> - Optional tracking method

Adaptee Interfaces (External API Response Types):

Create TypeScript interfaces representing the actual structure of each carrier's API responses:

  • USPSAPIResponse - Represents USPS XML-to-JSON structure
  • FedExAPIResponse - Represents FedEx JSON response structure
  • UPSAPIResponse - Represents UPS response structure

Note: Each carrier has a completely different response structure—this is why the Adapter pattern is essential!

Concrete Adapter Implementations:

USPSAdapter: Create adapter class for USPS that:

  • Implements CarrierAdapter interface
  • fetchRates() method:
    • Calls private callUSPSAPI() method to fetch raw API data
    • Passes response to adaptUSPSResponse() to convert to ShippingRate[]
  • callUSPSAPI() - Makes HTTP request to USPS API (for now, simulate with mock data and setTimeout)
  • adaptUSPSResponse() - Transforms USPS format to internal ShippingRate format:
    • Maps USPS service names to internal service codes
    • Parses string rates to numbers
    • Converts date strings to Date objects
    • Adds carrier-specific features array
  • Helper methods for service code mapping and speed determination
  • trackPackage() - Can throw "Not implemented" for now

FedExAdapter: Create adapter for FedEx with same structure but different transformation logic:

  • Different API structure (nested objects vs arrays)
  • Different field names (totalNetCharge vs Rate)
  • Different service type codes
  • Different date format
  • Different feature set

UPSAdapter: Create adapter for UPS following the same pattern with UPS-specific transformations.

Key Learning: Each adapter has different transformation logic but presents a uniform interface to the application.

2. Simple Factory Function - Adapter Creation

Pattern Purpose: Provide a clean API to obtain the correct adapter for a carrier without exposing instantiation details.

File to Create: src/adapters/carrier-adapters/index.ts

Factory Function:

Create a simple getCarrierAdapter(carrier: CarrierName): CarrierAdapter function that:

  • Maps carrier names to adapter instances
  • Returns the appropriate adapter for the given carrier
  • Throws error if carrier is not recognized
typescript
1const adapters: Record<CarrierName, CarrierAdapter> = {
2 USPS: new USPSAdapter(),
3 FedEx: new FedExAdapter(),
4 UPS: new UPSAdapter(),
5};
6
7export function getCarrierAdapter(carrier: CarrierName): CarrierAdapter {
8 const adapter = adapters[carrier];
9 if (!adapter) {
10 throw new Error(`Unknown carrier: ${carrier}`);
11 }
12 return adapter;
13}

Why Not Full Factory Pattern? A simple factory function is sufficient here because:

  • We only create one type of object (adapters)
  • No complex initialization logic required
  • No need for runtime algorithm swapping
  • Keeps the code simple and maintainable

3. Decorator Pattern - Additional Fees

Pattern Purpose: Dynamically add fees (insurance, signature, fragile handling) to the base rate without modifying the adapter classes.

Review: TypeScript Design Patterns course, Topic 8 - Decorator Pattern

File to Create: src/services/fee-decorators/decorator.ts

Component Interface:

Define RateComponent interface with methods:

  • getCost(): number - Returns total cost including all applied fees
  • getDescription(): string - Returns service description
  • getFees(): Fee[] - Returns array of all applied fees

Concrete Component (Base Rate):

Create BaseRate class implementing RateComponent:

  • Constructor accepts base amount and service name
  • getCost() returns the base amount
  • getDescription() returns service name
  • getFees() returns empty array (no additional fees)

Abstract Decorator:

Create abstract RateDecorator class that:

  • Implements RateComponent interface
  • Holds reference to wrapped RateComponent in protected field
  • Delegates all methods to the wrapped component by default
  • Allows subclasses to override and enhance behavior

Concrete Decorators:

Implement the following decorator classes, each extending RateDecorator:

1. InsuranceDecorator

  • Constructor accepts component and insured value
  • getCost() adds insurance fee to base cost
  • getFees() appends insurance fee to the fees array
  • Private method calculates insurance: $1 per $100 of value, minimum $2.50

2. SignatureDecorator

  • getCost() adds fixed $5.50 signature fee
  • getFees() appends signature fee to array

3. FragileHandlingDecorator

  • getCost() adds fixed $10.00 handling fee
  • getFees() appends fragile handling fee to array

4. SaturdayDeliveryDecorator

  • getCost() adds fixed $15.00 Saturday delivery fee
  • getFees() appends Saturday delivery fee to array

Helper Function:

Create applyFees(baseRate: RateComponent, options: ShippingOptions) function that:

  • Starts with the base rate component
  • Conditionally wraps it in decorators based on shipping options
  • Returns the fully decorated rate component
  • Allows stacking multiple decorators in sequence

4. Singleton Pattern - Configuration Management

Pattern Purpose: Ensure only one instance of configuration manager exists across the application.

Review: TypeScript Design Patterns course, Topic 6 - Singleton Pattern

File to Create: src/config/carrier-config.ts

Configuration Interfaces:

Define interfaces for:

  • CarrierCredentials - Contains apiKey, apiSecret, endpoint URL, and timeout value
  • CarrierConfiguration - Index signature mapping carrier names to credentials

Singleton Class Implementation:

Create CarrierConfigManager class with:

Private Static Instance:

  • Declare private static field to hold the single instance

Private Constructor:

  • Constructor must be private to prevent direct instantiation
  • Constructor calls loadConfiguration() to initialize config data

Static getInstance() Method:

  • Checks if instance exists; if not, creates it
  • Returns the single instance
  • This is the only way to obtain a CarrierConfigManager instance

Public Methods:

  • getCarrierCredentials(carrier: CarrierName): CarrierCredentials - Returns credentials for a specific carrier, throws error if not found

Private Methods:

  • loadConfiguration() - Loads carrier credentials from environment variables
    • Returns configuration object with USPS, FedEx, UPS, and DHL credentials
    • Each carrier has its API endpoint, keys, and timeout settings
    • Uses process.env to read environment variables

Module Export: Create and export a const instance using getInstance() for convenient application-wide access.

Why Singleton Here: Configuration should be loaded once and shared across all carrier adapters to avoid redundant environment variable reads and ensure consistency.

5. Orchestration - Parallel Rate Fetching

Goal: Tie all patterns together in a service that fetches rates from multiple carriers in parallel with error handling.

Review: JavaScript ES6+ Foundations course, Topic 5 - Async JavaScript (Promise.allSettled)

File to Create: src/services/rate-service.ts

Main Service Class:

Create RateService class with the following methods:

fetchAllRates(request: RateRequest): Promise<RateResponse>

Main orchestration method that:

  1. Generates unique request ID
  2. Determines which carriers to query (from request or use all)
  3. Creates array of promises, one per carrier, using fetchCarrierRate()
  4. Uses Promise.allSettled() to run all fetches in parallel and handle partial failures
  5. Processes results:
    • Fulfilled promises: add rates to rates array
    • Rejected promises: add error to errors array with carrier info and recoverability flag
  6. Sorts all rates by cost (cheapest first), then by delivery date
  7. Returns RateResponse with requestId, sorted rates, errors, and timestamp

fetchCarrierRate(carrier: CarrierName, request: RateRequest): Promise<ShippingRate[]>

Private method that fetches rates from a single carrier:

  1. Uses getCarrierAdapter(carrier) to get the appropriate adapter
  2. Calls adapter's fetchRates() method
  3. Maps over rates to apply additional fees using decorators
  4. Returns array of complete ShippingRate objects with fees applied
  5. Implements try-catch to call retryWithBackoff() on failure

applyAdditionalFees(rate: ShippingRate, options: ShippingOptions): ShippingRate

Private method that applies decorator pattern:

  1. Creates BaseRate component from the rate's base cost
  2. Calls applyFees() helper to wrap with appropriate decorators
  3. Returns updated rate with additionalFees array and totalCost from decorated component

retryWithBackoff(carrier: CarrierName, request: RateRequest, maxRetries: number): Promise<ShippingRate[]>

Private method implementing exponential backoff retry:

  1. Loop up to maxRetries attempts
  2. On each attempt, try to fetch rates
  3. On failure, wait for 2^attempt seconds before next try
  4. On final failure, re-throw error
  5. Return empty array if all retries exhausted

Helper Methods:

  • sortRates() - Sorts by cost, then delivery date
  • isRecoverableError() - Determines if error warrants retry (network errors vs validation errors)

Pattern Integration: This service brings together Adapter (API calls) and Decorator (fee application) patterns in a cohesive workflow.


Deliverables

By the end of Phase 3, you must have:

  • Adapter pattern for 3 carrier APIs (USPS, FedEx, UPS)
  • Simple factory function for adapter creation
  • Decorator pattern with 4 fee decorators
  • Singleton pattern for configuration management
  • RateService orchestrating parallel API calls
  • Error handling with retry logic (exponential backoff)
  • Unit tests for each pattern (minimum 80% coverage)
  • Integration tests for rate fetching workflow
  • Type-safe implementation (no any types)

Testing Requirements

Unit Tests by Pattern:

Adapter Pattern Tests (src/adapters/__tests__/):

  • Test each adapter normalizes carrier API responses correctly
  • Verify field mappings (rate amounts, dates, service codes)
  • Test service name to speed tier mapping
  • Ensure all adapters return consistent ShippingRate[] format
  • Mock carrier API responses for testing

Factory Function Tests (src/adapters/__tests__/):

  • Verify getCarrierAdapter() returns correct adapter for each carrier
  • Test with valid carrier names (USPS, FedEx, UPS)
  • Test error thrown for unknown carrier

Decorator Pattern Tests (src/services/fee-decorators/__tests__/):

  • Test each decorator adds correct fee amount
  • Verify decorators can be stacked in any order
  • Ensure getFees() returns all applied fees
  • Verify getCost() sums base rate + all fees
  • Test insurance calculation based on declared value
  • Ensure decorators don't modify the wrapped component

Singleton Pattern Tests (src/config/__tests__/):

  • Verify getInstance() always returns same instance
  • Test credentials retrieval for each carrier
  • Verify error thrown for unknown carrier
  • Test environment variable loading

Integration Tests (src/services/__tests__/):

  • Test RateService.fetchAllRates() with multiple carriers
  • Verify parallel fetching with Promise.allSettled()
  • Test partial failure handling (one carrier fails, others succeed)
  • Verify error categorization (recoverable vs non-recoverable)
  • Test retry logic with exponential backoff
  • Verify rate sorting (by cost, then delivery date)
  • Test decorator application in complete workflow

Validation Checklist

  • Adapters normalize different API responses correctly
  • Factory function returns correct adapter for each carrier
  • Decorators stack fees without modifying base classes
  • Singleton ensures single config instance
  • Parallel API calls work with Promise.allSettled
  • Error handling with retry logic functional
  • All tests pass with 80%+ coverage
  • No TypeScript errors
  • Code follows SOLID principles

Resources

  • Course: TypeScript Design Patterns (Topics 7, 8, 6)
  • Course: JavaScript ES6+ Foundations (Topic 5: Async JavaScript)
  • Gang of Four Design Patterns

Estimated Time

  • Adapter pattern: 4 hours
  • Decorator pattern: 3 hours
  • Singleton + orchestration: 2 hours
  • Testing: 3 hours
  • Total: 12 hours

Next Steps

Proceed to Phase 4: Results Display & User Experience where you'll build the React UI to display rates, implement comparison features, and add performance optimizations.

For carrier-specific API mapping reference, see:

  • Appendix A: FedEx Rate API Mapping (Topic 6)
  • Appendix B: UPS Rate API Mapping (Topic 7)