20 minlesson

Step-by-Step Implementation Guide

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:

  1. Value Objects and Types (Foundation)
  2. Chain of Responsibility (Address Validation)
  3. Strategy Pattern (Rate Calculation)
  4. Factory Method (Carrier Services)
  5. Decorator Pattern (Shipping Options)
  6. State Pattern (Order Lifecycle)
  7. Observer Pattern (Tracking Notifications)
  8. Facade Pattern (Integration)

Phase 1: Foundation - Value Objects

Goal: Create type-safe value objects and core interfaces

What to build:

typescript
1// Value objects with behavior
2class 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 }
9
10 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 }
16
17 multiply(factor: number): Money {
18 return new Money(this.amount * factor, this.currency);
19 }
20
21 toString(): string {
22 return `${this.currency} ${this.amount.toFixed(2)}`;
23 }
24}
25
26class Weight {
27 constructor(
28 public readonly value: number,
29 public readonly unit: 'lb' | 'kg' | 'oz'
30 ) {}
31
32 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 }
39
40 toKg(): number {
41 return this.toLbs() / 2.20462;
42 }
43}
44
45class 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 ) {}
52
53 getVolume(): number {
54 return this.length * this.width * this.height;
55 }
56
57 getVolumetricWeight(): Weight {
58 const volume = this.unit === 'in'
59 ? this.getVolume()
60 : this.getVolume() / 2.54;
61 const dimWeight = volume / 166; // Standard divisor
62 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

typescript
1interface Address {
2 street: string;
3 street2?: string;
4 city: string;
5 state: string;
6 postalCode: string;
7 country: string;
8}
9
10interface ValidationResult {
11 valid: boolean;
12 errors: string[];
13 warnings: string[];
14}
15
16interface AddressValidator {
17 setNext(handler: AddressValidator): AddressValidator;
18 handle(address: Address, result?: ValidationResult): ValidationResult;
19}

Step 2: Create abstract base handler

typescript
1abstract class AbstractValidator implements AddressValidator {
2 private next: AddressValidator | null = null;
3
4 setNext(handler: AddressValidator): AddressValidator {
5 this.next = handler;
6 return handler; // Enable chaining
7 }
8
9 handle(address: Address, result?: ValidationResult): ValidationResult {
10 // Initialize result on first call
11 result = result || { valid: true, errors: [], warnings: [] };
12
13 // Run this validator
14 const { errors, warnings } = this.validate(address);
15 result.errors.push(...errors);
16 result.warnings.push(...warnings);
17
18 // Mark invalid if errors found
19 if (errors.length > 0) {
20 result.valid = false;
21 }
22
23 // Pass to next handler if exists
24 return this.next ? this.next.handle(address, result) : result;
25 }
26
27 protected abstract validate(address: Address): {
28 errors: string[];
29 warnings: string[];
30 };
31}

Step 3: Implement concrete validators

typescript
1class FormatValidator extends AbstractValidator {
2 protected validate(address: Address) {
3 const errors: string[] = [];
4 const warnings: string[] = [];
5
6 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');
11
12 return { errors, warnings };
13 }
14}
15
16class 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 };
22
23 protected validate(address: Address) {
24 const errors: string[] = [];
25 const pattern = PostalCodeValidator.PATTERNS[address.country];
26
27 if (pattern && !pattern.test(address.postalCode)) {
28 errors.push(`Invalid postal code format for ${address.country}`);
29 }
30
31 return { errors, warnings: [] };
32 }
33}

Step 4: Create fluent builder

