Skip to content

Architectural & Design Patterns Catalog

This document catalogs all architectural and design patterns employed in the Forma 3D Connect codebase. It explains the rationale behind each pattern choice and demonstrates how they work together to create a maintainable, scalable, and robust system.


Table of Contents

  1. System Overview
  2. Architectural Patterns
  3. Layered Architecture
  4. Modular Monolith
  5. Event-Driven Architecture
  6. CQRS-Lite
  7. Structural Design Patterns
  8. Repository Pattern
  9. Service Layer Pattern
  10. Gateway Pattern
  11. Anti-Corruption Layer
  12. Behavioral Design Patterns
  13. Observer Pattern (Event Emitter)
  14. State Machine Pattern
  15. Strategy Pattern
  16. Command Pattern
  17. Integration Patterns
  18. Webhook Handler Pattern
  19. Retry with Exponential Backoff
  20. Circuit Breaker (Implicit)
  21. Idempotency Pattern
  22. Resilience Patterns
  23. Dead Letter Queue (Retry Queue)
  24. Saga Pattern (Orchestration)
  25. Correlation ID Pattern
  26. Frontend Patterns
  27. Container/Presentational Components
  28. Custom Hooks Pattern
  29. Context Provider Pattern
  30. Optimistic Updates
  31. Cross-Cutting Patterns
  32. Dependency Injection
  33. Interface Segregation
  34. DTO Pattern
  35. How Patterns Work Together
  36. Pattern Decision Matrix

System Overview

Forma 3D Connect is a print-on-demand order orchestration system that bridges Shopify e-commerce, 3D printing (SimplyPrint), and shipping (SendCloud). The architecture must handle:

  • Asynchronous workflows: Orders flow through multiple stages over hours/days
  • External system integration: Three external APIs with different reliability characteristics
  • Real-time updates: Operators need live dashboards
  • Fault tolerance: Network failures and API outages are expected

uml diagram


Architectural Patterns

Layered Architecture

Pattern: Organize code into horizontal layers with strict dependency rules.

Implementation: The codebase uses a 4-layer architecture:

uml diagram

Rationale: - Testability: Each layer can be tested in isolation with mocked dependencies - Maintainability: Changes to external APIs don't ripple to business logic - Clear boundaries: Import rules prevent architectural erosion

Code Example (from orders.service.ts):

@Injectable()
export class OrdersService implements IOrdersService {
  constructor(
    private readonly ordersRepository: OrdersRepository,  // Infrastructure
    private readonly eventEmitter: EventEmitter2,         // Cross-cutting
    private readonly eventLogService: EventLogService,    // Application
  ) {}

  // Service coordinates between layers
  async createFromShopify(input: CreateOrderInput): Promise<Order> {
    const order = await this.ordersRepository.create(input);  // Infrastructure
    this.eventEmitter.emit(ORDER_EVENTS.CREATED, ...);        // Application
    return order;
  }
}

Modular Monolith

Pattern: Structure a monolith as loosely-coupled modules that could be extracted to microservices.

Implementation: Each domain is a self-contained NestJS module:

uml diagram

Module Boundaries (from app.module.ts):

@Module({
  imports: [
    // Infrastructure & Cross-Cutting
    EventEmitterModule.forRoot(),
    CorrelationModule,

    // Core Domain
    OrdersModule,
    PrintJobsModule,
    ProductMappingsModule,

    // Integration
    ShopifyModule,
    SimplyPrintModule,
    SendcloudModule,

    // Coordination
    OrchestrationModule,
    FulfillmentModule,
  ],
})
export class AppModule {}

Rationale: - Future microservices path: Modules can be extracted when scale requires - Team autonomy: Different developers can own different modules - Bounded contexts: Each module encapsulates its domain logic


Event-Driven Architecture

Pattern: Components communicate through events rather than direct calls.

Implementation: NestJS EventEmitter2 for in-process events:

uml diagram

Event Definition (from order.events.ts):

export const ORDER_EVENTS = {
  CREATED: 'order.created',
  STATUS_CHANGED: 'order.status_changed',
  CANCELLED: 'order.cancelled',
  READY_FOR_FULFILLMENT: 'order.ready-for-fulfillment',
} as const;

export class OrderCreatedEvent implements OrderEvent {
  constructor(
    public readonly correlationId: string,
    public readonly timestamp: Date,
    public readonly source: string,
    public readonly orderId: string,
    public readonly shopifyOrderId: string,
    public readonly lineItemCount: number
  ) {}

