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 ratestrackPackage(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 structureFedExAPIResponse- Represents FedEx JSON response structureUPSAPIResponse- 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
CarrierAdapterinterface fetchRates()method:- Calls private
callUSPSAPI()method to fetch raw API data - Passes response to
adaptUSPSResponse()to convert toShippingRate[]
- Calls private
callUSPSAPI()- Makes HTTP request to USPS API (for now, simulate with mock data and setTimeout)adaptUSPSResponse()- Transforms USPS format to internalShippingRateformat:- 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
typescript1const adapters: Record<CarrierName, CarrierAdapter> = {2 USPS: new USPSAdapter(),3 FedEx: new FedExAdapter(),4 UPS: new UPSAdapter(),5};67export 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 feesgetDescription(): string- Returns service descriptiongetFees(): 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 amountgetDescription()returns service namegetFees()returns empty array (no additional fees)
Abstract Decorator:
Create abstract RateDecorator class that:
- Implements
RateComponentinterface - Holds reference to wrapped
RateComponentin 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 costgetFees()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 feegetFees()appends signature fee to array
3. FragileHandlingDecorator
getCost()adds fixed $10.00 handling feegetFees()appends fragile handling fee to array
4. SaturdayDeliveryDecorator
getCost()adds fixed $15.00 Saturday delivery feegetFees()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 valueCarrierConfiguration- 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
CarrierConfigManagerinstance
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:
- Generates unique request ID
- Determines which carriers to query (from request or use all)
- Creates array of promises, one per carrier, using
fetchCarrierRate() - Uses
Promise.allSettled()to run all fetches in parallel and handle partial failures - Processes results:
- Fulfilled promises: add rates to rates array
- Rejected promises: add error to errors array with carrier info and recoverability flag
- Sorts all rates by cost (cheapest first), then by delivery date
- Returns
RateResponsewith requestId, sorted rates, errors, and timestamp
fetchCarrierRate(carrier: CarrierName, request: RateRequest): Promise<ShippingRate[]>
Private method that fetches rates from a single carrier:
- Uses
getCarrierAdapter(carrier)to get the appropriate adapter - Calls adapter's
fetchRates()method - Maps over rates to apply additional fees using decorators
- Returns array of complete
ShippingRateobjects with fees applied - Implements try-catch to call
retryWithBackoff()on failure
applyAdditionalFees(rate: ShippingRate, options: ShippingOptions): ShippingRate
Private method that applies decorator pattern:
- Creates
BaseRatecomponent from the rate's base cost - Calls
applyFees()helper to wrap with appropriate decorators - Returns updated rate with
additionalFeesarray andtotalCostfrom decorated component
retryWithBackoff(carrier: CarrierName, request: RateRequest, maxRetries: number): Promise<ShippingRate[]>
Private method implementing exponential backoff retry:
- Loop up to maxRetries attempts
- On each attempt, try to fetch rates
- On failure, wait for 2^attempt seconds before next try
- On final failure, re-throw error
- Return empty array if all retries exhausted
Helper Methods:
sortRates()- Sorts by cost, then delivery dateisRecoverableError()- 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
anytypes)
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)