typescript
1class ValidationChainBuilder {
2 private handlers: AddressValidator[] = [];
3
4 add(handler: AddressValidator): this {
5 this.handlers.push(handler);
6 return this;
7 }
8
9 build(): AddressValidator {
10 if (this.handlers.length === 0) {
11 throw new Error('Chain must have at least one handler');
12 }
13
14 // Chain handlers together
15 for (let i = 0; i < this.handlers.length - 1; i++) {
16 this.handlers[i].setNext(this.handlers[i + 1]);
17 }
18
19 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

typescript
1interface ShipmentDetails {
2 totalWeight: Weight;
3 dimensions: Dimensions;
4 origin: Address;
5 destination: Address;
6 declaredValue: Money;
7}
8
9interface ShippingRate {
10 carrier: string;
11 service: string;
12 cost: Money;
13 estimatedDays: number;
14 features: string[];
15}
16
17interface RateCalculationStrategy {
18 calculateRate(details: ShipmentDetails): ShippingRate;
19 readonly name: string;
20}

Step 2: Implement strategies

typescript
1class StandardRateStrategy implements RateCalculationStrategy {
2 readonly name = 'Standard';
3
4 calculateRate(details: ShipmentDetails): ShippingRate {
5 const weightLbs = details.totalWeight.toLbs();
6 let baseRate = 0;
7
8 // Weight-based tiers
9 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;
13
14 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}
23
24class ZoneBasedRateStrategy implements RateCalculationStrategy {
25 readonly name = 'Zone-Based';
26
27 calculateRate(details: ShipmentDetails): ShippingRate {
28 const zone = this.calculateZone(details.origin, details.destination);
29 const weightLbs = details.totalWeight.toLbs();
30
31 const baseRate = this.getZoneRate(zone, weightLbs);
32
33 return {
34 carrier: 'FedEx',
35 service: 'Ground',
36 cost: new Money(baseRate),
37 estimatedDays: zone,
38 features: ['Zone-optimized pricing']
39 };
40 }
41
42 private calculateZone(origin: Address, dest: Address): number {
43 // Simplified: use state comparison
44 return origin.state === dest.state ? 2 : 5;
45 }
46
47 private getZoneRate(zone: number, weight: number): number {
48 return 8.00 + (zone * 2) + (weight * 0.50);
49 }
50}
51
52class VolumetricRateStrategy implements RateCalculationStrategy {
53 readonly name = 'Volumetric';
54
55 calculateRate(details: ShipmentDetails): ShippingRate {
56 const actualWeight = details.totalWeight.toLbs();
57 const dimWeight = details.dimensions.getVolumetricWeight().toLbs();
58 const billableWeight = Math.max(actualWeight, dimWeight);
59
60 const baseRate = 12.00 + (billableWeight * 0.85);
61
62 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)

typescript
1class RateService {
2 constructor(private strategies: RateCalculationStrategy[]) {}
3
4 compareRates(details: ShipmentDetails): ShippingRate[] {
5 return this.strategies.map(strategy =>
6 strategy.calculateRate(details)
7 );
8 }
9
10 findBestRate(details: ShipmentDetails): ShippingRate {
11 const rates = this.compareRates(details);
12 return rates.reduce((best, current) =>
13 current.cost.amount < best.cost.amount ? current : best
14 );
15 }
16
17 findFastestRate(details: ShipmentDetails): ShippingRate {
18 const rates = this.compareRates(details);
19 return rates.reduce((fastest, current) =>
20 current.estimatedDays < fastest.estimatedDays ? current : fastest
21 );
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

typescript
1interface ShippingService {
2 calculateRate(details: ShipmentDetails): ShippingRate;
3 createShipment(details: ShipmentDetails): Shipment;
4 validateAddress(address: Address): boolean;
5}
6
7interface LabelGenerator {
8 generateLabel(shipment: Shipment): Label;
9 getFormat(): string;
10}
11
12interface Shipment {
13 id: string;
14 trackingNumber: string;
15 carrier: string;
16 service: string;
17}
18
19interface Label {
20 trackingNumber: string;
21 barcode: string;
22 format: string;
23 carrier: string;
24}

Step 2: Create abstract factory

typescript
1abstract class CarrierFactory {
2 abstract createService(): ShippingService;
3 abstract createLabelGenerator(): LabelGenerator;
4
5 // Template method using factory methods
6 processShipment(details: ShipmentDetails): ShipmentResult {
7 const service = this.createService();
8 const labelGen = this.createLabelGenerator();
9
10 const shipment = service.createShipment(details);
11 const label = labelGen.generateLabel(shipment);
12
13 return { shipment, label };
14 }
15}

Step 3: Implement concrete factories and products

typescript
1// USPS implementations
2class USPSService implements ShippingService {
3 calculateRate(details: ShipmentDetails): ShippingRate {
4 return new StandardRateStrategy().calculateRate(details);
5 }
6
7 createShipment(details: ShipmentDetails): Shipment {
8 return {
9 id: `USPS-${Date.now()}`,
10 trackingNumber: this.generateTracking(),
11 carrier: 'USPS',
12 service: 'Priority Mail'
13 };
14 }
15
16 validateAddress(address: Address): boolean {
17 return !!address.postalCode?.match(/^\d{5}(-\d{4})?$/);
18 }
19
20 private generateTracking(): string {
21 return '9400' + Math.random().toString().slice(2, 20).padEnd(18, '0');
22 }
23}
24
25class 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 }
34
35 getFormat(): string {
36 return '4x6 thermal';
37 }
38}
39
40class USPSFactory extends CarrierFactory {
41 createService(): ShippingService {
42 return new USPSService();
43 }
44
45 createLabelGenerator(): LabelGenerator {
46 return new USPSLabelGenerator();
47 }
48}

Step 4: Create factory registry

typescript
1class CarrierFactoryRegistry {
2 private static instance: CarrierFactoryRegistry;
3 private factories = new Map<string, CarrierFactory>();
4
5 private constructor() {}
6
7 static getInstance(): CarrierFactoryRegistry {
8 if (!this.instance) {
9 this.instance = new CarrierFactoryRegistry();
10 }
11 return this.instance;
12 }
13
14 register(name: string, factory: CarrierFactory): void {
15 this.factories.set(name.toLowerCase(), factory);
16 }
17
18 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 }
25
26 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

typescript
1interface ShipmentComponent {
2 getCost(): Money;
3 getDescription(): string;
4 getFeatures(): string[];
5}

Step 2: Create base component

typescript
1class BaseShipment implements ShipmentComponent {
2 constructor(
3 private rate: ShippingRate,
4 private details: ShipmentDetails
5 ) {}
6
7 getCost(): Money {
8 return this.rate.cost;
9 }
10
11 getDescription(): string {
12 return `${this.rate.carrier} ${this.rate.service}`;
13 }
14
15 getFeatures(): string[] {
16 return [...this.rate.features];
17 }
18}

Step 3: Create decorator base class

typescript
1abstract class ShipmentDecorator implements ShipmentComponent {
2 constructor(protected shipment: ShipmentComponent) {}
3
4 getCost(): Money {
5 return this.shipment.getCost();
6 }
7
8 getDescription(): string {
9 return this.shipment.getDescription();
10 }
11
12 getFeatures(): string[] {
13 return this.shipment.getFeatures();
14 }
15}

Step 4: Implement concrete decorators

typescript
1class InsuranceDecorator extends ShipmentDecorator {
2 constructor(
3 shipment: ShipmentComponent,
4 private declaredValue: Money
5 ) {
6 super(shipment);
7 }
8
9 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.amount
14 ? insuranceFee
15 : minFee;
16 return baseCost.add(actualFee);
17 }
18
19 getDescription(): string {
20 return `${super.getDescription()} + Insurance`;
21 }
22
23 getFeatures(): string[] {
24 return [...super.getFeatures(), `Insurance: ${this.declaredValue}`];
25 }
26}
27
28class SignatureDecorator extends ShipmentDecorator {
29 getCost(): Money {
30 return super.getCost().add(new Money(5.50));
31 }
32
33 getDescription(): string {
34 return `${super.getDescription()} + Signature Required`;
35 }
36
37 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

typescript
1class ShippingFacade {
2 constructor(
3 private addressValidator: AddressValidator,
4 private rateService: RateService,
5 private carrierRegistry: CarrierFactoryRegistry,
6 private stateManager: OrderStateManager,
7 private trackingNotifier: TrackingSubject
8 ) {}
9
10 async processOrder(order: Order): Promise<ShipmentResult> {
11 // Step 1: Validate
12 const validation = this.addressValidator.handle(order.shippingAddress);
13 if (!validation.valid) {
14 throw new ValidationError(validation.errors);
15 }
16
17 // Step 2: Get rates
18 const details = this.extractShipmentDetails(order);
19 const bestRate = this.rateService.findBestRate(details);
20
21 // Step 3: Create shipment
22 const factory = this.carrierRegistry.getFactory(bestRate.carrier);
23 let shipment: ShipmentComponent = new BaseShipment(bestRate, details);
24
25 // Step 4: Apply options
26 if (order.requireInsurance) {
27 shipment = new InsuranceDecorator(shipment, details.declaredValue);
28 }
29 if (order.requireSignature) {
30 shipment = new SignatureDecorator(shipment);
31 }
32
33 // Step 5: Update state
34 const context = this.stateManager.getContext(order.id);
35 context.process();
36
37 // Step 6: Generate label
38 const result = factory.processShipment(details);
39
40 // Step 7: Notify
41 this.trackingNotifier.notify({
42 orderId: order.id,
43 trackingNumber: result.label.trackingNumber,
44 status: 'Processing',
45 location: 'Origin facility',
46 timestamp: new Date()
47 });
48
49 return result;
50 }
51}

Common Pitfalls to Avoid

1. Breaking the Chain

typescript
1// WRONG - stops chain on first error
2if (errors.length > 0) {
3 return result; // Stops here!
4}
5
6// RIGHT - accumulate and continue
7result.errors.push(...errors);
8if (this.next) {
9 return this.next.handle(address, result);
10}

2. Mutating Decorated Objects

typescript
1// WRONG - modifies original
2getCost(): Money {
3 const cost = this.shipment.getCost();
4 cost.amount += 5; // Mutates!
5 return cost;
6}
7
8// RIGHT - creates new instance
9getCost(): Money {
10 return this.shipment.getCost().add(new Money(5));
11}

3. Forgetting to Register Factories

typescript
1// Initialize registry at startup
2const registry = CarrierFactoryRegistry.getInstance();
3registry.register('usps', new USPSFactory());
4registry.register('fedex', new FedExFactory());
5registry.register('ups', new UPSFactory());

4. Invalid State Transitions

typescript
1// WRONG - allows any transition
2setState(newState: OrderState) {
3 this.state = newState;
4}
5
6// RIGHT - validate in state
7process(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

typescript
1const testAddress: Address = {
2 street: '123 Main St',
3 city: 'New York',
4 state: 'NY',
5 postalCode: '10001',
6 country: 'US'
7};
8
9const 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:

  1. Clear implementation order
  2. Code examples for each pattern
  3. Understanding of integration points
  4. Common pitfalls to avoid
  5. Testing strategy

Next: Complete hands-on workshop with Vitest tests!