Skip to content

Core Concepts ​

Understanding the core concepts of PubSub MFE will help you build robust, scalable microfrontend applications.

Architecture Overview ​

PubSub MFE follows a hub-and-spoke architecture where the bus acts as the central hub, and microfrontends are the spokes.

┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│  Cart MFE   │      │  Header MFE │      │ Product MFE │
└──────┬──────┘      └──────┬──────┘      └──────┬──────┘
       │                    │                    │
       └────────────────────┴────────────────────┘
                            │
                     ┌──────▼──────┐
                     │  PubSub Bus │
                     └──────┬──────┘
                            │
       ┌────────────────────┴────────────────────┐
       │                    │                    │
┌──────▼──────┐      ┌──────▼──────┐      ┌──────▼──────┐
│Analytics MFE│      │ Checkout MFE│      │  Auth MFE   │
└─────────────┘      └─────────────┘      └─────────────┘

Key Components ​

1. PubSub Bus ​

The central message broker that routes messages from publishers to subscribers.

typescript
import { createPubSub, type PubSubBus } from '@belyas/pubsub-mfe';

const bus: PubSubBus = createPubSub({
  app: 'my-app',  // Application identifier
  debug: false    // Enable debug logging
});

2. Publishers ​

Components that emit messages to the bus:

typescript
// Simple publish
bus.publish('cart.item.add', { 
  sku: 'ABC123', 
  qty: 1 
});

// Publish with options
bus.publish('order.created', orderData, {
  source: 'checkout-service',       // Source publisher
  schemaVersion: "cart.item.add@1", // Schema version
  correlationId: 1234,              // Correlation ID for .e.g., request-response tracing
  meta?: {}                         // Extra information as needed
});

3. Subscribers ​

Components that listen for specific topics:

typescript
bus.subscribe('cart.item.+', (msg) => {
  console.log(`Action on cart item: ${msg.topic}`);
  console.log('Payload:', msg.payload);
});

Message Structure ​

Every message follows a consistent structure:

typescript
export interface Message<T = unknown> {
  readonly id: MessageId;                 // Unique identifier
  readonly topic: Topic;                  // e.g., "cart.item.add"
  readonly ts: Timestamp;                 // When it was published
  readonly schemaVersion?: SchemaVersion; // Message schema version
  readonly payload: T;                    // Your data
  readonly meta?: MessageMeta;            // MessageMeta - below
}

export interface MessageMeta {
  /** Source identifier (e.g. component ID, microfrontend name) */
  readonly source?: string;
  /** Correlation ID for request-response patterns */
  readonly correlationId?: string;
  /** Custom properties */
  readonly [key: string]: unknown;
}

Example Message ​

typescript
{
  id: "33003f67-1acc-4cc1-b17c-a676f744c620",
  topic: 'cart.item.add',
  payload: { sku: 'ABC123', qty: 1, price: 29.99 },
  ts: 1704067200000,
  schemaVersion: "cart.item.add@1",
  meta: {
    source: 'cart-mfe',
    correlationId: 1234
  }
}

Topic Patterns ​

Topics are hierarchical strings using dot notation:

Pattern Syntax ​

ComponentDescriptionExample
LiteralExact matchcart.item.add
+Single-level wildcardcart.+.add
#Multi-level wildcardcart.#

Pattern Examples ​

typescript
// Exact match
'cart.item.add' → Matches only 'cart.item.add'

// Single-level wildcard
'cart.+.add' → Matches:
  ✅ 'cart.item.add'
  ✅ 'cart.promo.add'
  ❌ 'cart.item.detail.add' (too many levels)

// Multi-level wildcard
'cart.#' → Matches:
  ✅ 'cart.item.add'
  ✅ 'cart.checkout.start'
  ✅ 'cart.item.detail.update'
  ✅ 'cart.anything.at.any.depth'

// Match everything
'#' → Matches all topics

Pattern Matching Diagram ​

Diagram showing how topic patterns match against published topics with visual examples of +, #, and exact matches

Topic pattern matching visualization

Handler Isolation (Bulkhead Pattern) ​

One of PubSub MFE's most important features is handler isolation. If one subscriber throws an error, it doesn't affect other subscribers.

Without Isolation ​

typescript
// ❌ Bad: One error breaks everything
bus.subscribe('cart.#', () => {
  throw new Error('Subscriber 1 failed');
});

bus.subscribe('cart.#', () => {
  console.log('This never runs!'); // ❌ Blocked by previous error
});

