Multi-Region Architecture for Modern Web Apps
February 22, 20262 min read8 views
Multi-Region Architecture for Modern Web Apps
Edge compute, data consistency, replication trade-offs. When your users span continents and your database can only live in one place, every architectural decision becomes a physics problem.
The Speed of Light Problem
Network latency is bounded by physics. Light travels at ~200km/ms in fiber optic cable:
┌─────────────────────────────────────────────────────────────────┐
│ Global Round-Trip Latencies │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Origin: us-east-1 (Virginia) │
│ │
│ ┌────────────────────┬──────────┬──────────────────────┐ │
│ │ Destination │ Distance │ Min RTT (theoretical)│ │
│ ├────────────────────┼──────────┼──────────────────────┤ │
│ │ New York │ 400 km │ 4ms │ │
│ │ San Francisco │ 4,100 km │ 41ms │ │
│ │ London │ 5,600 km │ 56ms │ │
│ │ Frankfurt │ 6,600 km │ 66ms │ │
│ │ Singapore │ 15,300km │ 153ms │ │
│ │ Sydney │ 15,900km │ 159ms │ │
│ │ Mumbai │ 13,000km │ 130ms │ │
│ │ São Paulo │ 7,700 km │ 77ms │ │
│ └────────────────────┴──────────┴──────────────────────┘ │
│ │
│ Reality adds 2-3x overhead: │
│ • Routing inefficiency │
│ • Network hops │
│ • Processing at each hop │
│ • Protocol overhead │
│ │
│ Actual RTT Singapore → Virginia: ~250-350ms │
│ │
└─────────────────────────────────────────────────────────────────┘
For a user in Singapore accessing a US-based application:
┌─────────────────────────────────────────────────────────────────┐
│ Single-Region Request Breakdown │
├─────────────────────────────────────────────────────────────────┤
│ │
│ User in Singapore → Server in Virginia │
│ │
│ DNS lookup: 50ms │
│ TCP handshake: 250ms (1 RTT) │
│ TLS handshake: 500ms (2 RTT) │
│ HTTP request: 250ms (1 RTT) │
│ Server processing: 100ms │
│ HTTP response: 250ms (1 RTT) │
│ ───────────────────────────── │
│ Total TTFB: 1400ms │
│ │
│ + Asset downloads: 500-2000ms │
│ + JS execution: 200-500ms │
│ ───────────────────────────── │
│ Total TTI: 2100-3900ms │
│ │
│ This is unacceptable for modern applications. │
│ │
└─────────────────────────────────────────────────────────────────┘
Multi-Region Architecture Patterns
Pattern 1: Edge-Accelerated Single Origin
┌─────────────────────────────────────────────────────────────────┐
│ Edge-Accelerated Single Origin │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ Origin │ │
│ │ (us-east) │ │
│ │ Database │ │
│ └──────┬──────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │ Edge │ │ Edge │ │ Edge │ │
│ │ (Asia) │ │ (Europe)│ │ (LATAM) │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │ Users │ │ Users │ │ Users │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ Edge handles: │
│ • Static assets (cached) │
│ • TLS termination │
│ • Compression │
│ • Rate limiting │
│ • Bot protection │
│ │
│ Origin handles: │
│ • Dynamic content │
│ • Database queries │
│ • Business logic │
│ │
│ Pros: Simple, single source of truth │
│ Cons: Dynamic requests still hit origin │
│ │
└─────────────────────────────────────────────────────────────────┘
Pattern 2: Edge Compute with Origin Fallback
┌─────────────────────────────────────────────────────────────────┐
│ Edge Compute with Origin Fallback │
├─────────────────────────────────────────────────────────────────┤
│ │
│ User Request │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Edge (SIN) │◄─── Cloudflare/Vercel/Fastly │
│ │ │ │
│ │ 1. Auth │ │
│ │ 2. Cache? │──── HIT ───► Return cached │
│ │ 3. Compute? │──── Yes ───► Execute at edge │
│ │ 4. Origin │──── No ────► Fetch from origin │
│ │ │ │
│ └──────┬──────┘ │
│ │ │
│ │ (only uncached dynamic) │
│ ▼ │
│ ┌─────────────┐ │
│ │ Origin │ │
│ │ (us-east) │ │
│ └─────────────┘ │
│ │
│ Edge computes: │
│ • Personalization (geo, A/B tests) │
│ • Auth validation │
│ • API routing │
│ • Rate limiting │
│ • Header manipulation │
│ │
└─────────────────────────────────────────────────────────────────┘
Pattern 3: Multi-Region Active-Active
┌─────────────────────────────────────────────────────────────────┐
│ Multi-Region Active-Active │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Global Load Balancer │ │
│ │ (Anycast DNS / GeoDNS) │ │
│ └─────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Region A │ │ Region B │ │ Region C │ │
│ │ us-east │ │ eu-west │ │ ap-south │ │
│ │ │ │ │ │ │ │
│ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │
│ │ │ App │ │ │ │ App │ │ │ │ App │ │ │
│ │ └───┬──┘ │ │ └───┬──┘ │ │ └───┬──┘ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ┌───▼──┐ │ │ ┌───▼──┐ │ │ ┌───▼──┐ │ │
│ │ │ DB │◄─┼───┼──┤ DB │◄─┼───┼──┤ DB │ │ │
│ │ │replica│ │ │ │replica│ │ │ │replica│ │ │
│ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Primary │ │
│ │ Database │ │
│ │ (us-east) │ │
│ └─────────────┘ │
│ │
│ Replication: Async (eventual consistency) │
│ Writes: Routed to primary or use conflict resolution │
│ │
└─────────────────────────────────────────────────────────────────┘
Data Consistency Strategies
CAP Theorem in Practice
┌─────────────────────────────────────────────────────────────────┐
│ CAP Theorem Trade-offs │
├─────────────────────────────────────────────────────────────────┤
│ │
│ You can only have 2 of 3: │
│ │
│ Consistency │
│ /\ │
│ / \ │
│ / \ │
│ / CP \ CA │
│ / \ │
│ / AP \ │
│ /____________\ │
│ Availability Partition │
│ Tolerance │
│ │
│ CP Systems (Consistency + Partition Tolerance): │
│ • PostgreSQL single-region │
│ • CockroachDB │
│ • Spanner │
│ Trade-off: Reject writes during partitions │
│ │
│ AP Systems (Availability + Partition Tolerance): │
│ • Cassandra │
│ • DynamoDB (eventual consistency mode) │
│ • CockroachDB (with stale reads) │
│ Trade-off: May return stale data │
│ │
│ CA Systems (Consistency + Availability): │
│ • Single-node databases │
│ Trade-off: No partition tolerance (single point of failure) │
│ │
└─────────────────────────────────────────────────────────────────┘
Consistency Models by Use Case
// Different consistency requirements for different data
interface ConsistencyConfig {
model: 'strong' | 'eventual' | 'causal' | 'session';
maxStaleness?: number; // milliseconds
readFromReplica: boolean;
writeToRegion: 'local' | 'primary' | 'quorum';
}
const CONSISTENCY_PROFILES: Record<string, ConsistencyConfig> = {
// Financial transactions - strong consistency
payments: {
model: 'strong',
readFromReplica: false,
writeToRegion: 'primary',
},
// User authentication - session consistency
auth: {
model: 'session',
readFromReplica: true,
writeToRegion: 'primary',
},
// Product catalog - eventual consistency OK
products: {
model: 'eventual',
maxStaleness: 60000, // 1 minute stale is fine
readFromReplica: true,
writeToRegion: 'primary',
},
// User preferences - causal consistency
preferences: {
model: 'causal',
readFromReplica: true,
writeToRegion: 'local',
},
// Analytics/metrics - highly eventual
analytics: {
model: 'eventual',
maxStaleness: 300000, // 5 minutes
readFromReplica: true,
writeToRegion: 'local',
},
};
Read-Your-Writes Consistency
// Ensure users see their own writes immediately
class SessionConsistency {
private lastWriteTimestamps: Map<string, number> = new Map();
async write(
userId: string,
entity: string,
data: unknown
): Promise<WriteResult> {
const result = await this.primaryDB.write(entity, data);
// Track write timestamp
const key = `${userId}:${entity}`;
this.lastWriteTimestamps.set(key, result.timestamp);
return result;
}
async read(
userId: string,
entity: string,
query: Query
): Promise<ReadResult> {
const key = `${userId}:${entity}`;
const lastWrite = this.lastWriteTimestamps.get(key);
if (lastWrite) {
// Check if replica has caught up
const replicaTimestamp = await this.getReplicaTimestamp();
if (replicaTimestamp >= lastWrite) {
// Replica is up to date, safe to read
return this.replicaDB.read(entity, query);
} else {
// Replica is behind, read from primary
return this.primaryDB.read(entity, query);
}
}
// No recent writes, read from nearest replica
return this.replicaDB.read(entity, query);
}
}
// HTTP header-based approach
export function readYourWritesMiddleware(req: Request, res: Response, next: NextFunction) {
// Client sends timestamp of last write
const lastWrite = req.headers['x-last-write-timestamp'];
if (lastWrite) {
const timestamp = parseInt(lastWrite, 10);
// Check if we should read from primary
req.readFromPrimary = shouldReadFromPrimary(timestamp);
}
next();
}
// Client-side tracking
class ApiClient {
private lastWriteTimestamp: number = 0;
async post(url: string, data: unknown): Promise<Response> {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
// Track write timestamp from response
const timestamp = response.headers.get('x-write-timestamp');
if (timestamp) {
this.lastWriteTimestamp = parseInt(timestamp, 10);
}
return response;
}
async get(url: string): Promise<Response> {
return fetch(url, {
headers: {
// Send last write timestamp
'X-Last-Write-Timestamp': String(this.lastWriteTimestamp),
},
});
}
}
Edge Compute Implementation
Vercel Edge Functions
// middleware.ts - Runs at the edge
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Get user's region from Vercel's geo headers
const country = request.geo?.country ?? 'US';
const region = request.geo?.region ?? '';
const city = request.geo?.city ?? '';
// Add region headers for personalization
response.headers.set('x-user-country', country);
response.headers.set('x-user-region', region);
// Route to nearest origin for specific paths
if (request.nextUrl.pathname.startsWith('/api/')) {
const nearestOrigin = getNearestOrigin(country);
response.headers.set('x-origin-region', nearestOrigin);
}
// A/B testing at the edge
const bucket = getABTestBucket(request);
response.headers.set('x-ab-bucket', bucket);
// Feature flags based on region
const features = getRegionalFeatures(country);
response.headers.set('x-features', JSON.stringify(features));
return response;
}
function getNearestOrigin(country: string): string {
const regionMap: Record<string, string> = {
// Americas
US: 'us-east-1',
CA: 'us-east-1',
BR: 'sa-east-1',
MX: 'us-east-1',
// Europe
GB: 'eu-west-1',
DE: 'eu-central-1',
FR: 'eu-west-1',
// Asia Pacific
JP: 'ap-northeast-1',
SG: 'ap-southeast-1',
IN: 'ap-south-1',
AU: 'ap-southeast-2',
};
return regionMap[country] ?? 'us-east-1';
}
function getABTestBucket(request: NextRequest): string {
// Use consistent hashing for sticky sessions
const userId = request.cookies.get('userId')?.value;
if (userId) {
const hash = simpleHash(userId);
return hash % 2 === 0 ? 'control' : 'variant';
}
// Random assignment for new users
return Math.random() < 0.5 ? 'control' : 'variant';
}
function getRegionalFeatures(country: string): Record<string, boolean> {
// Different feature availability by region
return {
// Payment methods
applePay: ['US', 'CA', 'GB', 'AU'].includes(country),
pix: country === 'BR',
upi: country === 'IN',
// Regional features
gdprBanner: isEUCountry(country),
cookieConsent: isEUCountry(country),
// Performance features
aggressivePrefetch: !isSlowRegion(country),
imageOptimization: true,
};
}
Cloudflare Workers
// worker.ts - Cloudflare Worker for edge compute
export interface Env {
DB: D1Database;
KV: KVNamespace;
CACHE: Cache;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
// Static assets - cache forever
if (url.pathname.startsWith('/static/')) {
return handleStaticAsset(request, env);
}
// API routes - edge processing
if (url.pathname.startsWith('/api/')) {
return handleAPI(request, env, ctx);
}
// Pages - edge SSR with caching
return handlePage(request, env, ctx);
},
};
async function handleAPI(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const url = new URL(request.url);
// Edge-cacheable API responses
if (request.method === 'GET') {
const cacheKey = new Request(url.toString(), request);
const cache = caches.default;
// Check cache
let response = await cache.match(cacheKey);
if (response) {
return response;
}
// Fetch from origin
response = await fetchFromOrigin(request, env);
// Cache if cacheable
if (isCacheable(response)) {
ctx.waitUntil(cache.put(cacheKey, response.clone()));
}
return response;
}
// Mutations - proxy to origin
return fetchFromOrigin(request, env);
}
async function handlePage(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const url = new URL(request.url);
// Try edge cache first
const cacheKey = new Request(url.toString(), request);
const cache = caches.default;
let response = await cache.match(cacheKey);
if (response) {
// Add stale-while-revalidate
ctx.waitUntil(revalidateInBackground(request, env, cache, cacheKey));
return response;
}
// Edge render for personalized pages
if (needsPersonalization(request)) {
return edgeRender(request, env);
}
// Fetch from origin
response = await fetchFromOrigin(request, env);
// Cache non-personalized pages
if (!needsPersonalization(request)) {
ctx.waitUntil(cache.put(cacheKey, response.clone()));
}
return response;
}
async function edgeRender(request: Request, env: Env): Promise<Response> {
// Get user context
const country = request.cf?.country ?? 'US';
const city = request.cf?.city ?? '';
// Fetch data from edge KV
const config = await env.KV.get(`config:${country}`, 'json');
// Render HTML at edge
const html = renderPage({
country,
city,
config,
timestamp: Date.now(),
});
return new Response(html, {
headers: {
'Content-Type': 'text/html',
'Cache-Control': 'private, max-age=0',
},
});
}
function needsPersonalization(request: Request): boolean {
// Check for auth cookies
const cookies = request.headers.get('cookie');
if (cookies?.includes('auth=')) return true;
// Check for personalization params
const url = new URL(request.url);
if (url.searchParams.has('ref')) return true;
return false;
}
Database Replication Patterns
PostgreSQL with Read Replicas
// database-router.ts
import { Pool } from 'pg';
interface DatabaseConfig {
primary: Pool;
replicas: Pool[];
}
class DatabaseRouter {
private primary: Pool;
private replicas: Pool[];
private replicaIndex = 0;
constructor(config: DatabaseConfig) {
this.primary = config.primary;
this.replicas = config.replicas;
}
/**
* Get appropriate connection based on query type
*/
getConnection(options: {
write: boolean;
consistency?: 'strong' | 'eventual';
region?: string;
}): Pool {
// Writes always go to primary
if (options.write) {
return this.primary;
}
// Strong consistency reads go to primary
if (options.consistency === 'strong') {
return this.primary;
}
// Route to nearest replica
if (options.region) {
const regionalReplica = this.getRegionalReplica(options.region);
if (regionalReplica) return regionalReplica;
}
// Round-robin across replicas
return this.getNextReplica();
}
private getRegionalReplica(region: string): Pool | null {
// Map regions to replica indices
const regionMap: Record<string, number> = {
'us-east': 0,
'eu-west': 1,
'ap-south': 2,
};
const index = regionMap[region];
if (index !== undefined && this.replicas[index]) {
return this.replicas[index];
}
return null;
}
private getNextReplica(): Pool {
const replica = this.replicas[this.replicaIndex];
this.replicaIndex = (this.replicaIndex + 1) % this.replicas.length;
return replica;
}
/**
* Execute query with automatic routing
*/
async query<T>(
sql: string,
params: unknown[],
options: QueryOptions = {}
): Promise<T[]> {
const isWrite = this.isWriteQuery(sql);
const pool = this.getConnection({
write: isWrite,
consistency: options.consistency,
region: options.region,
});
const start = performance.now();
const result = await pool.query(sql, params);
const duration = performance.now() - start;
// Log routing decision
console.log({
type: isWrite ? 'write' : 'read',
pool: isWrite ? 'primary' : 'replica',
region: options.region,
duration,
});
return result.rows as T[];
}
private isWriteQuery(sql: string): boolean {
const writePatterns = /^(INSERT|UPDATE|DELETE|CREATE|DROP|ALTER)/i;
return writePatterns.test(sql.trim());
}
}
interface QueryOptions {
consistency?: 'strong' | 'eventual';
region?: string;
}
CockroachDB Multi-Region
-- CockroachDB multi-region configuration
-- Create database with multi-region config
CREATE DATABASE app
PRIMARY REGION "us-east1"
REGIONS "us-east1", "europe-west1", "asia-southeast1"
SURVIVE REGION FAILURE;
-- Global tables (replicated everywhere, strong consistency)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email STRING NOT NULL UNIQUE,
created_at TIMESTAMPTZ DEFAULT now()
) LOCALITY GLOBAL;
-- Regional tables (data stays in region)
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
region STRING NOT NULL,
total DECIMAL(10,2),
created_at TIMESTAMPTZ DEFAULT now()
) LOCALITY REGIONAL BY ROW AS region;
-- Regional by table (entire table in one region)
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
event_type STRING,
data JSONB,
created_at TIMESTAMPTZ DEFAULT now()
) LOCALITY REGIONAL BY TABLE IN "us-east1";
// CockroachDB client with region awareness
import { Client } from 'pg';
class CockroachClient {
private clients: Map<string, Client> = new Map();
async getRegionalClient(region: string): Promise<Client> {
if (!this.clients.has(region)) {
const client = new Client({
connectionString: this.getConnectionString(region),
});
await client.connect();
this.clients.set(region, client);
}
return this.clients.get(region)!;
}
private getConnectionString(region: string): string {
// CockroachDB Serverless provides regional endpoints
const endpoints: Record<string, string> = {
'us-east1': 'postgresql://user@cluster.us-east1.cockroachlabs.cloud:26257/app',
'europe-west1': 'postgresql://user@cluster.europe-west1.cockroachlabs.cloud:26257/app',
'asia-southeast1': 'postgresql://user@cluster.asia-southeast1.cockroachlabs.cloud:26257/app',
};
return endpoints[region] ?? endpoints['us-east1'];
}
async query<T>(
sql: string,
params: unknown[],
options: { region: string; staleRead?: boolean }
): Promise<T[]> {
const client = await this.getRegionalClient(options.region);
// Use follower reads for stale-tolerant queries
if (options.staleRead) {
await client.query("SET TRANSACTION AS OF SYSTEM TIME '-10s'");
}
const result = await client.query(sql, params);
return result.rows as T[];
}
}
Traffic Routing Strategies
Geo-Based DNS Routing
// Infrastructure as Code - Terraform
/*
resource "aws_route53_health_check" "primary" {
fqdn = "us-east.api.example.com"
port = 443
type = "HTTPS"
resource_path = "/health"
failure_threshold = 3
request_interval = 10
}
resource "aws_route53_record" "api_geo" {
zone_id = aws_route53_zone.main.zone_id
name = "api.example.com"
type = "A"
set_identifier = "us-east"
geolocation_routing_policy {
continent = "NA"
}
alias {
name = aws_lb.us_east.dns_name
zone_id = aws_lb.us_east.zone_id
evaluate_target_health = true
}
health_check_id = aws_route53_health_check.primary.id
}
resource "aws_route53_record" "api_geo_eu" {
zone_id = aws_route53_zone.main.zone_id
name = "api.example.com"
type = "A"
set_identifier = "eu-west"
geolocation_routing_policy {
continent = "EU"
}
alias {
name = aws_lb.eu_west.dns_name
zone_id = aws_lb.eu_west.zone_id
evaluate_target_health = true
}
}
*/
// Application-level routing
export function routeToRegion(request: Request): string {
// Get user's region from headers (set by CDN)
const country = request.headers.get('cf-ipcountry') ?? 'US';
// Map countries to API regions
const regionMap: Record<string, string> = {
// North America
US: 'us-east',
CA: 'us-east',
MX: 'us-east',
// Europe
GB: 'eu-west',
DE: 'eu-west',
FR: 'eu-west',
// ... more EU countries
// Asia Pacific
JP: 'ap-northeast',
KR: 'ap-northeast',
SG: 'ap-southeast',
IN: 'ap-south',
AU: 'ap-southeast',
};
return regionMap[country] ?? 'us-east';
}
Latency-Based Routing
// Client-side latency measurement for routing
class LatencyBasedRouter {
private latencies: Map<string, number[]> = new Map();
private endpoints = [
{ region: 'us-east', url: 'https://us-east.api.example.com' },
{ region: 'eu-west', url: 'https://eu-west.api.example.com' },
{ region: 'ap-south', url: 'https://ap-south.api.example.com' },
];
async measureLatencies(): Promise<void> {
const measurements = await Promise.all(
this.endpoints.map(async (endpoint) => {
const start = performance.now();
try {
await fetch(`${endpoint.url}/ping`, {
method: 'HEAD',
cache: 'no-store',
});
const latency = performance.now() - start;
return { region: endpoint.region, latency };
} catch {
return { region: endpoint.region, latency: Infinity };
}
})
);
// Store measurements
measurements.forEach(({ region, latency }) => {
if (!this.latencies.has(region)) {
this.latencies.set(region, []);
}
const history = this.latencies.get(region)!;
history.push(latency);
// Keep last 10 measurements
if (history.length > 10) {
history.shift();
}
});
}
getBestRegion(): string {
let bestRegion = 'us-east';
let bestLatency = Infinity;
this.latencies.forEach((history, region) => {
// Use median for stability
const sorted = [...history].sort((a, b) => a - b);
const median = sorted[Math.floor(sorted.length / 2)];
if (median < bestLatency) {
bestLatency = median;
bestRegion = region;
}
});
return bestRegion;
}
getEndpoint(): string {
const region = this.getBestRegion();
return this.endpoints.find((e) => e.region === region)!.url;
}
}
// Usage
const router = new LatencyBasedRouter();
// Measure on app start
router.measureLatencies();
// Re-measure periodically
setInterval(() => router.measureLatencies(), 60000);
// Use best endpoint
const apiUrl = router.getEndpoint();
Conflict Resolution
Last-Write-Wins (LWW)
// Simple but lossy conflict resolution
interface VersionedData<T> {
data: T;
version: number;
timestamp: number;
region: string;
}
class LWWConflictResolver<T> {
resolve(
local: VersionedData<T>,
remote: VersionedData<T>
): VersionedData<T> {
// Higher timestamp wins
if (remote.timestamp > local.timestamp) {
return remote;
}
// Tie-breaker: higher region name (deterministic)
if (remote.timestamp === local.timestamp) {
return remote.region > local.region ? remote : local;
}
return local;
}
}
CRDTs (Conflict-free Replicated Data Types)
// G-Counter CRDT for distributed counters
class GCounter {
private counts: Map<string, number> = new Map();
constructor(private nodeId: string) {}
increment(amount: number = 1): void {
const current = this.counts.get(this.nodeId) ?? 0;
this.counts.set(this.nodeId, current + amount);
}
value(): number {
let total = 0;
this.counts.forEach((count) => {
total += count;
});
return total;
}
merge(other: GCounter): void {
other.counts.forEach((count, nodeId) => {
const current = this.counts.get(nodeId) ?? 0;
this.counts.set(nodeId, Math.max(current, count));
});
}
toJSON(): Record<string, number> {
return Object.fromEntries(this.counts);
}
static fromJSON(nodeId: string, data: Record<string, number>): GCounter {
const counter = new GCounter(nodeId);
Object.entries(data).forEach(([node, count]) => {
counter.counts.set(node, count);
});
return counter;
}
}
// LWW-Register CRDT for single values
class LWWRegister<T> {
constructor(
private value: T,
private timestamp: number,
private nodeId: string
) {}
set(value: T, timestamp: number = Date.now()): void {
if (timestamp > this.timestamp) {
this.value = value;
this.timestamp = timestamp;
}
}
get(): T {
return this.value;
}
merge(other: LWWRegister<T>): void {
if (other.timestamp > this.timestamp) {
this.value = other.value;
this.timestamp = other.timestamp;
} else if (other.timestamp === this.timestamp) {
// Tie-breaker
if (other.nodeId > this.nodeId) {
this.value = other.value;
}
}
}
}
// OR-Set CRDT for sets with add/remove
class ORSet<T> {
private elements: Map<T, Set<string>> = new Map(); // value -> set of unique tags
private tombstones: Map<T, Set<string>> = new Map(); // removed tags
constructor(private nodeId: string) {}
add(element: T): void {
const tag = `${this.nodeId}:${Date.now()}:${Math.random()}`;
if (!this.elements.has(element)) {
this.elements.set(element, new Set());
}
this.elements.get(element)!.add(tag);
}
remove(element: T): void {
const tags = this.elements.get(element);
if (tags) {
if (!this.tombstones.has(element)) {
this.tombstones.set(element, new Set());
}
tags.forEach((tag) => {
this.tombstones.get(element)!.add(tag);
});
this.elements.delete(element);
}
}
has(element: T): boolean {
const tags = this.elements.get(element);
const tombs = this.tombstones.get(element) ?? new Set();
if (!tags) return false;
// Check if any tag is not tombstoned
for (const tag of tags) {
if (!tombs.has(tag)) return true;
}
return false;
}
values(): T[] {
return Array.from(this.elements.keys()).filter((e) => this.has(e));
}
merge(other: ORSet<T>): void {
// Merge elements
other.elements.forEach((tags, element) => {
if (!this.elements.has(element)) {
this.elements.set(element, new Set());
}
tags.forEach((tag) => {
this.elements.get(element)!.add(tag);
});
});
// Merge tombstones
other.tombstones.forEach((tags, element) => {
if (!this.tombstones.has(element)) {
this.tombstones.set(element, new Set());
}
tags.forEach((tag) => {
this.tombstones.get(element)!.add(tag);
});
});
}
}
Monitoring Multi-Region Systems
// Multi-region health dashboard
interface RegionHealth {
region: string;
status: 'healthy' | 'degraded' | 'unhealthy';
latency: {
p50: number;
p95: number;
p99: number;
};
errorRate: number;
replicationLag: number;
lastCheck: number;
}
class MultiRegionMonitor {
private health: Map<string, RegionHealth> = new Map();
async checkAllRegions(): Promise<void> {
const regions = ['us-east', 'eu-west', 'ap-south'];
const checks = await Promise.all(
regions.map((region) => this.checkRegion(region))
);
checks.forEach((check) => {
this.health.set(check.region, check);
});
}
private async checkRegion(region: string): Promise<RegionHealth> {
const endpoint = `https://${region}.api.example.com`;
// Latency check
const latencies: number[] = [];
for (let i = 0; i < 10; i++) {
const start = performance.now();
await fetch(`${endpoint}/health`);
latencies.push(performance.now() - start);
}
latencies.sort((a, b) => a - b);
// Error rate (last 5 minutes)
const errorRate = await this.getErrorRate(region);
// Replication lag
const replicationLag = await this.getReplicationLag(region);
// Determine status
let status: RegionHealth['status'] = 'healthy';
if (errorRate > 0.05 || latencies[9] > 500 || replicationLag > 10000) {
status = 'degraded';
}
if (errorRate > 0.1 || latencies[9] > 1000 || replicationLag > 60000) {
status = 'unhealthy';
}
return {
region,
status,
latency: {
p50: latencies[4],
p95: latencies[8],
p99: latencies[9],
},
errorRate,
replicationLag,
lastCheck: Date.now(),
};
}
private async getErrorRate(region: string): Promise<number> {
// Query metrics store
const response = await fetch(
`/metrics/error-rate?region=${region}&window=5m`
);
const data = await response.json();
return data.rate;
}
private async getReplicationLag(region: string): Promise<number> {
// Query replication status
const response = await fetch(
`/metrics/replication-lag?region=${region}`
);
const data = await response.json();
return data.lagMs;
}
getHealthReport(): {
global: 'healthy' | 'degraded' | 'unhealthy';
regions: RegionHealth[];
} {
const regions = Array.from(this.health.values());
const unhealthy = regions.filter((r) => r.status === 'unhealthy').length;
const degraded = regions.filter((r) => r.status === 'degraded').length;
let global: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';
if (degraded > 0) global = 'degraded';
if (unhealthy > regions.length / 2) global = 'unhealthy';
return { global, regions };
}
}
Summary
Multi-region architecture decisions cascade through every layer:
| Aspect | Single Region | Multi-Region Active-Passive | Multi-Region Active-Active |
|---|---|---|---|
| Latency | High for distant users | Medium (regional reads) | Low (local reads) |
| Consistency | Strong | Strong reads, eventual writes | Eventual (or complex) |
| Complexity | Low | Medium | High |
| Cost | Low | Medium | High |
| Availability | Single point of failure | Failover capable | Highly available |
| Data sovereignty | Simple | Regional storage | Complex compliance |
Key decisions:
- Edge vs. origin — What can run at the edge? Auth, personalization, caching.
- Read vs. write paths — Reads can be regional, writes often must be centralized.
- Consistency requirements — Not all data needs strong consistency.
- Conflict resolution — LWW is simple, CRDTs are correct, custom logic is necessary.
- Failure modes — What happens when regions partition?
The goal: minimize latency for reads (most common), maintain correctness for writes (most critical), and degrade gracefully when physics wins.
What did you think?