  static create(...): OrderCreatedEvent {
    return new OrderCreatedEvent(correlationId, new Date(), 'orders', ...);
  }
}

Rationale: - Loose coupling: Services don't need to know about all consumers - Extensibility: New features can subscribe to existing events - Auditability: Events naturally create an audit trail - Real-time updates: WebSocket gateway subscribes to same events as services


CQRS-Lite

Pattern: Separate read and write paths without full CQRS complexity.

Implementation: - Writes go through services with full validation and events - Reads can bypass services for simple queries

uml diagram

Rationale: - Performance: Read operations skip unnecessary business logic - Simplicity: Not a full CQRS with separate read models, but gains most benefits - Evolution path: Can add read replicas or materialized views later


Structural Design Patterns

Repository Pattern

Pattern: Abstract data access behind a domain-specific interface.

Implementation (from orders.repository.ts):

@Injectable()
export class OrdersRepository {
  constructor(private readonly prisma: PrismaService) {}

  async create(input: CreateOrderInput): Promise<OrderWithLineItems> {
    return this.prisma.order.create({
      data: { ... },
      include: { lineItems: true },
    });
  }

  async findById(id: string): Promise<OrderWithLineItems | null> {
    return this.prisma.order.findUnique({
      where: { id },
      include: { lineItems: true },
    });
  }

  async updateStatus(id: string, status: OrderStatus): Promise<Order> {
    return this.prisma.order.update({
      where: { id },
      data: { status },
    });
  }
}

uml diagram

Rationale: - Testability: Services can be tested with mock repositories - Abstraction: Business logic doesn't know about Prisma - Query encapsulation: Complex queries live in one place


Service Layer Pattern

Pattern: Encapsulate business logic in services that coordinate between components.

Implementation: Services are the "brain" of each module:

uml diagram

Key Responsibilities:

@Injectable()
export class OrdersService {
  // 1. Business rule enforcement
  private validateStatusTransition(from: OrderStatus, to: OrderStatus): void {
    const validTransitions: Record<OrderStatus, OrderStatus[]> = {
      [OrderStatus.PENDING]: [OrderStatus.PROCESSING, OrderStatus.CANCELLED],
      // ... 
    };
    if (!validTransitions[from].includes(to)) {
      throw new ConflictException(`Invalid transition from ${from} to ${to}`);
    }
  }

  // 2. Coordination of operations
  async updateStatus(id: string, newStatus: OrderStatus): Promise<Order> {
    const order = await this.findById(id);
    this.validateStatusTransition(order.status, newStatus);
    await this.ordersRepository.updateStatus(id, newStatus);
    await this.eventLogService.log({ ... });
    this.eventEmitter.emit(ORDER_EVENTS.STATUS_CHANGED, ...);
    return this.findById(id);
  }
}

Gateway Pattern

Pattern: Single entry point for external API communication with consistent error handling.

Implementation (from shopify-api.client.ts):

@Injectable()
export class ShopifyApiClient {
  private async request<T>(
    method: 'GET' | 'POST' | 'PUT' | 'DELETE',
    endpoint: string,
    body?: unknown
  ): Promise<T> {
    // Rate limiting handling
    if (response.status === 429) {
      const retryAfter = response.headers.get('Retry-After');
      await this.delay(parseInt(retryAfter) * 1000);
      return this.request<T>(method, endpoint, body);  // Retry
    }

    // Error normalization
    if (!response.ok) {
      throw new Error(`Shopify API error: ${response.status}`);
    }

    return response.json();
  }

  async createFulfillment(orderId: string, data: FulfillmentInput) {
    return this.request<FulfillmentResponse>(
      'POST',
      `/orders/${orderId}/fulfillments.json`,
      { fulfillment: data }
    );
  }
}

uml diagram


Anti-Corruption Layer

Pattern: Translate between external system models and internal domain models.

Implementation: DTOs and mapping methods isolate external data shapes:

uml diagram

Example (from shopify.service.ts):

// External Shopify format
interface ShopifyWebhookPayload {
  id: number;                    // External uses number
  order_number: string;          // snake_case
  line_items: Array<{
    sku: string;
    quantity: number;
  }>;
}

// Internal domain format
interface CreateOrderInput {
  shopifyOrderId: string;        // String for our IDs
  shopifyOrderNumber: string;    // camelCase
  lineItems: CreateLineItemInput[];
}

