Loading learning content...
So far, we've explored domain events within a single bounded context—how they're raised by aggregates, dispatched, and handled. But real-world systems are composed of multiple bounded contexts, each with its own ubiquitous language, its own model, and its own deployment.\n\nHow do these contexts communicate? How does the Orders context tell the Shipping context that an order is ready? How does the Payments context inform the Orders context that payment succeeded?\n\nIntegration Events are domain events specifically designed to cross bounded context boundaries. They form the backbone of inter-service communication in modern distributed systems, enabling loose coupling while maintaining semantic clarity.
By the end of this page, you will understand the difference between internal domain events and integration events, how to design integration events for cross-context communication, patterns for translating between domain and integration events, event-driven architecture principles, and practical strategies for building event-based integrations.
A critical distinction that often gets overlooked is the difference between internal domain events and integration events (also called public events or external events). While related, they serve different purposes and have different design considerations.
Why the distinction matters:\n\nIf you publish every internal domain event as-is to other contexts, you create tight coupling. Changes to your internal model break external consumers. You expose implementation details that others shouldn't depend on.\n\nIntegration events act as a public API for your bounded context. They're the events you choose to share, with schemas you commit to maintaining. This separation gives you freedom to evolve internally while providing stability externally.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
/** * INTERNAL DOMAIN EVENT * * Rich with internal details, uses internal types. * May change freely with refactoring. */class OrderPlaced_Internal implements DomainEvent { readonly eventType = 'OrderPlaced'; constructor( readonly eventId: string, readonly occurredAt: Date, readonly orderId: string, readonly customerId: string, readonly items: ReadonlyArray<{ productId: string; productName: string; sku: string; // Internal detail quantity: number; unitPrice: Money; warehouseLocation: string; // Internal routing detail }>, readonly totalAmount: Money, readonly appliedPromotions: AppliedPromotion[], // Internal detail readonly fraudScore: number, // Very internal readonly customerTier: CustomerTier, readonly internalNotes: string, // Never expose ) {}} /** * INTEGRATION EVENT * * Carefully designed public contract. * Stable, minimal, focused on what other contexts need. */namespace Integration { export interface OrderPlaced { readonly eventType: 'integration.orders.OrderPlaced'; readonly eventVersion: 1; readonly eventId: string; readonly occurredAt: string; // ISO string for cross-language compat // What other contexts need to know readonly orderId: string; readonly customerId: string; readonly orderTotal: { amount: number; currency: string; // String, not enum - easier cross-language }; readonly itemCount: number; // Summary, not full details readonly estimatedShipDate: string; // Exclude internal details like: // - fraudScore (security) // - internalNotes (not relevant) // - warehouseLocation (internal routing) // - appliedPromotions (internal discounting logic) }} /** * Mapper that translates internal to integration event. * * This is where you control what gets exposed. */class OrderPlacedIntegrationMapper { translate( internal: OrderPlaced_Internal ): Integration.OrderPlaced { return { eventType: 'integration.orders.OrderPlaced', eventVersion: 1, eventId: internal.eventId, occurredAt: internal.occurredAt.toISOString(), orderId: internal.orderId, customerId: internal.customerId, orderTotal: { amount: internal.totalAmount.amount, currency: internal.totalAmount.currency.toString(), }, itemCount: internal.items.length, estimatedShipDate: this.calculateEstimatedShipDate(internal), }; } private calculateEstimatedShipDate(order: OrderPlaced_Internal): string { // Internal logic, exposed as simple date const baseDate = new Date(); baseDate.setDate(baseDate.getDate() + 3); return baseDate.toISOString().split('T')[0]; }}The mapper acts as an anti-corruption layer. It protects external consumers from internal changes and protects your internal model from external requirements. If an external consumer needs more data, you add it to the integration event—not the internal event.
Integration events require more careful design than internal events because they represent a public contract. Consumers in different contexts, possibly maintained by different teams, will depend on these events. Breaking changes have real costs.
orders.OrderPlaced, payments.PaymentReceived. Prevents collisions and makes origin clear.eventVersion for evolution. Consumers can handle multiple versions.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
/** * Well-designed Integration Event Schema * * Used as a TypeScript interface and serialized to JSON for wire. */namespace IntegrationEvents { /** * Base structure for all integration events. * Provides consistent metadata across contexts. */ interface IntegrationEventBase { /** Fully qualified event type with namespace */ readonly eventType: string; /** Schema version for evolution */ readonly eventVersion: number; /** Unique identifier for deduplication */ readonly eventId: string; /** When the event occurred (ISO-8601) */ readonly occurredAt: string; /** Correlation ID for distributed tracing */ readonly correlationId?: string; } /** * OrderPlaced Integration Event (v1) * * Published by: Orders context * Consumed by: Shipping, Analytics, Loyalty, Fraud */ export interface OrderPlaced_V1 extends IntegrationEventBase { eventType: 'orders.OrderPlaced'; eventVersion: 1; // Core identifiers orderId: string; customerId: string; // Summary information (not full details) orderSummary: { totalAmount: number; currency: string; itemCount: number; productCategories: string[]; // For routing/analytics }; // Shipping-relevant data shipping: { address: { country: string; postalCode: string; city: string; // Full address might be in separate secure channel }; requestedDeliveryDate?: string; }; // Customer context (non-sensitive) customer: { email: string; // For notifications tier: string; // Gold, Silver, Bronze isFirstOrder: boolean; }; } /** * PaymentReceived Integration Event * * Published by: Payments context * Consumed by: Orders, Billing, Finance */ export interface PaymentReceived_V1 extends IntegrationEventBase { eventType: 'payments.PaymentReceived'; eventVersion: 1; paymentId: string; orderId: string; // Links back to order payment: { amount: number; currency: string; method: string; // 'CARD' | 'BANK' | 'WALLET' processedAt: string; }; // Don't expose: card numbers, bank accounts, fraud scores } /** * InventoryReserved Integration Event * * Published by: Inventory context * Consumed by: Orders, Shipping */ export interface InventoryReserved_V1 extends IntegrationEventBase { eventType: 'inventory.InventoryReserved'; eventVersion: 1; reservationId: string; orderId: string; items: Array<{ productId: string; quantity: number; warehouseId: string; // Shipping needs to know where to pick }>; expectedPickDate: string; }} /** * Schema documentation - critical for integration events */const OrderPlacedDocs = { eventType: 'orders.OrderPlaced', version: 1, description: 'Published when a customer successfully places an order', producer: 'Orders Service', consumers: ['Shipping Service', 'Analytics Service', 'Loyalty Service'], guarantees: { delivery: 'At least once, within 5 minutes of order placement', ordering: 'No ordering guarantees across different orders', }, breaking_changes: { policy: 'Never remove fields; deprecate and add new version', deprecation: '3 month notice before removing deprecated fields', }, schema_evolution: { v1: 'Initial version (2023-01-01)', v2: 'Added customer.loyaltyPoints (2024-03-15)', },};Integration events often flow through message queues, get logged, and may be stored for replay. Never include sensitive data like passwords, full credit card numbers, or health records. Use separate secure channels for sensitive information.
Each bounded context has its own ubiquitous language. The concept of 'Customer' in the Sales context might differ from 'Customer' in the Support context. When events cross boundaries, translation is needed.\n\nContext Mapping Patterns define how contexts relate to each other and how translation happens:
| Pattern | Description | When to Use |
|---|---|---|
| Published Language | Contexts agree on a shared language for integration | When multiple teams need a stable contract |
| Customer/Supplier | Upstream supplies events; downstream consumes as-is | Clear dependency direction; supplier sets terms |
| Conformist | Downstream adopts upstream's model without translation | Limited resources; upstream model is good enough |
| Anti-Corruption Layer | Downstream translates upstream events to its own model | Protecting domain from external influence |
| Open Host Service | Publishers provide well-documented, stable event API | Many consumers; need stable integration points |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
/** * Anti-Corruption Layer Pattern * * The Shipping context translates Orders context events * into its own domain language. */namespace OrdersContext { // Orders context uses 'Order' and 'OrderItem' export interface OrderPlaced { orderId: string; customerId: string; items: Array<{ productId: string; quantity: number; unitPrice: number; }>; shippingAddress: Address; }} namespace ShippingContext { // Shipping context uses 'Shipment' and 'Package' interface Shipment { shipmentId: string; orderId: string; // Reference to Orders context recipient: Recipient; packages: Package[]; status: ShipmentStatus; } interface Package { packageId: string; items: PackageItem[]; weight: Weight; dimensions: Dimensions; } /** * Anti-Corruption Layer: Translates OrderPlaced to Shipment */ class OrderEventTranslator { constructor( private productCatalog: ProductCatalog, private packingService: PackingService, ) {} async translateOrderPlaced( event: OrdersContext.OrderPlaced ): Promise<CreateShipmentCommand> { // Translate Order items to Package items const packageItems = await Promise.all( event.items.map(async (item) => { const product = await this.productCatalog.findById( item.productId ); return { productId: item.productId, quantity: item.quantity, weight: product.shippingWeight, dimensions: product.shippingDimensions, isFragile: product.isFragile, }; }) ); // Determine optimal packaging const packages = await this.packingService.planPackages( packageItems ); // Translate address to Recipient const recipient = this.translateAddress(event.shippingAddress); // Return command in Shipping context language return new CreateShipmentCommand({ orderId: event.orderId, recipient, packages, priority: this.determinePriority(event), }); } private translateAddress(address: Address): Recipient { return { name: address.fullName, address: { line1: address.street, line2: address.apartment, city: address.city, state: address.state, postalCode: address.postalCode, country: address.country, }, phone: address.phone, }; } } /** * Event handler that uses the translator */ class OrderPlacedHandler { constructor( private translator: OrderEventTranslator, private commandBus: CommandBus, ) {} async handle(event: OrdersContext.OrderPlaced): Promise<void> { // Translate external event to internal command const command = await this.translator.translateOrderPlaced(event); // Execute in Shipping context's own language await this.commandBus.dispatch(command); } }} /** * Published Language Pattern * * Multiple contexts agree on a shared event schema for analytics. */namespace SharedLanguage { /** * Common transaction event that all contexts publish. * Defined collaboratively by Orders, Payments, Shipping teams. */ export interface TransactionEvent { transactionId: string; transactionType: 'ORDER' | 'PAYMENT' | 'SHIPMENT' | 'REFUND'; timestamp: string; amount?: { value: number; currency: string; }; customerId: string; // Common dimensions for analytics dimensions: { region: string; segment: string; channel: string; }; } // Each context translates its events to this shared format // before publishing to the analytics queue}Translation can happen at the publisher (translating before publishing) or at the consumer (translating after receiving). Consumer-side translation (Anti-Corruption Layer) is more common because the consumer knows what it needs and protects itself from upstream changes.
When integration events become the primary mechanism for cross-context communication, you're building an event-driven architecture. Several patterns emerge for structuring these integrations.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
/** * Pattern: Event Notification * * Thin events that trigger consumers to fetch more data. */// Orders publishes thin eventconst orderPlacedNotification = { eventType: 'orders.OrderPlaced', orderId: 'order-123', occurredAt: '2024-01-15T10:00:00Z', // No order details - just notification}; // Consumer fetches details if neededclass ShippingEventHandler { async handleOrderPlaced(event: OrderPlacedNotification): Promise<void> { // Call back to Orders service for full details const orderDetails = await this.ordersApi.getOrder(event.orderId); await this.createShipment(orderDetails); }} /** * Pattern: Event-Carried State Transfer * * Rich events that provide all needed context. */// Orders publishes rich eventconst orderPlacedFull = { eventType: 'orders.OrderPlaced', orderId: 'order-123', occurredAt: '2024-01-15T10:00:00Z', customer: { id: 'cust-456', email: 'customer@example.com', tier: 'GOLD', }, items: [ { productId: 'prod-1', quantity: 2, weight: 1.5 }, { productId: 'prod-2', quantity: 1, weight: 0.5 }, ], shippingAddress: { /* full address */ }, // All data shipping needs}; // Consumer processes without callbackclass ShippingEventHandlerAutonomous { async handleOrderPlaced(event: OrderPlacedFull): Promise<void> { // No external calls needed - all data in event await this.createShipment({ orderId: event.orderId, recipient: event.customer.email, items: event.items, address: event.shippingAddress, }); }} /** * Pattern: Choreography * * Services react to events independently, no central coordinator. */// OrderPlaced triggers independent reactions: // Inventory Serviceclass InventoryService { @EventHandler('orders.OrderPlaced') async reserveStock(event: OrderPlaced): Promise<void> { for (const item of event.items) { await this.inventory.reserve(item.productId, item.quantity); } // Publishes: inventory.StockReserved }} // Payment Serviceclass PaymentService { @EventHandler('orders.OrderPlaced') async initiatePayment(event: OrderPlaced): Promise<void> { await this.payments.charge(event.customerId, event.orderTotal); // Publishes: payments.PaymentReceived or payments.PaymentFailed }} // Notification Serviceclass NotificationService { @EventHandler('orders.OrderPlaced') async sendConfirmation(event: OrderPlaced): Promise<void> { await this.email.send({ to: event.customer.email, template: 'order-confirmation', data: event, }); }} /** * Pattern: Saga / Process Manager * * Central coordinator for complex workflows requiring compensation. */class OrderFulfillmentSaga { private state: SagaState; @Handles('orders.OrderPlaced') async start(event: OrderPlaced): Promise<void> { this.state = { orderId: event.orderId, status: 'STARTED', completedSteps: [], }; // Start the workflow await this.commandBus.send(new ReserveInventoryCommand( event.orderId, event.items, )); } @Handles('inventory.StockReserved') async onStockReserved(event: StockReserved): Promise<void> { if (event.orderId !== this.state.orderId) return; this.state.completedSteps.push('INVENTORY_RESERVED'); // Next step await this.commandBus.send(new ProcessPaymentCommand( this.state.orderId, )); } @Handles('payments.PaymentReceived') async onPaymentReceived(event: PaymentReceived): Promise<void> { if (event.orderId !== this.state.orderId) return; this.state.completedSteps.push('PAYMENT_PROCESSED'); // Next step await this.commandBus.send(new CreateShipmentCommand( this.state.orderId, )); } @Handles('payments.PaymentFailed') async onPaymentFailed(event: PaymentFailed): Promise<void> { if (event.orderId !== this.state.orderId) return; // Compensating action: release inventory await this.commandBus.send(new ReleaseInventoryCommand( this.state.orderId, )); // Compensating action: cancel order await this.commandBus.send(new CancelOrderCommand( this.state.orderId, 'PAYMENT_FAILED', )); this.state.status = 'FAILED'; }}Choreography is simpler for straightforward flows—services just react. Use orchestration (Sagas) when you need compensating transactions, when the flow has many steps, or when failure handling is complex. Many systems use both: choreography for simple cases, sagas for complex workflows.
Event-driven integration inherently means eventual consistency. When an order is placed, the Order context updates immediately, but the Inventory and Shipping contexts receive the event asynchronously. There's a window where different contexts have different views of the world.\n\nThis isn't a bug—it's a feature. Eventual consistency enables:\n- Higher availability (contexts operate independently)\n- Better scalability (no distributed locks)\n- Greater resilience (failures don't cascade)\n\nBut it requires different design thinking.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
/** * Designing for Eventual Consistency */ // 1. ACCEPT PROCESSING STATESenum OrderStatus { PLACED = 'PLACED', // Order created, awaiting confirmation PAYMENT_PENDING = 'PAYMENT_PENDING', CONFIRMED = 'CONFIRMED', // All downstream processing complete FAILED = 'FAILED', // Some step failed} class Order { // Order can be in 'limbo' while downstream processes complete static place(/* ... */): Order { const order = new Order(/* ... */); order.status = OrderStatus.PLACED; // Not CONFIRMED yet! order.addDomainEvent(new OrderPlaced(/* ... */)); return order; } // Called when all downstream confirmations received confirm(): void { this.status = OrderStatus.CONFIRMED; this.addDomainEvent(new OrderConfirmed(/* ... */)); }} // 2. COMPENSATING ACTIONSclass OrderEventHandlers { @Handles('payments.PaymentFailed') async handlePaymentFailed(event: PaymentFailed): Promise<void> { const order = await this.orderRepository.findById(event.orderId); // Compensate: release any reserved inventory order.markPaymentFailed(event.reason); // Emit compensation events order.addDomainEvent(new OrderPaymentFailed({ orderId: event.orderId, reason: event.reason, })); // Inventory service will listen and release reservations // Email service will listen and notify customer }} // 3. SAGA TRACKING STATEinterface FulfillmentState { orderId: string; currentStep: FulfillmentStep; completedSteps: FulfillmentStep[]; failedAt?: FulfillmentStep; compensatedSteps: FulfillmentStep[];} class OrderFulfillmentSaga { async handleStepFailure( orderId: string, failedStep: FulfillmentStep, ): Promise<void> { const state = await this.sagaRepository.find(orderId); state.failedAt = failedStep; // Compensate completed steps in reverse order const stepsToCompensate = [...state.completedSteps].reverse(); for (const step of stepsToCompensate) { await this.compensate(step, state); state.compensatedSteps.push(step); } await this.sagaRepository.save(state); } private async compensate( step: FulfillmentStep, state: FulfillmentState, ): Promise<void> { switch (step) { case 'INVENTORY_RESERVED': await this.commandBus.send( new ReleaseInventoryCommand(state.orderId) ); break; case 'PAYMENT_CAPTURED': await this.commandBus.send( new RefundPaymentCommand(state.orderId) ); break; // ... other compensations } }} // 4. OPTIMISTIC UIclass OrderPlacementUI { async placeOrder(orderData: OrderData): Promise<void> { // Immediately show optimistic result this.displayOptimisticOrder({ ...orderData, status: 'PROCESSING', message: 'Your order is being confirmed...', }); try { const order = await this.ordersApi.placeOrder(orderData); // Poll or use WebSocket for real updates this.subscribeToOrderUpdates(order.id, (update) => { this.displayOrderUpdate(update); }); } catch (error) { // Correct the optimistic update this.displayOrderError(error); } }}Strong consistency within an aggregate (single transaction), eventual consistency between aggregates and bounded contexts. This is the fundamental trade-off of distributed systems, and events make it explicit.
Integration events require reliable infrastructure for production use. The choice of messaging system, serialization format, and operational patterns significantly impacts system behavior.
| Technology | Characteristics | Best For |
|---|---|---|
| Apache Kafka | Log-based, ordered, durable, high throughput | Event sourcing, high-volume streams, replay capability |
| RabbitMQ | Traditional message broker, flexible routing | Task queues, complex routing, mixed patterns |
| AWS SNS/SQS | Managed, scalable, pub-sub + queuing | AWS-native, simple pub-sub, decoupling |
| Azure Service Bus | Enterprise features, transactions, sessions | Azure-native, complex workflows, FIFO |
| Google Pub/Sub | Managed, global, guaranteed delivery | GCP-native, global distribution |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
/** * CloudEvents-compatible event format * * Standard format for interoperability between different systems. * https://cloudevents.io */interface CloudEvent<T> { // Required specversion: '1.0'; // CloudEvents spec version type: string; // Event type (e.g., 'orders.OrderPlaced') source: string; // Event source (e.g., '/orders-service') id: string; // Unique event ID // Optional but recommended time?: string; // ISO-8601 timestamp datacontenttype?: string; // 'application/json' subject?: string; // Subject of event (e.g., order ID) // Extension attributes correlationid?: string; // Custom extension causationid?: string; // Custom extension // Event data (your domain payload) data?: T;} /** * Example: Publishing with CloudEvents format */function createOrderPlacedCloudEvent( order: Order, correlationId: string,): CloudEvent<OrderPlacedPayload> { return { specversion: '1.0', type: 'orders.OrderPlaced', source: '/orders-service', id: generateUUID(), time: new Date().toISOString(), datacontenttype: 'application/json', subject: order.id, correlationid: correlationId, data: { orderId: order.id, customerId: order.customerId, totalAmount: order.totalAmount.toDTO(), itemCount: order.items.length, }, };} /** * Schema Registry for evolution management */interface SchemaRegistry { register(eventType: string, schema: JSONSchema): number; // Returns version getSchema(eventType: string, version: number): JSONSchema; getLatestSchema(eventType: string): JSONSchema; validateEvent(event: unknown, eventType: string): ValidationResult;} class KafkaEventPublisher { constructor( private kafka: KafkaClient, private schemaRegistry: SchemaRegistry, ) {} async publish<T>( topic: string, event: CloudEvent<T>, ): Promise<void> { // Validate against schema const validation = await this.schemaRegistry.validateEvent( event, event.type, ); if (!validation.valid) { throw new SchemaValidationError(event.type, validation.errors); } // Serialize const serialized = JSON.stringify(event); // Publish with key for partitioning (ensures ordering per aggregate) await this.kafka.send({ topic, messages: [{ key: event.subject, // Order ID for ordering value: serialized, headers: { 'content-type': 'application/json', 'ce-type': event.type, 'ce-source': event.source, }, }], }); }}As systems grow, the number of integration events proliferates. Without governance, you end up with duplicative events, undocumented contracts, and breaking changes that cascade through the organization.\n\nEvent Governance establishes standards, processes, and tooling to manage events at scale.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
/** * Event Catalog Schema * * Documents all integration events for discoverability and governance. */interface EventCatalogEntry { // Identity eventType: string; namespace: string; currentVersion: number; // Ownership owner: { team: string; contact: string; repository: string; }; // Documentation description: string; businessContext: string; examplePayload: object; // Schema schema: JSONSchema; previousVersions: Array<{ version: number; schema: JSONSchema; deprecatedAt?: string; removedAt?: string; }>; // Relationships producer: ServiceInfo; consumers: ServiceInfo[]; // Operational averageVolumePerDay: number; sla: { deliveryLatency: string; // e.g., '< 1 second' availability: string; // e.g., '99.9%' }; // Lifecycle status: 'active' | 'deprecated' | 'retired'; createdAt: string; deprecatedAt?: string; retireAt?: string;} /** * AsyncAPI specification for event documentation * * Industry standard for event-driven API documentation. */const ordersServiceAsyncAPI = { asyncapi: '2.6.0', info: { title: 'Orders Service Events', version: '1.0.0', description: 'Events published by the Orders bounded context', }, channels: { 'orders.events': { publish: { summary: 'Order lifecycle events', message: { oneOf: [ { '$ref': '#/components/messages/OrderPlaced' }, { '$ref': '#/components/messages/OrderShipped' }, { '$ref': '#/components/messages/OrderCancelled' }, ], }, }, }, }, components: { messages: { OrderPlaced: { name: 'OrderPlaced', title: 'Order Placed', summary: 'Published when a customer places an order', payload: { type: 'object', properties: { eventType: { const: 'orders.OrderPlaced' }, eventVersion: { const: 1 }, orderId: { type: 'string', format: 'uuid' }, customerId: { type: 'string' }, orderTotal: { '$ref': '#/components/schemas/Money' }, }, required: ['eventType', 'orderId', 'customerId'], }, }, }, schemas: { Money: { type: 'object', properties: { amount: { type: 'number' }, currency: { type: 'string', pattern: '^[A-Z]{3}$' }, }, }, }, },}; /** * Automated compatibility check for event changes */class EventCompatibilityChecker { checkBreakingChanges( oldSchema: JSONSchema, newSchema: JSONSchema, ): BreakingChange[] { const changes: BreakingChange[] = []; // Check for removed required fields const oldRequired = new Set(oldSchema.required || []); const newRequired = new Set(newSchema.required || []); for (const field of oldRequired) { if (!newSchema.properties?.[field]) { changes.push({ type: 'REMOVED_FIELD', field, severity: 'BREAKING', message: `Required field '${field}' was removed`, }); } } // Check for type changes for (const [field, oldProp] of Object.entries(oldSchema.properties || {})) { const newProp = newSchema.properties?.[field]; if (newProp && oldProp.type !== newProp.type) { changes.push({ type: 'TYPE_CHANGE', field, severity: 'BREAKING', message: `Field '${field}' changed from ${oldProp.type} to ${newProp.type}`, }); } } return changes; }}AsyncAPI is to event-driven APIs what OpenAPI is to REST APIs. It provides a standard way to document your events, enabling code generation, documentation sites, and validation tooling. Consider adopting it for your integration events.
We've covered how domain events become the integration backbone between bounded contexts. Let's consolidate the key principles:
Module Complete:\n\nYou now have a comprehensive understanding of domain events—from their fundamental nature as immutable records of business occurrences, through naming and structural patterns, to the mechanics of raising and handling, and finally to their role as the integration backbone of distributed systems.\n\nDomain events are a fundamental building block for maintaining loose coupling while enabling rich communication between parts of your system. They enable event sourcing, CQRS, saga patterns, and event-driven architecture—patterns that power the world's most scalable and resilient systems.
Congratulations! You've mastered Domain Events. You understand how to design, name, structure, raise, handle, and use events for integration. These skills are essential for building modern, scalable, event-driven systems using Domain-Driven Design principles.