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¶
- System Overview
- Architectural Patterns
- Layered Architecture
- Microservices Architecture
- Event-Driven Architecture
- CQRS-Lite
- Structural Design Patterns
- Repository Pattern
- Service Layer Pattern
- Gateway Pattern
- Anti-Corruption Layer
- Behavioral Design Patterns
- Observer Pattern (Event Emitter)
- State Machine Pattern
- Strategy Pattern
- Command Pattern
- Integration Patterns
- Webhook Handler Pattern
- Retry with Exponential Backoff
- Circuit Breaker (Implicit)
- Idempotency Pattern
- Resilience Patterns
- Dead Letter Queue (Retry Queue)
- Saga Pattern (Orchestration)
- Correlation ID Pattern
- Frontend Patterns
- Container/Presentational Components
- Custom Hooks Pattern
- Context Provider Pattern
- Optimistic Updates
- Cross-Cutting Patterns
- Dependency Injection
- Interface Segregation
- DTO Pattern
- How Patterns Work Together
- Pattern Decision Matrix
System Overview¶
Forma 3D Connect is a print-on-demand order orchestration platform that bridges Shopify e-commerce, 3D printing (SimplyPrint), and shipping (SendCloud). The system is deployed as domain-aligned microservices communicating via BullMQ event queues (Redis) and internal HTTP APIs. The architecture must handle:
- Asynchronous workflows: Orders flow through multiple stages over hours/days
- External system integration: Multiple external APIs with different reliability characteristics
- Real-time updates: Operators need live dashboards
- Fault tolerance: Network failures and API outages are expected
- Multi-tenancy: Each tenant has isolated credentials and data context
Architectural Patterns¶
Layered Architecture¶
Pattern: Organize code into horizontal layers with strict dependency rules.
Implementation: The codebase uses a 4-layer architecture:
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 apps/order-service/src/orders/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;
}
}
Microservices Architecture¶
Pattern: Decompose the system into domain-aligned services that communicate via event queues and internal HTTP APIs.
Implementation: Each domain runs as an independent NestJS application, routed through an API Gateway:
Gateway Routing (from apps/gateway/src/routing/route-config.ts):
export const ROUTE_TARGETS: RouteTarget[] = [
// Webhooks — bypass session auth, routed to respective services
{ pathPrefix: '/api/v1/webhooks/shopify', serviceUrlEnvKey: 'ORDER_SERVICE_URL', isPublic: true },
{ pathPrefix: '/webhooks/simplyprint', serviceUrlEnvKey: 'PRINT_SERVICE_URL', isPublic: true },
{ pathPrefix: '/webhooks/sendcloud', serviceUrlEnvKey: 'SHIPPING_SERVICE_URL', isPublic: true },
// Domain routes
{ pathPrefix: '/api/v1/orders', serviceUrlEnvKey: 'ORDER_SERVICE_URL', isPublic: false },
{ pathPrefix: '/api/v1/print-jobs', serviceUrlEnvKey: 'PRINT_SERVICE_URL', isPublic: false },
{ pathPrefix: '/api/v1/shipments', serviceUrlEnvKey: 'SHIPPING_SERVICE_URL', isPublic: false },
{ pathPrefix: '/api/v1/gridflock', serviceUrlEnvKey: 'GRIDFLOCK_SERVICE_URL', isPublic: false },
// ... additional routes
];
Service Module Boundaries (from apps/order-service/src/app/app.module.ts):
@Module({
imports: [
// Infrastructure & Cross-Cutting
EventEmitterModule.forRoot(),
ScheduleModule.forRoot(),
DatabaseModule,
CorrelationModule,
// BullMQ Event Bus (cross-service communication)
EventBusModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
redisUrl: configService.get<string>('REDIS_URL') || 'redis://localhost:6379',
}),
inject: [ConfigService],
}),
// Service clients (for calling other microservices)
InternalAuthModule,
ServiceClientModule,
// Auth & Tenancy (user context from gateway headers)
SharedAuthModule.forRoot(),
TenancyModule,
// Core Domain
OrdersModule,
PrintJobsModule,
OrchestrationModule,
FulfillmentModule,
CancellationModule,
InventoryModule,
// BullMQ Event Subscribers
EventsModule,
],
})
export class AppModule {}
Rationale:
- Fault isolation: SimplyPrint API down? Only Print Service is affected
- Independent scaling: Print job processing can scale separately from order intake
- Domain ownership: Each service owns its domain logic, events, and external integrations
- Bounded contexts: Services communicate via well-defined event contracts and shared interfaces
Event-Driven Architecture¶
Pattern: Services communicate through events rather than direct calls, using a dual-layer event system.
Implementation: Two event layers work together:
- BullMQ (cross-service): Redis-backed queues for reliable inter-service communication with retry, persistence, and exactly-once delivery
- EventEmitter2 (intra-service): In-process events for local domain coordination within a single service
Bridge services (EventPublisherService / EventSubscriberService) connect the two layers:
Cross-Service Event Types (from libs/service-common/src/lib/events/event-types.ts):
export const SERVICE_EVENTS = {
// Order Service publishes
ORDER_CREATED: 'order.created',
ORDER_READY_FOR_FULFILLMENT: 'order.ready-for-fulfillment',
ORDER_CANCELLED: 'order.cancelled',
// Print Service publishes
PRINT_JOB_COMPLETED: 'print-job.completed',
PRINT_JOB_FAILED: 'print-job.failed',
PRINT_JOB_STATUS_CHANGED: 'print-job.status-changed',
PRINT_JOB_CANCELLED: 'print-job.cancelled',
// Shipping Service publishes
SHIPMENT_CREATED: 'shipment.created',
SHIPMENT_STATUS_CHANGED: 'shipment.status-changed',
// GridFlock Service publishes
GRIDFLOCK_MAPPING_READY: 'gridflock.mapping-ready',
GRIDFLOCK_PIPELINE_FAILED: 'gridflock.pipeline-failed',
} as const;
export interface ServiceEvent {
eventId: string; // UUID v4 — enables idempotency checks
eventType: string; // Queue name (e.g. 'order.created')
source: string; // Source service (e.g. 'order-service')
tenantId: string; // Multi-tenancy context
timestamp: string; // ISO 8601
correlationId?: string; // Distributed tracing
}
Event Bridge — Publisher (from apps/order-service/src/events/event-publisher.service.ts):
@Injectable()
export class EventPublisherService {
constructor(@Inject(EVENT_BUS) private readonly eventBus: IEventBus) {}
@OnEvent(ORDER_EVENTS.CREATED)
async onOrderCreated(payload: OrderCreatedPayload): Promise<void> {
const event: OrderCreatedEvent = {
eventId: randomUUID(),
eventType: SERVICE_EVENTS.ORDER_CREATED,
source: 'order-service',
tenantId: payload.tenantId,
orderId: payload.orderId,
lineItems: payload.lineItems || [],
timestamp: new Date().toISOString(),
correlationId: payload.correlationId,
};
await this.eventBus.publish(event);
}
}
Rationale:
- Loose coupling: Services don't need to know about all consumers
- Reliability: BullMQ provides retry (3 attempts, exponential backoff), persistence, and dead-letter handling
- Exactly-once processing: BullMQ workers ensure no duplicate event processing across replicas
- Auditability: Events carry
eventId,correlationId, andtenantIdfor full traceability - Real-time updates: WebSocket gateway subscribes to same local events as domain handlers
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
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 apps/order-service/src/orders/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 },
});
}
}
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:
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 apps/order-service/src/shopify/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 }
);
}
}
Anti-Corruption Layer¶
Pattern: Translate between external system models and internal domain models.
Implementation: DTOs and mapping methods isolate external data shapes:
Example (from apps/order-service/src/shopify/shopify.service.ts):
// External Shopify format
interface ShopifyWebhookPayload {
id: number; // External uses number
order_number: string; // snake_case
line_items: Array<{
sku: string | null;
product_id: number;
variant_id: number;
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),
shopifyProductId: String(li.product_id),
shopifyVariantId: String(li.variant_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: Two observer layers — local (EventEmitter2) and cross-service (BullMQ):
Gateway as Observer (from apps/order-service/src/gateway/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:
Validation Logic (from apps/order-service/src/orders/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.PARTIALLY_COMPLETED]: [
OrderStatus.PROCESSING,
OrderStatus.COMPLETED,
OrderStatus.FAILED,
OrderStatus.CANCELLED,
],
[OrderStatus.COMPLETED]: [], // Terminal state
[OrderStatus.FAILED]: [
OrderStatus.PENDING, // Manual retry
OrderStatus.PROCESSING, // Auto-revive via requeued job
],
[OrderStatus.CANCELLED]: [
OrderStatus.PROCESSING, // Auto-revive via requeued job
],
};
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:
Implementation (from apps/order-service/src/retry-queue/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.
Implementation (from apps/order-service/src/shopify/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.
Idempotency Pattern¶
Pattern: Operations can be safely retried without side effects.
Implementation (from apps/order-service/src/orders/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);
// ...
}
Resilience Patterns¶
Dead Letter Queue (Retry Queue)¶
Pattern: Store failed operations for later retry with visibility into failures.
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 in the Order Service coordinates the order lifecycle. It reacts to both local events (order creation) and cross-service events (print job completion from Print Service, delivered via BullMQ → EventSubscriber → EventEmitter2):
Key Orchestration Logic (from apps/order-service/src/orchestration/orchestration.service.ts):
@Injectable()
export class OrchestrationService {
constructor(
@Inject(ORDERS_SERVICE)
private readonly ordersService: IOrdersService,
private readonly printJobsService: PrintJobsService,
private readonly eventEmitter: EventEmitter2,
private readonly correlationService: CorrelationService,
) {}
@OnEvent(ORDER_EVENTS.CREATED)
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.processNewOrder(event.orderId);
}
@OnEvent(PRINT_JOB_EVENTS.COMPLETED)
async handlePrintJobCompleted(event: PrintJobCompletedEvent): Promise<void> {
await this.checkOrderCompletion(event.orderId);
}
private async checkOrderCompletion(orderId: string): Promise<void> {
const printJobs = await this.printJobsService.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.
Implementation (from apps/order-service/src/common/correlation/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.
Custom Hooks Pattern¶
Pattern: Extract and reuse stateful logic across components.
Implementation (from apps/web/src/hooks/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'] });
},
});
}
Context Provider Pattern¶
Pattern: Share state across component tree without prop drilling.
Implementation (from apps/web/src/contexts/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);
}
Cross-Cutting Patterns¶
Dependency Injection¶
Pattern: Components receive dependencies through constructor rather than creating them.
Implementation: NestJS DI container with custom tokens from @forma3d/domain-contracts:
// Define injection token (libs/domain-contracts)
export const ORDERS_SERVICE = Symbol('IOrdersService');
// Register provider (apps/order-service)
@Module({
providers: [
{
provide: ORDERS_SERVICE,
useClass: OrdersService,
},
],
exports: [ORDERS_SERVICE],
})
export class OrdersModule {}
// Inject by token (apps/order-service/src/orchestration)
@Injectable()
export class OrchestrationService {
constructor(
@Inject(ORDERS_SERVICE)
private readonly ordersService: IOrdersService,
) {}
}
Interface Segregation¶
Pattern: Define focused interfaces that clients actually need.
Implementation (from libs/domain-contracts/src/):
// 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.
How Patterns Work Together¶
The following diagram shows how patterns interact in a typical order flow across services:
Pattern Decision Matrix¶
| Concern | Pattern | Why This Pattern |
|---|---|---|
| Code organization | Layered Architecture | Clear boundaries, testable layers |
| Service boundaries | Microservices | Fault isolation, independent scaling, domain ownership |
| Service communication | Event-Driven (BullMQ + EventEmitter2) | Loose coupling, reliability, 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 → Gateway (proxy) → Service Controller → Service → Repository → Database
↓
EventEmitter2 (local) → [Orchestrator, WebSocket, Logger]
↓
EventPublisher → BullMQ (Redis) → Other Services
↓
EventSubscriber → EventEmitter2 → Domain Handlers
Key Invariants:
- Controllers never access repositories directly
- Services emit local events (EventEmitter2) after successful operations
- EventPublisher bridges local events to BullMQ for cross-service delivery
- EventSubscriber receives BullMQ events and re-emits as local EventEmitter2 events
- Repositories don't emit events (that's the service's job)
- External API clients handle their own retry logic
- Orchestration service (in Order Service) coordinates multi-step workflows across service boundaries
- WebSocket gateway only subscribes, never initiates operations
- All cross-service events carry
tenantId,eventId,correlationId, andsource