Skip to content

Event Mesh Architecture for Multi-Cloud Systems

Design event mesh architectures that route events across clouds and regions with dynamic topic routing, protocol bridging and schema governance.

5 min read
Event mesh topology showing interconnected brokers across multiple cloud regions with dynamic routing paths and protocol bridges

An event broker works fine when everything runs in one cluster. But modern systems span multiple clouds, regions, and on-premise data centers. An event mesh extends the event-driven pattern across these boundaries, creating a fabric where any producer can reach any consumer regardless of where they run. Unlike a centralized broker, the mesh is distributed—each node handles local traffic and routes cross-boundary events dynamically.

The challenges go beyond networking. You need consistent schemas across environments, protocol translation between different messaging systems, and delivery guarantees that survive regional outages.

Mesh Topology and Dynamic Routing

An event mesh connects brokers across regions with intelligent routing. Events flow to consumers based on topic subscriptions, not hardcoded destinations.

tstypescript
// ❌ Point-to-point integration — doesn't scale
const routes = {
  "us-east": "kafka://us-east-broker:9092",
  "eu-west": "kafka://eu-west-broker:9092",
  "ap-south": "rabbitmq://ap-south-broker:5672",
};
// Adding a new region means updating every producer
// Different protocols? Manual translation at each hop
tstypescript
// ✅ Event mesh with dynamic routing
interface MeshNode {
  id: string;
  region: string;
  cloud: string;
  protocol: "kafka" | "amqp" | "mqtt" | "nats";
  topics: Set<string>;
  connectedPeers: Set<string>;
}
 
interface RouteEntry {
  topic: string;
  targetNodes: string[];
  priority: number;
  ttl: number;
}
 
class EventMeshRouter {
  private nodes: Map<string, MeshNode> = new Map();
  private routingTable: Map<string, RouteEntry[]> = new Map();
 
  registerNode(node: MeshNode): void {
    this.nodes.set(node.id, node);
    this.rebuildRoutes();
  }
 
  subscribe(nodeId: string, topic: string): void {
    const node = this.nodes.get(nodeId);
    if (!node) return;
 
    node.topics.add(topic);
    this.updateRoutesForTopic(topic);
  }
 
  route(
    topic: string,
    sourceNode: string
  ): MeshNode[] {
    const routes = this.routingTable.get(topic) ?? [];
 
    return routes
      .flatMap((route) =>
        route.targetNodes
          .filter((id) => id !== sourceNode)
          .map((id) => this.nodes.get(id))
          .filter((n): n is MeshNode => n !== undefined)
      );
  }
 
  private rebuildRoutes(): void {
    this.routingTable.clear();
 
    for (const [nodeId, node] of this.nodes) {
      for (const topic of node.topics) {
        this.updateRoutesForTopic(topic);
      }
    }
  }
 
  private updateRoutesForTopic(topic: string): void {
    const subscribedNodes: string[] = [];
 
    for (const [nodeId, node] of this.nodes) {
      if (node.topics.has(topic)) {
        subscribedNodes.push(nodeId);
      }
    }
 
    this.routingTable.set(topic, [
      {
        topic,
        targetNodes: subscribedNodes,
        priority: 1,
        ttl: 300,
      },
    ]);
  }
}

When a new region comes online, its broker registers with the mesh, subscribes to topics, and immediately starts receiving events. No producer changes needed.

Protocol Bridging

Different environments use different messaging protocols. The mesh must translate between them transparently.

tstypescript
interface MeshEvent {
  id: string;
  topic: string;
  payload: Uint8Array;
  contentType: string;
  headers: Map<string, string>;
  timestamp: number;
  sourceNode: string;
  schemaVersion: string;
}
 
interface ProtocolBridge {
  protocol: string;
  serialize(event: MeshEvent): Uint8Array;
  deserialize(data: Uint8Array, metadata: Record<string, string>): MeshEvent;
}
 
class KafkaBridge implements ProtocolBridge {
  protocol = "kafka";
 
  serialize(event: MeshEvent): Uint8Array {
    const record = {
      key: event.id,
      value: event.payload,
      headers: Object.fromEntries(event.headers),
      timestamp: event.timestamp,
    };
    return new TextEncoder().encode(JSON.stringify(record));
  }
 
