Building a reliable, globally distributed event store is a cornerstone of modern micro‑service architectures. This guide walks you through designing a multi‑region event sourcing system with NATS JetStream running on Kubernetes, from topology choices to operational safeguards.
Why Choose NATS JetStream for Multi‑Region Event Sourcing
NATS JetStream offers native at‑least‑once delivery, durable storage, and built‑in stream replication—all essential for event sourcing. Compared with Kafka, JetStream’s lightweight footprint and simple API reduce operational overhead while still providing strong consistency guarantees.
Key Benefits
- Low latency: Sub‑millisecond publish latency in the same region.
- Scalable storage: Streams can be backed by file‑system or object‑store mounts.
- Multi‑cluster mirroring: JetStream supports cluster‑wide replication without external tools.
High‑Level Architecture Overview
The architecture consists of three logical layers: ingestion, storage/replication, and projection services. Each layer is deployed as a Helm‑managed StatefulSet in a dedicated Kubernetes cluster per region.
- Ingress Layer: A Gateway Service (e.g., Envoy + gRPC) receives commands, converts them to events, and publishes to NATS.
- JetStream Cluster: A 3‑node NATS cluster per region persists streams and mirrors them to peer regions via cluster‐to‐cluster connections.
- Projection Layer: Consumers (e.g., Go or Rust workers) rebuild aggregate state from streams and expose read‑model APIs.
Step‑by‑Step Deployment on Kubernetes
Follow these concrete steps to get a production‑grade setup up and running.
1. Prepare Three Regional Clusters
- Use managed services (EKS, GKE, AKS) in us‑east‑1, eu‑central‑1, ap‑southeast‑2.
- Enable VPC‑peering or a global service mesh (Istio, Linkerd) for cross‑region traffic.
2. Deploy NATS JetStream with Helm
Use the official nats Helm chart version 0.15.2 and set the following values:
replicaCount: 3
jetstream:
enabled: true
maxMemStore: 4Gi
maxFileStore: 50Gi
storeDir: "/data/jetstream"
cluster:
enabled: true
name: "nats-cluster"
routes:
- nats://nats-0.nats-cluster.default.svc.cluster.local:6222
- nats://nats-1.nats-cluster.default.svc.cluster.local:6222
- nats://nats-2.nats-cluster.default.svc.cluster.local:6222
Apply the chart in each region, ensuring the cluster.routes list points to the remote region endpoints.
3. Enable Cross‑Region Mirroring
Add a JetStreamExport custom resource (CR) that defines a remote stream replica:
apiVersion: jetstream.nats.io/v1alpha1
kind: JetStreamExport
metadata:
name: orders-mirror
spec:
sourceCluster: us-east-1
targetCluster: eu-central-1
stream: orders
mirror: true
maxBytes: 100Gi
Repeat for each region pair (us‑east‑1 ↔ ap‑southeast‑2, eu‑central‑1 ↔ ap‑southeast‑2).
4. Deploy the Ingress Gateway
Use envoy with a grpc_http1_bridge filter. The Envoy deployment routes /publish calls to a lightweight order-writer service that serializes domain events and calls nats.publish().
5. Run Projection Workers
Each region runs a state-rebuilder consumer group subscribed to the orders stream. The workers use the JetStream Pull API with durable_name to guarantee exactly‑once processing.
Operational Trade‑offs and Best Practices
Designing for multi‑region introduces latency and consistency challenges. Below are the most common trade‑offs and how to mitigate them.
Latency vs. Consistency
Cross‑region mirroring adds ~80‑120 ms RTT. For read‑heavy workloads, serve queries from the local projection store; rely on eventual consistency for global aggregates.
Storage Cost vs. Durability
JetStream’s file‑store uses SSD disks. In high‑write scenarios (>200 k events/sec), consider tiered storage: hot SSD for recent streams, cold object storage (e.g., S3) for archival snapshots.
Failure Isolation
- Configure NATS
leafnodegateways so a regional outage does not cascade. - Use
PodDisruptionBudgetto guarantee at least two JetStream pods stay online during upgrades.
Observability
Expose standard Prometheus metrics via the NATS exporter. Monitor stream_bytes, consumer_pending, and cross‑region replication lag. Alert when lag exceeds 500 ms.
Testing Resilience Before Production
Run chaos experiments with kubectl‑chaos or Gremlin:
- Terminate a JetStream pod and verify automatic recovery.
- Block cross‑region traffic for 30 seconds; ensure write operations still succeed locally and sync once connectivity restores.
- Inject a network partition on the VPC and observe mirroring lag metrics.
Document the recovery time objective (RTO) and recovery point objective (RPO) for each scenario.
Security Considerations
Secure both data in‑flight and at‑rest:
- Enable TLS for all NATS client and leafnode connections.
- Apply Kubernetes NetworkPolicies to restrict traffic to the NATS namespace.
- Encrypt JetStream file stores with dm‑crypt or use encrypted PVCs.
- Rotate JWT‑based NATS user credentials every 90 days.
Cost Optimisation Tips
Because each region runs three NATS pods, total node count can grow quickly. To keep costs low:
- Right‑size nodes (e.g., c5.large) based on
maxFileStoreusage. - Leverage spot instances for non‑critical projection workers.
- Use horizontal pod autoscaling (HPA) on the ingress gateway only during peak load.
Future‑Proofing Your Event Store
As your domain model evolves, consider versioned streams (orders_v1, orders_v2) and migration jobs that replay old events into the new schema. NATS JetStream’s consumer_seq field makes it easy to track migration progress.
Conclusion
Designing a multi‑region event sourcing system with NATS JetStream on Kubernetes balances low latency, high durability, and operational simplicity. By following the deployment steps, applying the outlined trade‑off mitigations, and continuously testing resilience, you can deliver a globally consistent event store that scales with your business. Start a pilot in a single region, then replicate the Helm release to additional clusters to achieve true multi‑region coverage.