With Isolation (Built-in) ​

typescript
// ✅ Good: Errors are isolated
bus.subscribe('cart.#', () => {
  throw new Error('Subscriber 1 failed');
  // Error is caught and logged, doesn't propagate
});

bus.subscribe('cart.#', () => {
  console.log('This still runs!'); // ✅ Independent execution
});

Error Handling ​

PubSub MFE provides hooks for monitoring handler errors:

typescript
const bus = createPubSub({
  app: 'my-app',
  onDiagnostic(event: DiagnosticEvent) {
    if (event.type === "handler-error") {
      console.error(`Handler error on ${event.topic}:`, event.error);

      // Send to error tracking service
      Sentry.captureException(event.error, {
        tags: { topic: event.topic, messageId: event.messageId }
      });
    }
  }
});

Lifecycle Management ​

AbortSignal Support ​

Integrate with the AbortSignal API for automatic cleanup:

typescript
const controller = new AbortController();

// Subscribe with signal
bus.subscribe('cart.#', handler, {
  signal: controller.signal
});

// Later: cleanup all subscriptions
controller.abort();

Manual Unsubscribe ​

typescript
const unsubscribe = bus.subscribe('cart.#', handler);

// Later: remove this specific subscription
unsubscribe();

Component Lifecycle Integration ​

React ​

typescript
useEffect(() => {
  const controller = new AbortController();
  
  bus.subscribe('cart.#', handler, { 
    signal: controller.signal 
  });
  
  return () => controller.abort();
}, []);

Vue ​

typescript
onMounted(() => {
  const controller = new AbortController();

  bus.subscribe('cart.#', handler, { signal: controller.signal });
  
  onUnmounted(() => controller.abort());
});

Source Filtering ​

Filter messages by their origin to avoid loops or target specific sources:

Include Pattern ​

typescript
// Only receive messages from specific sources
bus.subscribe('cart.#', handler, {
  sourceFilter: {
    include: ['auth-mfe', 'checkout-mfe']
  }
});

Exclude Pattern ​

typescript
// Ignore messages from yourself
bus.subscribe('cart.#', handler, {
  sourceFilter: {
    exclude: ['cart-mfe'] // Don't receive your own messages
  }
});

Use Cases ​

typescript
// 1. Prevent feedback loops
bus.subscribe('sync.#', (msg) => {
  // Process external syncs, ignore your own
}, {
  sourceFilter: {
    exclude: ['my-component-id']
  }
});

// 2. Target specific services
bus.subscribe('analytics.#', (msg) => {
  // Only process analytics from trusted sources
}, {
  sourceFilter: {
    include: ['analytics-sdk', 'tracking-service']
  }
});

Message Flow Diagram ​

Detailed message flow showing publish, routing, filtering, and delivery to subscribers with error isolation

Complete message flow with filtering and isolation

Performance Characteristics ​

Time Complexity ​

  • Publish: O(n) where n = number of matching subscribers
  • Subscribe: O(1) registration, O(m) pattern compilation where m = pattern segments
  • Unsubscribe: O(1) removal

Memory Footprint ​

  • Core bus: ~5KB minified + gzipped
  • Cross-tab adapter: ~8KB (with BroadcastChannel transport)
  • History adapter: ~12KB (with IndexedDB)

See Performance Benchmarks for detailed metrics.

Best Practices ​

1. Use Specific Topics

typescript
// ✅ Good: Specific, clear intent
bus.publish('cart.item.add', data);
bus.publish('order.payment.success', data);

// ❌ Bad: Too generic
bus.publish('update', data);
bus.publish('event', data);

2. Organize by Domain

typescript
// ✅ Good: Domain.Entity.Action
'inventory.product.update'
'user.profile.created'
'payment.transaction.completed'

// ❌ Bad: Flat structure
'productUpdate'
'newUser'
'paymentDone'

3. Always Clean Up

typescript
// ✅ Good: Automatic cleanup
useEffect(() => {
  const controller = new AbortController();
  bus.subscribe('cart.#', handler, { signal: controller.signal });
  return () => controller.abort();
}, []);

// ❌ Bad: Memory leak
useEffect(() => {
  bus.subscribe('cart.#', handler); // Never cleaned up!
}, []);

Next Steps ​

🔒 Security Note

Never trust message payloads. Always validate and sanitize data, especially when using cross-tab or iframe adapters.

Released under the Apache-2.0 License.