// Mapping in ShopifyService
async handleOrderCreated(payload: ShopifyWebhookPayload): Promise<void> {
  const input: CreateOrderInput = {
    shopifyOrderId: String(payload.id),
    shopifyOrderNumber: payload.order_number,
    lineItems: payload.line_items.map(li => ({
      shopifyLineItemId: String(li.id),
      productSku: li.sku,
      quantity: li.quantity,
    })),
  };
  await this.ordersService.createFromShopify(input);
}

Behavioral Design Patterns

Observer Pattern (Event Emitter)

Pattern: Objects subscribe to events without knowing the publisher.

Implementation: Three observer implementations:

uml diagram

Gateway as Observer (from events.gateway.ts):

@WebSocketGateway({ namespace: '/events' })
export class EventsGateway {
  @WebSocketServer()
  server!: Server;

  @OnEvent(ORDER_EVENTS.CREATED)
  handleOrderCreated(event: OrderEventPayload): void {
    this.server.emit('order:created', {
      id: event.orderId,
      orderNumber: event.shopifyOrderNumber,
    });
  }

  @OnEvent(PRINT_JOB_EVENTS.STATUS_CHANGED)
  handlePrintJobStatusChanged(event: PrintJobEventPayload): void {
    this.server.emit('printjob:updated', {
      id: event.printJob.id,
      status: event.newStatus,
    });
  }
}

State Machine Pattern

Pattern: Enforce valid state transitions for entities with complex lifecycles.

Implementation: Order and PrintJob status transitions are validated:

uml diagram

Validation Logic (from orders.service.ts):

private validateStatusTransition(from: OrderStatus, to: OrderStatus): void {
  const validTransitions: Record<OrderStatus, OrderStatus[]> = {
    [OrderStatus.PENDING]: [
      OrderStatus.PROCESSING,
      OrderStatus.CANCELLED,
      OrderStatus.FAILED,
    ],
    [OrderStatus.PROCESSING]: [
      OrderStatus.PARTIALLY_COMPLETED,
      OrderStatus.COMPLETED,
      OrderStatus.FAILED,
      OrderStatus.CANCELLED,
    ],
    [OrderStatus.COMPLETED]: [],  // Terminal state
    [OrderStatus.CANCELLED]: [],  // Terminal state
    [OrderStatus.FAILED]: [OrderStatus.PENDING],  // Can retry
  };

  if (!validTransitions[from].includes(to)) {
    throw new ConflictException(
      `Invalid status transition from ${from} to ${to}`
    );
  }
}

Rationale: - Data integrity: Prevents invalid states in the database - Self-documenting: State machine diagram matches code exactly - Business rule enforcement: No "jumping" to completed without processing


Strategy Pattern

Pattern: Define a family of algorithms and make them interchangeable.

Implementation: Retry job processor uses strategies for different job types:

uml diagram

Implementation (from retry-queue.processor.ts):

private async processJob(job: RetryQueue): Promise<void> {
  switch (job.jobType) {
    case RetryJobType.FULFILLMENT:
      await this.processFulfillmentRetry(job);
      break;

    case RetryJobType.PRINT_JOB_CREATION:
      await this.processPrintJobRetry(job);
      break;

    case RetryJobType.NOTIFICATION:
      await this.processNotificationRetry(job);
      break;
  }
}

private async processFulfillmentRetry(job: RetryQueue): Promise<void> {
  const payload = job.payload as { orderId: string };
  await this.fulfillmentService.createFulfillment({ orderId: payload.orderId });
}

Command Pattern

Pattern: Encapsulate requests as objects with all information needed for execution.

Implementation: Event objects are commands with factory methods:

export class OrderCreatedEvent implements OrderEvent {
  constructor(
    public readonly correlationId: string,
    public readonly timestamp: Date,
    public readonly source: string,
    public readonly orderId: string,
    public readonly shopifyOrderId: string,
    public readonly lineItemCount: number
  ) {}

  // Factory method ensures consistent creation
  static create(
    correlationId: string,
    orderId: string,
    shopifyOrderId: string,
    lineItemCount: number
  ): OrderCreatedEvent {
    return new OrderCreatedEvent(
      correlationId,
      new Date(),
      'orders',
      orderId,
      shopifyOrderId,
      lineItemCount
    );
  }
}

Integration Patterns

