Module 17: Communication Patterns That Scale (Production Architecture)
Chapter 17 • Advanced
Communication Patterns That Scale (Production Architecture)
The Real Problem
Scenario: Products team adds a new event "cart:item-added-v2" with new structure. Checkout team hasn't updated yet. Checkout breaks.
The Question: How do I notify Checkout when Products adds an item, without breaking when Products team deploys a new version?
This is not about events. This is about contracts.
Communication as Contracts, Not Events
The Wrong Way (That Breaks)
// Products App
eventBus.emit('cart:item-added', {
productId: '123',
quantity: 2,
price: 29.99
})
// Checkout App
eventBus.on('cart:item-added').subscribe(data => {
// What if Products changes structure?
// What if Products adds new fields?
// What if Products removes fields?
console.log(data.productId) // Breaks if Products changes!
})
Why This Breaks:
- ❌ No versioning
- ❌ No backward compatibility
- ❌ Tight coupling
- ❌ Breaks on deployment
The Production Reality
Our E-Commerce System
- Products App (Team A) - Emits events
- Checkout App (Team B) - Listens to events
- Host App (Platform Team) - Provides event bus
Constraints:
- Teams deploy independently
- Products can deploy before Checkout updates
- Old Checkout must work with new Products
- Network can fail
- Events can be lost
The Solution: Typed, versioned event contracts.
Pattern 1: Typed Event Contracts (Recommended)
Define Event Schema
// Shared types (in Host or separate package)
export interface EventContract {
event: string
version: string
data: any
timestamp: number
}
export interface CartItemAddedEvent extends EventContract {
event: 'cart:item-added'
version: '1.0.0'
data: {
itemId: string
productId: string
quantity: number
price: number
}
}
Products App: Emit Typed Events
// products-app/src/app/events/cart.events.ts
export class CartEvents {
static itemAdded(item: CartItem): CartItemAddedEvent {
return {
event: 'cart:item-added',
version: '1.0.0',
data: {
itemId: item.id,
productId: item.productId,
quantity: item.quantity,
price: item.price
},
timestamp: Date.now()
}
}
}
// products-app/src/app/cart/cart.service.ts
addItem(item: CartItem) {
// Emit typed event
const event = CartEvents.itemAdded(item)
this.eventBus.emit(event)
}
Checkout App: Handle Events Safely
// checkout-app/src/app/events/event-handler.ts
export class EventHandler {
constructor(private eventBus: EventBus) {
this.eventBus.on('cart:item-added').subscribe(event => {
this.handleCartItemAdded(event)
})
}
private handleCartItemAdded(event: EventContract) {
// Version check
if (event.version === '1.0.0') {
this.handleV1(event)
} else if (event.version === '2.0.0') {
this.handleV2(event)
} else {
console.warn('Unknown event version:', event.version)
// Fallback to latest known version
this.handleV1(event)
}
}
private handleV1(event: CartItemAddedEvent) {
const { itemId, productId, quantity, price } = event.data
// Handle v1 structure
}
private handleV2(event: any) {
// Handle v2 structure (when implemented)
}
}
Why This Works
✅ Version Safety - Old Checkout handles new Products events
✅ Type Safety - TypeScript catches mismatches
✅ Backward Compatible - New events don't break old listeners
✅ Explicit Contracts - Event structure is documented
Pattern 2: Event Evolution Strategy
The Problem
Products wants to add "discount" field to cart:item-added event. Checkout hasn't updated yet.
The Solution: Additive Changes Only
// Version 1.0.0
interface CartItemAddedData {
itemId: string
productId: string
quantity: number
price: number
}
// Version 2.0.0 (ADDITIVE - doesn't break v1)
interface CartItemAddedData {
itemId: string
productId: string
quantity: number
price: number
discount?: number // Optional - v1 can ignore
appliedPromo?: string // Optional - v1 can ignore
}
// Checkout handles both
private handleCartItemAdded(event: EventContract) {
if (event.version === '1.0.0') {
// Old format - works fine
const { itemId, productId, quantity, price } = event.data
} else if (event.version === '2.0.0') {
// New format - can use new fields
const { itemId, productId, quantity, price, discount } = event.data
}
}
Key Principle: New versions must be additive. Never remove fields.
Pattern 3: PostMessage for True Isolation
When to Use
- Apps on different origins
- Need true isolation
- Security is critical
Implementation
// Products App (origin: products.example.com)
export class PostMessageService {
send(event: EventContract) {
window.parent.postMessage(event, 'https://app.example.com')
}
}
// Host App (origin: app.example.com)
export class PostMessageListener {
constructor() {
window.addEventListener('message', (event) => {
// Verify origin
if (event.origin === 'https://products.example.com') {
this.eventBus.emit(event.data)
}
})
}
}
Security: Always verify origin. Never trust messages from unknown origins.
Pattern 4: Shared Services (Use Carefully)
When to Use
- Simple, stable APIs
- Synchronous communication needed
- Same origin only
Implementation
// Host App - Provides service
@Injectable({ providedIn: 'root' })
export class NotificationService {
private notifications$ = new Subject<Notification>()
show(notification: Notification) {
this.notifications$.next(notification)
}
getNotifications(): Observable<Notification> {
return this.notifications$.asObservable()
}
}
// Products App - Uses service
export class ProductsComponent {
constructor(private notifications: NotificationService) {}
addToCart() {
this.notifications.show({
message: 'Item added to cart',
type: 'success'
})
}
}
Warning: Creates tight coupling. Use only for stable, shared concerns.
Backward-Compatible Event Evolution
Scenario: Products Adds New Event
Step 1: Products emits new event version
// Products App
eventBus.emit({
event: 'cart:item-added',
version: '2.0.0', // New version
data: {
itemId: '123',
productId: '456',
quantity: 2,
price: 29.99,
discount: 5.00 // New field
}
})
Step 2: Checkout handles both versions
// Checkout App
handleCartItemAdded(event: EventContract) {
if (event.version === '1.0.0') {
// Old version - ignore new fields
return this.handleV1(event)
} else if (event.version === '2.0.0') {
// New version - use new fields
return this.handleV2(event)
}
}
Step 3: Products can still emit v1 (during transition)
// Products App - Emit both versions during transition
addItem(item: CartItem) {
// Emit v1 (for old Checkout)
eventBus.emit(CartEvents.itemAddedV1(item))
// Emit v2 (for new Checkout)
eventBus.emit(CartEvents.itemAddedV2(item))
}
Step 4: Once all apps updated, remove v1
What Breaks (And How to Prevent)
Breaking Change 1: Removed Field
// ❌ BREAKS: Removed field
interface CartItemAddedData {
itemId: string
// productId removed - Checkout breaks!
quantity: number
price: number
}
Prevention: Never remove fields. Mark as deprecated.
Breaking Change 2: Changed Field Type
// ❌ BREAKS: Changed type
interface CartItemAddedData {
itemId: string
price: string // Was number - Checkout breaks!
}
Prevention: Use new field name. Support both during transition.
Breaking Change 3: Required Field
// ❌ BREAKS: Made required
interface CartItemAddedData {
itemId: string
discount: number // Required now - old events break!
}
Prevention: New required fields must have defaults or be optional.
Notification System Example
The Problem
Products adds item. Checkout needs to show notification. Host needs to update cart badge.
The Solution: Typed Notification Events
// Shared notification contract
export interface NotificationEvent extends EventContract {
event: 'notification:show'
version: '1.0.0'
data: {
message: string
type: 'success' | 'error' | 'info'
duration?: number
}
}
// Products App
addItem(item: CartItem) {
// Add to cart
this.cartService.addItem(item)
// Show notification
this.eventBus.emit({
event: 'notification:show',
version: '1.0.0',
data: {
message: 'Item added to cart',
type: 'success',
duration: 3000
}
})
}
// Host App - Shows notifications
export class NotificationComponent {
constructor(private eventBus: EventBus) {
this.eventBus.on('notification:show').subscribe(event => {
if (event.version === '1.0.0') {
this.showNotification(event.data)
}
})
}
}
Testing Communication
Test Event Emission
describe('CartService', () => {
it('should emit cart:item-added event', () => {
const mockEventBus = jasmine.createSpyObj('EventBus', ['emit'])
const service = new CartService(mockEventBus)
service.addItem({ id: '1', name: 'Product', price: 10 })
expect(mockEventBus.emit).toHaveBeenCalledWith(
jasmine.objectContaining({
event: 'cart:item-added',
version: '1.0.0'
})
)
})
})
Test Event Handling
describe('CheckoutEventHandler', () => {
it('should handle v1 events', () => {
const handler = new CheckoutEventHandler(mockEventBus)
mockEventBus.emit({
event: 'cart:item-added',
version: '1.0.0',
data: { itemId: '1', productId: '2', quantity: 1, price: 10 }
})
expect(handler.cartItems).toHaveLength(1)
})
it('should handle v2 events', () => {
const handler = new CheckoutEventHandler(mockEventBus)
mockEventBus.emit({
event: 'cart:item-added',
version: '2.0.0',
data: { itemId: '1', productId: '2', quantity: 1, price: 10, discount: 2 }
})
expect(handler.cartItems).toHaveLength(1)
expect(handler.cartItems[0].discount).toBe(2)
})
})
Production Checklist
- [ ] Event contracts defined and documented
- [ ] Versioning strategy implemented
- [ ] Backward compatibility ensured
- [ ] Event evolution strategy defined
- [ ] Error handling for failed events
- [ ] Testing strategy for communication
- [ ] Documentation for event contracts
Key Takeaways
- Communication is contracts - Not just events
- Version everything - Enable backward compatibility
- Additive changes only - Never remove fields
- Test independently - Mock event bus in tests
- Document contracts - Event structure is API
Remember: In micro frontends, communication patterns determine whether your system survives independent deployments.
The next module: What happens when things fail.
Related Tutorials
Previous: Module 16: State Management Across Micro Frontends (Production Architecture)
Learn about module 16: state management across micro frontends (production architecture)
Next: Module 18: Failure Is the Default (Error & Resilience)
Continue with module 18: failure is the default (error & resilience)