Skip to main content

Routers

Routers are intelligent message routing components that direct requests, events, and data flows between different parts of a distributed system. They provide dynamic routing capabilities, load balancing, protocol translation, and message transformation while maintaining high performance and reliability.

Core Responsibilities

Message Routing

  • Dynamic Routing: Route messages based on content, headers, or metadata
  • Load Balancing: Distribute traffic across multiple service instances
  • Failover Management: Redirect traffic when services become unavailable
  • Protocol Translation: Convert between different communication protocols

Traffic Management

  • Rate Limiting: Control message throughput to prevent system overload
  • Circuit Breaking: Protect downstream services from cascading failures
  • Retry Logic: Implement intelligent retry strategies for failed requests
  • Caching: Cache responses to reduce downstream service load

Router Types

Content-Based Routers

interface ContentBasedRouter {
route(message: Message): Promise<RouteDestination>;
}

class OrderRouter implements ContentBasedRouter {
async route(message: Message): Promise<RouteDestination> {
const order = message.payload as Order;

// Route based on order type
if (order.type === 'digital') {
return { service: 'digital-fulfillment', instance: 'primary' };
} else if (order.type === 'physical') {
return { service: 'warehouse-service', instance: 'nearest' };
} else if (order.value > 1000) {
return { service: 'premium-processing', instance: 'priority' };
}

return { service: 'standard-processing', instance: 'default' };
}
}

Protocol Routers

class ProtocolRouter {
private routes: Map<string, ProtocolHandler> = new Map();

constructor() {
this.routes.set('http', new HttpHandler());
this.routes.set('grpc', new GrpcHandler());
this.routes.set('websocket', new WebSocketHandler());
this.routes.set('amqp', new AmqpHandler());
}

async route(request: IncomingRequest): Promise<Response> {
const protocol = this.detectProtocol(request);
const handler = this.routes.get(protocol);

if (!handler) {
throw new UnsupportedProtocolError(`Protocol ${protocol} not supported`);
}

return await handler.process(request);
}

private detectProtocol(request: IncomingRequest): string {
if (request.headers['content-type']?.includes('application/grpc')) {
return 'grpc';
} else if (request.headers['upgrade'] === 'websocket') {
return 'websocket';
} else if (request.headers['x-amqp-routing-key']) {
return 'amqp';
}
return 'http';
}
}

Geographic Routers

class GeographicRouter {
private regionMap: Map<string, ServiceEndpoint[]> = new Map();

constructor() {
this.regionMap.set('us-east', [
{ url: 'https://api-us-east-1.example.com', latency: 10 },
{ url: 'https://api-us-east-2.example.com', latency: 15 }
]);

this.regionMap.set('eu-west', [
{ url: 'https://api-eu-west-1.example.com', latency: 12 },
{ url: 'https://api-eu-west-2.example.com', latency: 18 }
]);
}

async route(request: Request): Promise<ServiceEndpoint> {
const clientRegion = this.detectRegion(request);
const endpoints = this.regionMap.get(clientRegion) || this.getDefaultEndpoints();

// Select endpoint with lowest latency
return endpoints.reduce((best, current) =>
current.latency < best.latency ? current : best
);
}

private detectRegion(request: Request): string {
const clientIP = request.headers['x-forwarded-for'] || request.ip;
return this.geoIpService.getRegion(clientIP);
}
}

Routing Strategies

Round Robin

class RoundRobinRouter {
private currentIndex = 0;

constructor(private endpoints: ServiceEndpoint[]) {}

getNextEndpoint(): ServiceEndpoint {
const endpoint = this.endpoints[this.currentIndex];
this.currentIndex = (this.currentIndex + 1) % this.endpoints.length;
return endpoint;
}
}

Weighted Round Robin