  deserialize(
    data: Uint8Array,
    metadata: Record<string, string>
  ): MeshEvent {
    const record = JSON.parse(new TextDecoder().decode(data));
    return {
      id: record.key,
      topic: metadata.topic ?? "",
      payload: new TextEncoder().encode(
        typeof record.value === "string"
          ? record.value
          : JSON.stringify(record.value)
      ),
      contentType: "application/json",
      headers: new Map(Object.entries(record.headers ?? {})),
      timestamp: record.timestamp ?? Date.now(),
      sourceNode: metadata.sourceNode ?? "",
      schemaVersion: record.headers?.["schema-version"] ?? "1.0",
    };
  }
}
 
class AmqpBridge implements ProtocolBridge {
  protocol = "amqp";
 
  serialize(event: MeshEvent): Uint8Array {
    const message = {
      messageId: event.id,
      body: event.payload,
      applicationProperties: Object.fromEntries(event.headers),
      creationTime: event.timestamp,
    };
    return new TextEncoder().encode(JSON.stringify(message));
  }
 
  deserialize(
    data: Uint8Array,
    metadata: Record<string, string>
  ): MeshEvent {
    const message = JSON.parse(new TextDecoder().decode(data));
    return {
      id: message.messageId,
      topic: metadata.topic ?? "",
      payload: message.body,
      contentType: "application/json",
      headers: new Map(
        Object.entries(message.applicationProperties ?? {})
      ),
      timestamp: message.creationTime ?? Date.now(),
      sourceNode: metadata.sourceNode ?? "",
      schemaVersion:
        message.applicationProperties?.["schema-version"] ?? "1.0",
    };
  }
}
 
class BridgeManager {
  private bridges: Map<string, ProtocolBridge> = new Map();
 
  register(bridge: ProtocolBridge): void {
    this.bridges.set(bridge.protocol, bridge);
  }
 
  translate(
    event: MeshEvent,
    sourceProtocol: string,
    targetProtocol: string
  ): Uint8Array {
    const targetBridge = this.bridges.get(targetProtocol);
    if (!targetBridge) {
      throw new Error(
        `No bridge for protocol: ${targetProtocol}`
      );
    }
    return targetBridge.serialize(event);
  }
}

Schema Governance Across the Mesh

Events flowing between independently deployed services need schema contracts. Without governance, producers and consumers drift apart silently.

tstypescript
interface SchemaDefinition {
  id: string;
  topic: string;
  version: number;
  schema: object;
  compatibility: "backward" | "forward" | "full" | "none";
  registeredAt: Date;
}
 
class MeshSchemaRegistry {
  private schemas: Map<string, SchemaDefinition[]> = new Map();
 
  register(schema: SchemaDefinition): {
    accepted: boolean;
    reason?: string;
  } {
    const existing = this.schemas.get(schema.topic) ?? [];
    const latest = existing[existing.length - 1];
 
    if (latest) {
      const compatible = this.checkCompatibility(
        latest,
        schema
      );
      if (!compatible.ok) {
        return {
          accepted: false,
          reason: compatible.reason,
        };
      }
    }
 
    existing.push(schema);
    this.schemas.set(schema.topic, existing);
    return { accepted: true };
  }
 
  getLatest(topic: string): SchemaDefinition | null {
    const versions = this.schemas.get(topic);
    return versions?.[versions.length - 1] ?? null;
  }
 
  private checkCompatibility(
    existing: SchemaDefinition,
    proposed: SchemaDefinition
  ): { ok: boolean; reason?: string } {
    switch (existing.compatibility) {
      case "backward":
        // New schema can read old data
        return this.isBackwardCompatible(existing, proposed);
      case "forward":
        // Old schema can read new data
        return this.isForwardCompatible(existing, proposed);
      case "full":
        // Both directions
        const backward = this.isBackwardCompatible(
          existing, proposed
        );
        const forward = this.isForwardCompatible(
          existing, proposed
        );
        return {
          ok: backward.ok && forward.ok,
          reason: backward.reason ?? forward.reason,
        };
      case "none":
        return { ok: true };
    }
  }
 
  private isBackwardCompatible(
    existing: SchemaDefinition,
    proposed: SchemaDefinition
  ): { ok: boolean; reason?: string } {
    // Simplified: check no required fields were removed
    const existingFields = this.extractRequiredFields(
      existing.schema
    );
    const proposedFields = this.extractRequiredFields(
      proposed.schema
    );
 
    for (const field of existingFields) {
      if (!proposedFields.has(field)) {
        return {
          ok: false,
          reason: `Required field "${field}" removed`,
        };
      }
    }
    return { ok: true };
  }
 