Webhook Handler Pattern

Pattern: Secure endpoint that validates and processes external system callbacks.

uml diagram

Implementation (from shopify-webhook.guard.ts):

@Injectable()
export class ShopifyWebhookGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const hmacHeader = request.headers['x-shopify-hmac-sha256'];
    const rawBody = request.rawBody;

    const computedHmac = crypto
      .createHmac('sha256', this.webhookSecret)
      .update(rawBody)
      .digest('base64');

    return crypto.timingSafeEqual(
      Buffer.from(hmacHeader),
      Buffer.from(computedHmac)
    );
  }
}

Retry with Exponential Backoff

Pattern: Retry failed operations with increasing delays.

uml diagram


Idempotency Pattern

Pattern: Operations can be safely retried without side effects.

Implementation (from orders.service.ts):

async createFromShopify(input: CreateOrderInput): Promise<Order> {
  // Check for existing order (idempotency)
  const existing = await this.ordersRepository.findByShopifyOrderId(
    input.shopifyOrderId
  );

  if (existing) {
    this.logger.debug(
      `Order ${input.shopifyOrderId} already exists, skipping creation`
    );
    return existing;  // Return existing, don't create duplicate
  }

  // Create new order only if not exists
  const order = await this.ordersRepository.create(input);
  // ...
}

uml diagram


Resilience Patterns

Dead Letter Queue (Retry Queue)

Pattern: Store failed operations for later retry with visibility into failures.

uml diagram

Schema:

interface RetryQueue {
  id: string;
  jobType: RetryJobType;
  payload: JsonValue;
  status: RetryStatus;
  attempts: number;
  maxAttempts: number;
  lastError: string | null;
  nextRunAt: Date;
  createdAt: Date;
  updatedAt: Date;
}

Saga Pattern (Orchestration)

Pattern: Coordinate long-running transactions across multiple services.

Implementation: OrchestrationService coordinates the order lifecycle:

uml diagram

Key Orchestration Logic (from orchestration.service.ts):

@OnEvent(ORDER_EVENTS.CREATED)
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
  // Update order status
  await this.ordersService.updateStatus(event.orderId, OrderStatus.PROCESSING);

  // Get line items and create print jobs
  const lineItems = await this.ordersService.getLineItems(event.orderId);
  for (const lineItem of lineItems) {
    await this.printJobsService.createPrintJobsForLineItem(lineItem);
  }
}

@OnEvent(PRINT_JOB_EVENTS.COMPLETED)
async handlePrintJobCompleted(event: PrintJobCompletedEvent): Promise<void> {
  // Check if order is complete
  await this.checkOrderCompletion(event.orderId);
}

private async checkOrderCompletion(orderId: string): Promise<void> {
  const printJobs = await this.printJobsInterface.findByOrderId(orderId);
  const statusCounts = this.countJobStatuses(printJobs);

  if (statusCounts.completed === printJobs.length) {
    await this.markOrderReadyForFulfillment(orderId);
  } else if (statusCounts.failed > 0 && statusCounts.pending === 0) {
    await this.markOrderPartiallyCompleted(orderId, statusCounts);
  }
}

Correlation ID Pattern

Pattern: Track requests across async boundaries with a unique identifier.

uml diagram

Implementation (from correlation.service.ts):

@Injectable({ scope: Scope.DEFAULT })
export class CorrelationService {
  private readonly storage = new AsyncLocalStorage<CorrelationContext>();

  runWithContext<T>(correlationId: string, fn: () => T): T {
    return this.storage.run({ correlationId }, fn);
  }

  getOrCreateCorrelationId(): string {
    return this.getCorrelationId() ?? randomUUID();
  }
}

Frontend Patterns

Container/Presentational Components

Pattern: Separate data fetching from UI rendering.

uml diagram


Custom Hooks Pattern

Pattern: Extract and reuse stateful logic across components.

Implementation (from use-orders.ts):

// Read hook - data fetching
export function useOrders(query: OrdersQuery = {}) {
  return useQuery({
    queryKey: ['orders', query],
    queryFn: () => apiClient.orders.list(query),
  });
}

// Single entity hook
export function useOrder(id: string) {
  return useQuery({
    queryKey: ['orders', id],
    queryFn: () => apiClient.orders.get(id),
    enabled: !!id,
  });
}