class WeightedRoundRobinRouter {
private currentWeights: number[] = [];

constructor(private endpoints: WeightedEndpoint[]) {
this.currentWeights = endpoints.map(e => 0);
}

getNextEndpoint(): ServiceEndpoint {
let totalWeight = 0;
let selectedIndex = -1;

for (let i = 0; i < this.endpoints.length; i++) {
this.currentWeights[i] += this.endpoints[i].weight;
totalWeight += this.endpoints[i].weight;

if (selectedIndex === -1 ||
this.currentWeights[i] > this.currentWeights[selectedIndex]) {
selectedIndex = i;
}
}

this.currentWeights[selectedIndex] -= totalWeight;
return this.endpoints[selectedIndex];
}
}

Least Connections

class LeastConnectionsRouter {
private connectionCounts: Map<string, number> = new Map();

constructor(private endpoints: ServiceEndpoint[]) {
endpoints.forEach(endpoint => {
this.connectionCounts.set(endpoint.id, 0);
});
}

getNextEndpoint(): ServiceEndpoint {
let leastConnections = Infinity;
let selectedEndpoint: ServiceEndpoint | null = null;

for (const endpoint of this.endpoints) {
const connections = this.connectionCounts.get(endpoint.id) || 0;
if (connections < leastConnections) {
leastConnections = connections;
selectedEndpoint = endpoint;
}
}

if (selectedEndpoint) {
this.connectionCounts.set(selectedEndpoint.id, leastConnections + 1);
}

return selectedEndpoint!;
}

releaseConnection(endpointId: string): void {
const current = this.connectionCounts.get(endpointId) || 0;
this.connectionCounts.set(endpointId, Math.max(0, current - 1));
}
}

Consistent Hashing

class ConsistentHashRouter {
private ring: Map<number, ServiceEndpoint> = new Map();
private virtualNodes = 150;

constructor(private endpoints: ServiceEndpoint[]) {
this.buildHashRing();
}

private buildHashRing(): void {
for (const endpoint of this.endpoints) {
for (let i = 0; i < this.virtualNodes; i++) {
const hash = this.hash(`${endpoint.id}:${i}`);
this.ring.set(hash, endpoint);
}
}
}

route(key: string): ServiceEndpoint {
const hash = this.hash(key);
const sortedHashes = Array.from(this.ring.keys()).sort((a, b) => a - b);

// Find the first hash greater than or equal to the key hash
for (const ringHash of sortedHashes) {
if (ringHash >= hash) {
return this.ring.get(ringHash)!;
}
}

// Wrap around to the first node
return this.ring.get(sortedHashes[0])!;
}

private hash(input: string): number {
// Simple hash function (use crypto.createHash in production)
let hash = 0;
for (let i = 0; i < input.length; i++) {
hash = ((hash << 5) - hash + input.charCodeAt(i)) & 0xffffffff;
}
return Math.abs(hash);
}
}

Advanced Routing Features

Circuit Breaker Integration

class CircuitBreakerRouter {
private circuitBreakers: Map<string, CircuitBreaker> = new Map();

constructor(private fallbackRouter: Router) {}

async route(request: Request): Promise<Response> {
const endpoint = await this.selectEndpoint(request);
const circuitBreaker = this.getCircuitBreaker(endpoint.id);

try {
return await circuitBreaker.execute(() => this.sendRequest(endpoint, request));
} catch (error) {
if (circuitBreaker.isOpen()) {
// Circuit is open, use fallback
return await this.fallbackRouter.route(request);
}
throw error;
}
}

private getCircuitBreaker(endpointId: string): CircuitBreaker {
if (!this.circuitBreakers.has(endpointId)) {
this.circuitBreakers.set(endpointId, new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 30000,
monitoringPeriod: 10000
}));
}
return this.circuitBreakers.get(endpointId)!;
}
}

Rate Limiting

class RateLimitedRouter {
private rateLimiters: Map<string, RateLimiter> = new Map();

async route(request: Request): Promise<Response> {
const clientId = this.getClientId(request);
const rateLimiter = this.getRateLimiter(clientId);

if (!await rateLimiter.allowRequest()) {
throw new RateLimitExceededError('Rate limit exceeded');
}

return await this.forwardRequest(request);
}

private getRateLimiter(clientId: string): RateLimiter {
if (!this.rateLimiters.has(clientId)) {
this.rateLimiters.set(clientId, new TokenBucketRateLimiter({
capacity: 100,
refillRate: 10,
refillPeriod: 1000
}));
}
return this.rateLimiters.get(clientId)!;
}
}