  private isForwardCompatible(
    existing: SchemaDefinition,
    proposed: SchemaDefinition
  ): { ok: boolean; reason?: string } {
    // New fields must have defaults
    const existingFields = this.extractAllFields(existing.schema);
    const proposedFields = this.extractAllFields(proposed.schema);
    const newFields = [...proposedFields].filter(
      (f) => !existingFields.has(f)
    );
 
    const requiredNew = this.extractRequiredFields(
      proposed.schema
    );
    for (const field of newFields) {
      if (requiredNew.has(field)) {
        return {
          ok: false,
          reason: `New required field "${field}" without default`,
        };
      }
    }
    return { ok: true };
  }
 
  private extractRequiredFields(schema: object): Set<string> {
    const s = schema as any;
    return new Set(s.required ?? []);
  }
 
  private extractAllFields(schema: object): Set<string> {
    const s = schema as any;
    return new Set(Object.keys(s.properties ?? {}));
  }
}

Cross-Region Delivery Guarantees

Events crossing regional boundaries face network partitions, latency spikes, and ordering challenges. The mesh must provide predictable delivery semantics.

tstypescript
interface DeliveryConfig {
  guaranty: "at-most-once" | "at-least-once" | "exactly-once";
  maxRetries: number;
  retryBackoff: number;
  deduplicationWindow: number;
  orderingKey?: string;
}
 
class CrossRegionDelivery {
  private delivered: Map<string, number> = new Map();
 
  async deliver(
    event: MeshEvent,
    targets: MeshNode[],
    config: DeliveryConfig
  ): Promise<DeliveryReport> {
    const results: DeliveryResult[] = [];
 
    for (const target of targets) {
      const result = await this.deliverToNode(
        event,
        target,
        config
      );
      results.push(result);
    }
 
    return {
      eventId: event.id,
      totalTargets: targets.length,
      delivered: results.filter((r) => r.success).length,
      failed: results.filter((r) => !r.success),
    };
  }
 
  private async deliverToNode(
    event: MeshEvent,
    target: MeshNode,
    config: DeliveryConfig
  ): Promise<DeliveryResult> {
    // Deduplication check
    if (config.guaranty !== "at-most-once") {
      const deliveryKey = `${event.id}:${target.id}`;
      const lastDelivery = this.delivered.get(deliveryKey);
 
      if (
        lastDelivery &&
        Date.now() - lastDelivery < config.deduplicationWindow
      ) {
        return {
          targetNode: target.id,
          success: true,
          deduplicated: true,
        };
      }
    }
 
    // Retry with backoff
    for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
      try {
        await this.send(event, target);
 
        const deliveryKey = `${event.id}:${target.id}`;
        this.delivered.set(deliveryKey, Date.now());
 
        return {
          targetNode: target.id,
          success: true,
          attempts: attempt + 1,
        };
      } catch (error) {
        if (attempt < config.maxRetries) {
          const delay =
            config.retryBackoff * Math.pow(2, attempt);
          await new Promise((r) => setTimeout(r, delay));
        }
      }
    }
 
    return {
      targetNode: target.id,
      success: false,
      attempts: config.maxRetries + 1,
      error: "Max retries exceeded",
    };
  }
 
  private async send(
    event: MeshEvent,
    target: MeshNode
  ): Promise<void> {
    // Protocol-specific send logic
    console.log(
      `Delivering ${event.id} to ${target.id} via ${target.protocol}`
    );
  }
}
 
interface DeliveryResult {
  targetNode: string;
  success: boolean;
  attempts?: number;
  deduplicated?: boolean;
  error?: string;
}
 
interface DeliveryReport {
  eventId: string;
  totalTargets: number;
  delivered: number;
  failed: DeliveryResult[];
}

Key Takeaways

An event mesh extends event-driven architecture across cloud, region, and protocol boundaries by connecting distributed brokers with dynamic routing. Dynamic topic-based routing means new regions or services join the mesh by registering and subscribing—no producer changes required. Protocol bridges translate events between Kafka, AMQP, MQTT, and other protocols transparently, allowing heterogeneous environments to participate in the same event fabric. Schema governance across the mesh prevents silent contract drift—register schemas with compatibility checks that reject breaking changes before they reach consumers. Cross-region delivery requires explicit guarantees: choose between at-most-once, at-least-once, or exactly-once based on business requirements, and implement deduplication windows to handle the inevitable duplicates from retry logic. The event mesh isn't a single product—it's an architectural pattern that composes routers, bridges, registries, and delivery guarantees into a unified fabric for distributed event flow.

Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX