Step-by-Step Implementation Guide
A systematic approach to implementing the complete logistics system.
Implementation Strategy
You'll build the system in 7 phases, from simplest to most complex:
- Value Objects and Types (Foundation)
- Chain of Responsibility (Address Validation)
- Strategy Pattern (Rate Calculation)
- Factory Method (Carrier Services)
- Decorator Pattern (Shipping Options)
- State Pattern (Order Lifecycle)
- Observer Pattern (Tracking Notifications)
- Facade Pattern (Integration)
Phase 1: Foundation - Value Objects
Goal: Create type-safe value objects and core interfaces
What to build:
typescript1// Value objects with behavior2class Money {3 constructor(4 public readonly amount: number,5 public readonly currency: string = 'USD'6 ) {7 if (amount < 0) throw new Error('Amount cannot be negative');8 }910 add(other: Money): Money {11 if (this.currency !== other.currency) {12 throw new Error('Currency mismatch');13 }14 return new Money(this.amount + other.amount, this.currency);15 }1617 multiply(factor: number): Money {18 return new Money(this.amount * factor, this.currency);19 }2021 toString(): string {22 return `${this.currency} ${this.amount.toFixed(2)}`;23 }24}2526class Weight {27 constructor(28 public readonly value: number,29 public readonly unit: 'lb' | 'kg' | 'oz'30 ) {}3132 toLbs(): number {33 switch (this.unit) {34 case 'lb': return this.value;35 case 'kg': return this.value * 2.20462;36 case 'oz': return this.value / 16;37 }38 }3940 toKg(): number {41 return this.toLbs() / 2.20462;42 }43}4445class Dimensions {46 constructor(47 public readonly length: number,48 public readonly width: number,49 public readonly height: number,50 public readonly unit: 'in' | 'cm'51 ) {}5253 getVolume(): number {54 return this.length * this.width * this.height;55 }5657 getVolumetricWeight(): Weight {58 const volume = this.unit === 'in'59 ? this.getVolume()60 : this.getVolume() / 2.54;61 const dimWeight = volume / 166; // Standard divisor62 return new Weight(dimWeight, 'lb');63 }64}
Why start here:
- Value objects are immutable and have no dependencies
- They're used throughout the system
- Easy to test in isolation
Tests to write:
- Money addition with same/different currencies
- Weight conversion between units
- Volumetric weight calculation
Phase 2: Chain of Responsibility - Address Validation
Goal: Build a flexible validation chain that accumulates errors
Implementation order:
Step 1: Define interfaces
typescript1interface Address {2 street: string;3 street2?: string;4 city: string;5 state: string;6 postalCode: string;7 country: string;8}910interface ValidationResult {11 valid: boolean;12 errors: string[];13 warnings: string[];14}1516interface AddressValidator {17 setNext(handler: AddressValidator): AddressValidator;18 handle(address: Address, result?: ValidationResult): ValidationResult;19}
Step 2: Create abstract base handler
typescript1abstract class AbstractValidator implements AddressValidator {2 private next: AddressValidator | null = null;34 setNext(handler: AddressValidator): AddressValidator {5 this.next = handler;6 return handler; // Enable chaining7 }89 handle(address: Address, result?: ValidationResult): ValidationResult {10 // Initialize result on first call11 result = result || { valid: true, errors: [], warnings: [] };1213 // Run this validator14 const { errors, warnings } = this.validate(address);15 result.errors.push(...errors);16 result.warnings.push(...warnings);1718 // Mark invalid if errors found19 if (errors.length > 0) {20 result.valid = false;21 }2223 // Pass to next handler if exists24 return this.next ? this.next.handle(address, result) : result;25 }2627 protected abstract validate(address: Address): {28 errors: string[];29 warnings: string[];30 };31}
Step 3: Implement concrete validators
typescript1class FormatValidator extends AbstractValidator {2 protected validate(address: Address) {3 const errors: string[] = [];4 const warnings: string[] = [];56 if (!address.street?.trim()) errors.push('Street is required');7 if (!address.city?.trim()) errors.push('City is required');8 if (!address.postalCode?.trim()) errors.push('Postal code is required');9 if (!address.country?.trim()) errors.push('Country is required');10 if (!address.state?.trim()) warnings.push('State is recommended');1112 return { errors, warnings };13 }14}1516class PostalCodeValidator extends AbstractValidator {17 private static readonly PATTERNS: Record<string, RegExp> = {18 'US': /^\d{5}(-\d{4})?$/,19 'CA': /^[A-Z]\d[A-Z] \d[A-Z]\d$/i,20 'UK': /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i,21 };2223 protected validate(address: Address) {24 const errors: string[] = [];25 const pattern = PostalCodeValidator.PATTERNS[address.country];2627 if (pattern && !pattern.test(address.postalCode)) {28 errors.push(`Invalid postal code format for ${address.country}`);29 }3031 return { errors, warnings: [] };32 }33}
Step 4: Create fluent builder
typescript1class ValidationChainBuilder {2 private handlers: AddressValidator[] = [];34 add(handler: AddressValidator): this {5 this.handlers.push(handler);6 return this;7 }89 build(): AddressValidator {10 if (this.handlers.length === 0) {11 throw new Error('Chain must have at least one handler');12 }1314 // Chain handlers together15 for (let i = 0; i < this.handlers.length - 1; i++) {16 this.handlers[i].setNext(this.handlers[i + 1]);17 }1819 return this.handlers[0];20 }21}
Key testing points:
- Each validator in isolation
- Chain accumulates errors from all validators
- Chain continues even after errors
- Builder creates proper chain
Phase 3: Strategy Pattern - Rate Calculation
Goal: Calculate shipping rates using different algorithms
Step 1: Define strategy interface
typescript1interface ShipmentDetails {2 totalWeight: Weight;3 dimensions: Dimensions;4 origin: Address;5 destination: Address;6 declaredValue: Money;7}89interface ShippingRate {10 carrier: string;11 service: string;12 cost: Money;13 estimatedDays: number;14 features: string[];15}1617interface RateCalculationStrategy {18 calculateRate(details: ShipmentDetails): ShippingRate;19 readonly name: string;20}
Step 2: Implement strategies
typescript1class StandardRateStrategy implements RateCalculationStrategy {2 readonly name = 'Standard';34 calculateRate(details: ShipmentDetails): ShippingRate {5 const weightLbs = details.totalWeight.toLbs();6 let baseRate = 0;78 // Weight-based tiers9 if (weightLbs <= 1) baseRate = 5.99;10 else if (weightLbs <= 5) baseRate = 9.99;11 else if (weightLbs <= 10) baseRate = 14.99;12 else baseRate = 14.99 + (weightLbs - 10) * 0.75;1314 return {15 carrier: 'USPS',16 service: 'Priority Mail',17 cost: new Money(baseRate),18 estimatedDays: 3,19 features: ['Tracking included', 'Insurance up to $50']20 };21 }22}2324class ZoneBasedRateStrategy implements RateCalculationStrategy {25 readonly name = 'Zone-Based';2627 calculateRate(details: ShipmentDetails): ShippingRate {28 const zone = this.calculateZone(details.origin, details.destination);29 const weightLbs = details.totalWeight.toLbs();3031 const baseRate = this.getZoneRate(zone, weightLbs);3233 return {34 carrier: 'FedEx',35 service: 'Ground',36 cost: new Money(baseRate),37 estimatedDays: zone,38 features: ['Zone-optimized pricing']39 };40 }4142 private calculateZone(origin: Address, dest: Address): number {43 // Simplified: use state comparison44 return origin.state === dest.state ? 2 : 5;45 }4647 private getZoneRate(zone: number, weight: number): number {48 return 8.00 + (zone * 2) + (weight * 0.50);49 }50}5152class VolumetricRateStrategy implements RateCalculationStrategy {53 readonly name = 'Volumetric';5455 calculateRate(details: ShipmentDetails): ShippingRate {56 const actualWeight = details.totalWeight.toLbs();57 const dimWeight = details.dimensions.getVolumetricWeight().toLbs();58 const billableWeight = Math.max(actualWeight, dimWeight);5960 const baseRate = 12.00 + (billableWeight * 0.85);6162 return {63 carrier: 'UPS',64 service: 'Ground',65 cost: new Money(baseRate),66 estimatedDays: 4,67 features: ['Dimensional pricing', 'Volume optimization']68 };69 }70}
Step 3: Create context (RateService)
typescript1class RateService {2 constructor(private strategies: RateCalculationStrategy[]) {}34 compareRates(details: ShipmentDetails): ShippingRate[] {5 return this.strategies.map(strategy =>6 strategy.calculateRate(details)7 );8 }910 findBestRate(details: ShipmentDetails): ShippingRate {11 const rates = this.compareRates(details);12 return rates.reduce((best, current) =>13 current.cost.amount < best.cost.amount ? current : best14 );15 }1617 findFastestRate(details: ShipmentDetails): ShippingRate {18 const rates = this.compareRates(details);19 return rates.reduce((fastest, current) =>20 current.estimatedDays < fastest.estimatedDays ? current : fastest21 );22 }23}
Testing strategy:
- Each strategy with known inputs → expected outputs
- Compare rates returns all strategies
- Best rate selection logic
Phase 4: Factory Method - Carrier Services
Goal: Create carrier-specific services without tight coupling
Step 1: Define product interfaces
typescript1interface ShippingService {2 calculateRate(details: ShipmentDetails): ShippingRate;3 createShipment(details: ShipmentDetails): Shipment;4 validateAddress(address: Address): boolean;5}67interface LabelGenerator {8 generateLabel(shipment: Shipment): Label;9 getFormat(): string;10}1112interface Shipment {13 id: string;14 trackingNumber: string;15 carrier: string;16 service: string;17}1819interface Label {20 trackingNumber: string;21 barcode: string;22 format: string;23 carrier: string;24}
Step 2: Create abstract factory
typescript1abstract class CarrierFactory {2 abstract createService(): ShippingService;3 abstract createLabelGenerator(): LabelGenerator;45 // Template method using factory methods6 processShipment(details: ShipmentDetails): ShipmentResult {7 const service = this.createService();8 const labelGen = this.createLabelGenerator();910 const shipment = service.createShipment(details);11 const label = labelGen.generateLabel(shipment);1213 return { shipment, label };14 }15}
Step 3: Implement concrete factories and products
typescript1// USPS implementations2class USPSService implements ShippingService {3 calculateRate(details: ShipmentDetails): ShippingRate {4 return new StandardRateStrategy().calculateRate(details);5 }67 createShipment(details: ShipmentDetails): Shipment {8 return {9 id: `USPS-${Date.now()}`,10 trackingNumber: this.generateTracking(),11 carrier: 'USPS',12 service: 'Priority Mail'13 };14 }1516 validateAddress(address: Address): boolean {17 return !!address.postalCode?.match(/^\d{5}(-\d{4})?$/);18 }1920 private generateTracking(): string {21 return '9400' + Math.random().toString().slice(2, 20).padEnd(18, '0');22 }23}2425class USPSLabelGenerator implements LabelGenerator {26 generateLabel(shipment: Shipment): Label {27 return {28 trackingNumber: shipment.trackingNumber,29 barcode: `*${shipment.trackingNumber}*`,30 format: '4x6',31 carrier: 'USPS'32 };33 }3435 getFormat(): string {36 return '4x6 thermal';37 }38}3940class USPSFactory extends CarrierFactory {41 createService(): ShippingService {42 return new USPSService();43 }4445 createLabelGenerator(): LabelGenerator {46 return new USPSLabelGenerator();47 }48}
Step 4: Create factory registry
typescript1class CarrierFactoryRegistry {2 private static instance: CarrierFactoryRegistry;3 private factories = new Map<string, CarrierFactory>();45 private constructor() {}67 static getInstance(): CarrierFactoryRegistry {8 if (!this.instance) {9 this.instance = new CarrierFactoryRegistry();10 }11 return this.instance;12 }1314 register(name: string, factory: CarrierFactory): void {15 this.factories.set(name.toLowerCase(), factory);16 }1718 getFactory(name: string): CarrierFactory {19 const factory = this.factories.get(name.toLowerCase());20 if (!factory) {21 throw new Error(`No factory registered for carrier: ${name}`);22 }23 return factory;24 }2526 getSupportedCarriers(): string[] {27 return Array.from(this.factories.keys());28 }29}
Common pitfall: Don't create factories inside the factory methods! Create products.
Phase 5: Decorator Pattern - Shipping Options
Goal: Add optional features to shipments flexibly
Step 1: Define component interface
typescript1interface ShipmentComponent {2 getCost(): Money;3 getDescription(): string;4 getFeatures(): string[];5}
Step 2: Create base component
typescript1class BaseShipment implements ShipmentComponent {2 constructor(3 private rate: ShippingRate,4 private details: ShipmentDetails5 ) {}67 getCost(): Money {8 return this.rate.cost;9 }1011 getDescription(): string {12 return `${this.rate.carrier} ${this.rate.service}`;13 }1415 getFeatures(): string[] {16 return [...this.rate.features];17 }18}
Step 3: Create decorator base class
typescript1abstract class ShipmentDecorator implements ShipmentComponent {2 constructor(protected shipment: ShipmentComponent) {}34 getCost(): Money {5 return this.shipment.getCost();6 }78 getDescription(): string {9 return this.shipment.getDescription();10 }1112 getFeatures(): string[] {13 return this.shipment.getFeatures();14 }15}
Step 4: Implement concrete decorators
typescript1class InsuranceDecorator extends ShipmentDecorator {2 constructor(3 shipment: ShipmentComponent,4 private declaredValue: Money5 ) {6 super(shipment);7 }89 getCost(): Money {10 const baseCost = super.getCost();11 const insuranceFee = this.declaredValue.multiply(0.01); // 1%12 const minFee = new Money(2.50);13 const actualFee = insuranceFee.amount > minFee.amount14 ? insuranceFee15 : minFee;16 return baseCost.add(actualFee);17 }1819 getDescription(): string {20 return `${super.getDescription()} + Insurance`;21 }2223 getFeatures(): string[] {24 return [...super.getFeatures(), `Insurance: ${this.declaredValue}`];25 }26}2728class SignatureDecorator extends ShipmentDecorator {29 getCost(): Money {30 return super.getCost().add(new Money(5.50));31 }3233 getDescription(): string {34 return `${super.getDescription()} + Signature Required`;35 }3637 getFeatures(): string[] {38 return [...super.getFeatures(), 'Signature confirmation'];39 }40}
Testing focus: Cost accumulation through multiple decorators
Phase 6: State Pattern - Order Lifecycle
Goal: Manage order state transitions with validation
Implementation in workshop
You'll implement 5 states with enforced transitions. Key points:
- Each state knows its valid next states
- Invalid transitions throw errors
- States can have entry/exit actions
- Context delegates to current state
Phase 7: Observer Pattern - Tracking Notifications
Goal: Decouple tracking events from notification delivery
Implementation in workshop
You'll implement a subject/observer system with:
- Multiple observer types (email, SMS, webhook)
- Attach/detach at runtime
- Batch notifications for efficiency
- Error handling in individual observers
Phase 8: Facade Pattern - Integration
Goal: Provide simple API that coordinates all subsystems
Step-by-step integration
typescript1class ShippingFacade {2 constructor(3 private addressValidator: AddressValidator,4 private rateService: RateService,5 private carrierRegistry: CarrierFactoryRegistry,6 private stateManager: OrderStateManager,7 private trackingNotifier: TrackingSubject8 ) {}910 async processOrder(order: Order): Promise<ShipmentResult> {11 // Step 1: Validate12 const validation = this.addressValidator.handle(order.shippingAddress);13 if (!validation.valid) {14 throw new ValidationError(validation.errors);15 }1617 // Step 2: Get rates18 const details = this.extractShipmentDetails(order);19 const bestRate = this.rateService.findBestRate(details);2021 // Step 3: Create shipment22 const factory = this.carrierRegistry.getFactory(bestRate.carrier);23 let shipment: ShipmentComponent = new BaseShipment(bestRate, details);2425 // Step 4: Apply options26 if (order.requireInsurance) {27 shipment = new InsuranceDecorator(shipment, details.declaredValue);28 }29 if (order.requireSignature) {30 shipment = new SignatureDecorator(shipment);31 }3233 // Step 5: Update state34 const context = this.stateManager.getContext(order.id);35 context.process();3637 // Step 6: Generate label38 const result = factory.processShipment(details);3940 // Step 7: Notify41 this.trackingNotifier.notify({42 orderId: order.id,43 trackingNumber: result.label.trackingNumber,44 status: 'Processing',45 location: 'Origin facility',46 timestamp: new Date()47 });4849 return result;50 }51}
Common Pitfalls to Avoid
1. Breaking the Chain
typescript1// WRONG - stops chain on first error2if (errors.length > 0) {3 return result; // Stops here!4}56// RIGHT - accumulate and continue7result.errors.push(...errors);8if (this.next) {9 return this.next.handle(address, result);10}
2. Mutating Decorated Objects
typescript1// WRONG - modifies original2getCost(): Money {3 const cost = this.shipment.getCost();4 cost.amount += 5; // Mutates!5 return cost;6}78// RIGHT - creates new instance9getCost(): Money {10 return this.shipment.getCost().add(new Money(5));11}
3. Forgetting to Register Factories
typescript1// Initialize registry at startup2const registry = CarrierFactoryRegistry.getInstance();3registry.register('usps', new USPSFactory());4registry.register('fedex', new FedExFactory());5registry.register('ups', new UPSFactory());
4. Invalid State Transitions
typescript1// WRONG - allows any transition2setState(newState: OrderState) {3 this.state = newState;4}56// RIGHT - validate in state7process(context: OrderContext) {8 if (!(this instanceof PendingState)) {9 throw new Error('Cannot process non-pending order');10 }11 context.setState(new ProcessingState());12}
Testing Strategy
Unit Tests
- Each validator independently
- Each strategy with known inputs
- Each factory creates correct products
- Each decorator adds correct cost
- Each state handles transitions
Integration Tests
- Complete workflow from order to shipment
- Multiple decorators stacked
- State transitions through lifecycle
- Observer notifications triggered
Test Data
typescript1const testAddress: Address = {2 street: '123 Main St',3 city: 'New York',4 state: 'NY',5 postalCode: '10001',6 country: 'US'7};89const testDetails: ShipmentDetails = {10 totalWeight: new Weight(5, 'lb'),11 dimensions: new Dimensions(12, 8, 6, 'in'),12 origin: warehouseAddress,13 destination: testAddress,14 declaredValue: new Money(100)15};
Ready for the Workshop?
You now have:
- Clear implementation order
- Code examples for each pattern
- Understanding of integration points
- Common pitfalls to avoid
- Testing strategy
Next: Complete hands-on workshop with Vitest tests!