Message Transformation

class TransformingRouter {
private transformers: Map<string, MessageTransformer> = new Map();

constructor() {
this.transformers.set('v1-to-v2', new ApiVersionTransformer());
this.transformers.set('xml-to-json', new FormatTransformer());
this.transformers.set('legacy-format', new LegacyFormatTransformer());
}

async route(message: Message): Promise<Response> {
const destination = await this.selectDestination(message);
const transformer = this.getTransformer(message, destination);

let transformedMessage = message;
if (transformer) {
transformedMessage = await transformer.transform(message);
}

return await this.forwardMessage(transformedMessage, destination);
}

private getTransformer(
message: Message,
destination: ServiceEndpoint
): MessageTransformer | null {
const sourceVersion = message.headers['api-version'];
const targetVersion = destination.supportedVersions[0];

if (sourceVersion !== targetVersion) {
return this.transformers.get(`${sourceVersion}-to-${targetVersion}`);
}

return null;
}
}

Performance Optimization

Connection Pooling

class PooledRouter {
private connectionPools: Map<string, ConnectionPool> = new Map();

async route(request: Request): Promise<Response> {
const endpoint = await this.selectEndpoint(request);
const pool = this.getConnectionPool(endpoint);

const connection = await pool.acquire();
try {
return await connection.send(request);
} finally {
pool.release(connection);
}
}

private getConnectionPool(endpoint: ServiceEndpoint): ConnectionPool {
if (!this.connectionPools.has(endpoint.id)) {
this.connectionPools.set(endpoint.id, new ConnectionPool({
endpoint,
minConnections: 5,
maxConnections: 50,
acquireTimeout: 5000,
idleTimeout: 300000
}));
}
return this.connectionPools.get(endpoint.id)!;
}
}

Caching Layer

class CachingRouter {
private cache: Cache;

constructor(private underlyingRouter: Router) {
this.cache = new LRUCache({ max: 10000, ttl: 300000 });
}

async route(request: Request): Promise<Response> {
const cacheKey = this.generateCacheKey(request);

// Check cache first
const cachedResponse = this.cache.get(cacheKey);
if (cachedResponse) {
return this.createCachedResponse(cachedResponse);
}

// Forward to underlying router
const response = await this.underlyingRouter.route(request);

// Cache response if cacheable
if (this.isCacheable(request, response)) {
this.cache.set(cacheKey, response.data);
}

return response;
}

private isCacheable(request: Request, response: Response): boolean {
return request.method === 'GET' &&
response.status === 200 &&
!response.headers['cache-control']?.includes('no-cache');
}
}

Batch Processing

class BatchingRouter {
private batchQueue: Map<string, BatchItem[]> = new Map();
private batchTimers: Map<string, NodeJS.Timeout> = new Map();

async route(request: Request): Promise<Response> {
const batchKey = this.getBatchKey(request);

if (this.isBatchable(request)) {
return await this.addToBatch(request, batchKey);
} else {
return await this.routeImmediately(request);
}
}

private async addToBatch(request: Request, batchKey: string): Promise<Response> {
return new Promise((resolve, reject) => {
const batchItem: BatchItem = { request, resolve, reject };

if (!this.batchQueue.has(batchKey)) {
this.batchQueue.set(batchKey, []);
}

this.batchQueue.get(batchKey)!.push(batchItem);

// Set timer if not already set
if (!this.batchTimers.has(batchKey)) {
const timer = setTimeout(() => this.processBatch(batchKey), 100);
this.batchTimers.set(batchKey, timer);
}

// Process immediately if batch is full
const batch = this.batchQueue.get(batchKey)!;
if (batch.length >= 10) {
this.processBatch(batchKey);
}
});
}
}

Monitoring and Observability

Metrics Collection

class InstrumentedRouter {
private metrics: MetricsCollector;

constructor(private underlyingRouter: Router) {
this.metrics = new MetricsCollector();
}

async route(request: Request): Promise<Response> {
const startTime = Date.now();
const labels = {
route: request.path,
method: request.method,
source: request.headers['x-source-service']
};

this.metrics.increment('router.requests.total', labels);

try {
const response = await this.underlyingRouter.route(request);

this.metrics.increment('router.requests.success', {
...labels,
status: response.status.toString()
});

return response;
} catch (error) {
this.metrics.increment('router.requests.failure', {
...labels,
error: error.constructor.name
});

throw error;
} finally {
const duration = Date.now() - startTime;
this.metrics.histogram('router.request.duration', duration, labels);
}
}
}

Health Checking

class HealthAwareRouter {
private healthChecker: HealthChecker;
private healthyEndpoints: Set<string> = new Set();

constructor(private endpoints: ServiceEndpoint[]) {
this.healthChecker = new HealthChecker();
this.startHealthChecking();
}

async route(request: Request): Promise<Response> {
const healthyEndpoints = this.getHealthyEndpoints();

if (healthyEndpoints.length === 0) {
throw new NoHealthyEndpointsError('All endpoints are unhealthy');
}

const selectedEndpoint = this.selectFromHealthy(healthyEndpoints, request);
return await this.forwardRequest(selectedEndpoint, request);
}

private startHealthChecking(): void {
setInterval(async () => {
for (const endpoint of this.endpoints) {
try {
await this.healthChecker.check(endpoint);
this.healthyEndpoints.add(endpoint.id);
} catch (error) {
this.healthyEndpoints.delete(endpoint.id);
}
}
}, 30000); // Check every 30 seconds
}
}

Testing Strategies

Unit Testing

describe('ContentBasedRouter', () => {
let router: ContentBasedRouter;

beforeEach(() => {
router = new ContentBasedRouter();
});

it('should route digital orders to digital fulfillment', async () => {
const message = {
payload: { type: 'digital', id: '123' },
headers: {}
};

const destination = await router.route(message);

expect(destination.service).toBe('digital-fulfillment');
});

it('should route high-value orders to premium processing', async () => {
const message = {
payload: { type: 'physical', value: 1500, id: '456' },
headers: {}
};

const destination = await router.route(message);

expect(destination.service).toBe('premium-processing');
});
});

Integration Testing

describe('Router Integration', () => {
let router: Router;
let mockEndpoints: MockEndpoint[];

beforeEach(async () => {
mockEndpoints = [
new MockEndpoint('service-a', 'healthy'),
new MockEndpoint('service-b', 'healthy')
];

router = new LoadBalancingRouter(mockEndpoints);
});

it('should distribute load evenly', async () => {
const requests = Array.from({ length: 100 }, (_, i) =>
createMockRequest(`request-${i}`)
);

for (const request of requests) {
await router.route(request);
}

const serviceACalls = mockEndpoints[0].getCallCount();
const serviceBCalls = mockEndpoints[1].getCallCount();

expect(Math.abs(serviceACalls - serviceBCalls)).toBeLessThan(10);
});
});

Best Practices

Design Principles

  • Statelessness: Keep routers stateless for horizontal scaling
  • Fail Fast: Quickly detect and handle failures
  • Graceful Degradation: Provide fallback mechanisms
  • Observability: Instrument all routing decisions

Performance Guidelines

  • Connection Reuse: Pool and reuse connections
  • Efficient Algorithms: Use appropriate routing algorithms
  • Memory Management: Monitor memory usage for large routing tables
  • Async Processing: Use non-blocking I/O operations

Security Considerations

  • Input Validation: Validate all routing parameters
  • Rate Limiting: Protect against abuse and DoS attacks
  • Authentication: Verify client identity before routing
  • Audit Logging: Log all routing decisions for security analysis
  • Gateways: Higher-level request routing and API management
  • Adapters: Protocol and format adaptation
  • Services: Target destinations for routed messages
  • Workflows: Process orchestration using routing