// Mutation hook with cache invalidation
export function useCancelOrder() {
  const queryClient = useQueryClient();
  const { apiKey } = useAuth();

  return useMutation({
    mutationFn: (orderId: string) => 
      apiClient.orders.cancel(orderId, apiKey),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['orders'] });
    },
  });
}

uml diagram


Context Provider Pattern

Pattern: Share state across component tree without prop drilling.

Implementation (from socket-context.tsx):

const SocketContext = createContext<SocketContextType>({
  socket: null,
  isConnected: false,
});

export function SocketProvider({ children }: { children: ReactNode }) {
  const [socket, setSocket] = useState<Socket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const queryClient = useQueryClient();

  useEffect(() => {
    const socketInstance = io(`${SOCKET_URL}/events`);

    // Auto-invalidate queries on events
    socketInstance.on('order:created', () => {
      toast.success(`New order received!`);
      queryClient.invalidateQueries({ queryKey: ['orders'] });
    });

    socketInstance.on('printjob:updated', () => {
      queryClient.invalidateQueries({ queryKey: ['orders'] });
    });

    return () => socketInstance.disconnect();
  }, [queryClient]);

  return (
    <SocketContext.Provider value={{ socket, isConnected }}>
      {children}
    </SocketContext.Provider>
  );
}

export function useSocket() {
  return useContext(SocketContext);
}

uml diagram


Cross-Cutting Patterns

Dependency Injection

Pattern: Components receive dependencies through constructor rather than creating them.

Implementation: NestJS DI container with custom tokens:

// Define injection token
export const ORDERS_SERVICE = Symbol('IOrdersService');

// Register provider
@Module({
  providers: [
    {
      provide: ORDERS_SERVICE,
      useClass: OrdersService,
    },
  ],
  exports: [ORDERS_SERVICE],
})
export class OrdersModule {}

// Inject by token
@Injectable()
export class OrchestrationService {
  constructor(
    @Inject(ORDERS_SERVICE)
    private readonly ordersService: IOrdersService,
  ) {}
}

uml diagram


Interface Segregation

Pattern: Define focused interfaces that clients actually need.

Implementation (from domain-contracts):

// Focused interface for cross-domain communication
export interface IOrdersService {
  findById(id: string): Promise<OrderDto | null>;
  updateStatus(id: string, status: OrderStatus): Promise<OrderDto>;
  getLineItems(orderId: string): Promise<LineItemDto[]>;
  updateTracking(id: string, trackingNumber: string, trackingUrl: string | null): Promise<OrderDto>;
}

// Separate interface for fulfillment concerns
export interface IFulfillmentService {
  createFulfillment(input: FulfillmentInput): Promise<void>;
  isFulfilled(orderId: string): Promise<boolean>;
}

// Separate interface for print job concerns
export interface IPrintJobsService {
  findByOrderId(orderId: string): Promise<PrintJobDto[]>;
  updateStatus(id: string, status: PrintJobStatus): Promise<PrintJobDto>;
}

DTO Pattern

Pattern: Use dedicated objects for data transfer between layers.

uml diagram


How Patterns Work Together

The following diagram shows how patterns interact in a typical order flow:

uml diagram


Pattern Decision Matrix

Concern Pattern Why This Pattern
Code organization Layered Architecture Clear boundaries, testable layers
Module boundaries Modular Monolith Future microservices path, team autonomy
Service communication Event-Driven Loose coupling, extensibility
Data access Repository Testability, query encapsulation
External APIs Gateway + ACL Isolation from external changes
Entity lifecycle State Machine Data integrity, clear transitions
Failed operations Retry Queue + DLQ Resilience, visibility
Long transactions Saga (Orchestration) Coordination, compensation
Request tracing Correlation ID Debugging, observability
Frontend data React Query + Hooks Caching, consistency
Real-time updates WebSocket + Observer Live dashboard, notifications
Duplicate requests Idempotency Safe retries, webhook handling

Appendix: Pattern Interactions Cheat Sheet

External Request → Guard → Controller → Service → Repository → Database
                                ↓
                           EventEmitter → [Orchestrator, Gateway, Logger]
                                              ↓
                                          RetryQueue (on failure)

Key Invariants:

  1. Controllers never access repositories directly
  2. Services emit events after successful operations
  3. Repositories don't emit events (that's the service's job)
  4. External API clients handle their own retry logic
  5. Orchestration service coordinates multi-step workflows
  6. WebSocket gateway only subscribes, never initiates operations