Microservices Architecture¶
Overview¶
Microservices architecture structures an application as a collection of loosely coupled, independently deployable services. Each service is focused on a specific business capability and can be developed, deployed, and scaled independently.
EasyChamp Microservices Architecture¶
Service Map¶
graph TB
subgraph "Client Layer"
WEB[ec-web-ui]
MOB[Mobile App]
ADMIN[ec-webengine-ui]
end
subgraph "API Gateway"
GW[API Gateway/BFF]
end
subgraph "Core Services"
AUTH[ec-security-api]
CHAMP[ec-standings-api]
CHAT[ec-chat-api]
SYNC[ec-sync-worker]
end
subgraph "Infrastructure Services"
MCP1[ec-mcp-server]
MCP2[ec-mcp-server-ai]
NOTIF[Notification Service]
end
subgraph "Data Layer"
PG[(PostgreSQL)]
REDIS[(Redis)]
S3[(S3/Blob Storage)]
end
WEB --> GW
MOB --> GW
ADMIN --> GW
GW --> AUTH
GW --> CHAMP
GW --> CHAT
AUTH --> PG
CHAMP --> PG
CHAT --> REDIS
SYNC --> PG
SYNC -.-> CHAMP
NOTIF -.-> AUTH
Service Design Principles¶
Single Responsibility¶
Each service handles one business capability.
// ec-standings-api - Focused on championship management
class StandingsService {
// Only championship-related operations
createChampionship(data: ChampionshipDto): Promise<Championship>
updateMatch(matchId: string, result: MatchResult): Promise<void>
calculateStandings(championshipId: string): Promise<Standing[]>
getLeaderboard(championshipId: string): Promise<Leaderboard>
}
// ec-security-api - Focused on authentication/authorization
class SecurityService {
// Only security-related operations
authenticate(credentials: LoginDto): Promise<AuthToken>
authorize(token: string, resource: string): Promise<boolean>
refreshToken(refreshToken: string): Promise<AuthToken>
revokeAccess(userId: string): Promise<void>
}
Service Boundaries¶
Define clear boundaries using Domain-Driven Design.
// Bounded contexts in EasyChamp
namespace ChampionshipContext {
export interface Championship {
id: string;
name: string;
teams: Team[];
matches: Match[];
rules: ChampionshipRules;
}
export interface Team {
id: string;
name: string;
players: string[]; // Reference to User context
}
}
namespace UserContext {
export interface User {
id: string;
email: string;
profile: UserProfile;
preferences: UserPreferences;
}
export interface UserProfile {
displayName: string;
avatar: string;
stats: PlayerStats;
}
}
Service Communication¶
Synchronous Communication (REST)¶
// API Gateway aggregating multiple services
class BFFController {
constructor(
private userService: UserServiceClient,
private championshipService: ChampionshipServiceClient,
private chatService: ChatServiceClient
) {}
@Get('/dashboard/:userId')
async getUserDashboard(userId: string) {
// Parallel calls to multiple services
const [user, championships, recentChats] = await Promise.all([
this.userService.getUser(userId),
this.championshipService.getUserChampionships(userId),
this.chatService.getRecentChats(userId, { limit: 5 })
]);
// Aggregate and transform
return {
user: this.transformUser(user),
activeChampionships: championships.filter(c => c.active),
recentActivity: this.mergeActivity(championships, recentChats)
};
}
}
Asynchronous Communication (Message Queue)¶
// Event-based communication between services
class ChampionshipService {
async completeMatch(matchId: string, result: MatchResult) {
// Update local state
await this.matchRepository.updateResult(matchId, result);
// Publish event for other services
await this.eventBus.publish({
type: 'MATCH_COMPLETED',
payload: {
matchId,
championshipId: result.championshipId,
result,
timestamp: new Date()
}
});
}
}
// Notification service subscribes to events
class NotificationService {
@Subscribe('MATCH_COMPLETED')
async handleMatchCompleted(event: MatchCompletedEvent) {
const followers = await this.getChampionshipFollowers(
event.payload.championshipId
);
await this.sendNotifications(followers, {
title: 'Match Result',
body: `Match ${event.payload.matchId} has been completed`,
data: event.payload.result
});
}
}
Service Mesh Pattern¶
# Istio service mesh configuration
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: championship-service
spec:
hosts:
- championship
http:
- match:
- headers:
x-version:
exact: v2
route:
- destination:
host: championship
subset: v2
weight: 100
- route:
- destination:
host: championship
subset: v1
weight: 90
- destination:
host: championship
subset: v2
weight: 10 # Canary deployment
Data Management¶
Database per Service¶
// Each service owns its data
namespace StandingsDatabase {
export const config = {
host: 'standings-db.internal',
database: 'championships',
tables: [
'championships',
'teams',
'matches',
'standings'
]
};
}
namespace SecurityDatabase {
export const config = {
host: 'security-db.internal',
database: 'authentication',
tables: [
'users',
'sessions',
'permissions',
'audit_log'
]
};
}
Data Consistency Patterns¶
Saga Pattern for Distributed Transactions¶
class CreateChampionshipSaga {
private steps = [
this.createChampionship.bind(this),
this.assignOrganizer.bind(this),
this.setupNotifications.bind(this),
this.initializeStandings.bind(this)
];
private compensations = [
this.deleteChampionship.bind(this),
this.removeOrganizer.bind(this),
this.disableNotifications.bind(this),
null // No compensation needed
];
async execute(data: CreateChampionshipDto) {
const completed = [];
try {
for (let i = 0; i < this.steps.length; i++) {
await this.steps[i](data);
completed.push(i);
}
return { success: true };
} catch (error) {
// Compensate in reverse order
for (let i = completed.length - 1; i >= 0; i--) {
if (this.compensations[i]) {
await this.compensations[i](data);
}
}
throw error;
}
}
}
CQRS for Read/Write Separation¶
// Write model
class ChampionshipCommandService {
async createMatch(command: CreateMatchCommand) {
const match = new Match(command);
await this.eventStore.append(match.aggregateId, match.events);
await this.projectionUpdater.update(match.events);
}
}
// Read model
class ChampionshipQueryService {
async getChampionshipView(id: string) {
// Read from denormalized view
return this.readDb.query(`
SELECT * FROM championship_view
WHERE id = $1
`, [id]);
}
}
Service Discovery¶
Client-Side Discovery¶
class ServiceDiscoveryClient {
private consul: Consul;
async getServiceUrl(serviceName: string): Promise<string> {
const services = await this.consul.health.service(serviceName);
if (services.length === 0) {
throw new Error(`Service ${serviceName} not found`);
}
// Simple round-robin
const service = services[Math.floor(Math.random() * services.length)];
return `http://${service.Service.Address}:${service.Service.Port}`;
}
}
// Usage
const championshipUrl = await discovery.getServiceUrl('championship-service');
const response = await fetch(`${championshipUrl}/api/championships`);
Server-Side Discovery (API Gateway)¶
# NGINX Plus configuration with service discovery
upstream championship-service {
zone championship 64k;
server championship-service.internal resolve;
}
server {
location /api/championships {
proxy_pass http://championship-service;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Resilience Patterns¶
Circuit Breaker¶
class CircuitBreaker {
private failures = 0;
private lastFailTime: Date;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
constructor(
private threshold = 5,
private timeout = 60000 // 1 minute
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailTime.getTime() > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
private onFailure() {
this.failures++;
this.lastFailTime = new Date();
if (this.failures >= this.threshold) {
this.state = 'OPEN';
}
}
}
Retry with Exponential Backoff¶
class RetryClient {
async callWithRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
let lastError: Error;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i < maxRetries - 1) {
const delay = baseDelay * Math.pow(2, i);
await this.sleep(delay);
}
}
}
throw lastError;
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
Bulkhead Pattern¶
class BulkheadExecutor {
private semaphore: Semaphore;
constructor(maxConcurrent = 10) {
this.semaphore = new Semaphore(maxConcurrent);
}
async execute<T>(fn: () => Promise<T>): Promise<T> {
await this.semaphore.acquire();
try {
return await fn();
} finally {
this.semaphore.release();
}
}
}
Monitoring and Observability¶
Distributed Tracing¶
// OpenTelemetry integration
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
class ChampionshipController {
private tracer = trace.getTracer('championship-service');
async getChampionship(req: Request, res: Response) {
const span = this.tracer.startSpan('getChampionship');
span.setAttributes({
'http.method': req.method,
'http.url': req.url,
'championship.id': req.params.id
});
try {
// Propagate trace context
const ctx = trace.setSpan(context.active(), span);
const championship = await context.with(ctx, () =>
this.service.findById(req.params.id)
);
span.setStatus({ code: SpanStatusCode.OK });
res.json(championship);
} catch (error) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message
});
res.status(500).json({ error: error.message });
} finally {
span.end();
}
}
}
Metrics Collection¶
// Prometheus metrics
import { register, Counter, Histogram, Gauge } from 'prom-client';
class MetricsCollector {
private requestCounter = new Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'route', 'status']
});
private requestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'route'],
buckets: [0.1, 0.5, 1, 2, 5]
});
private activeConnections = new Gauge({
name: 'active_connections',
help: 'Number of active connections'
});
recordRequest(method: string, route: string, status: number, duration: number) {
this.requestCounter.inc({ method, route, status });
this.requestDuration.observe({ method, route }, duration);
}
}
Centralized Logging¶
// Structured logging with correlation IDs
import winston from 'winston';
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.Http({
host: 'logstash.internal',
port: 5000
})
]
});
class LoggingMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const correlationId = req.headers['x-correlation-id'] || uuid();
req.logger = logger.child({
correlationId,
service: 'championship-service',
userId: req.user?.id
});
req.logger.info('Request received', {
method: req.method,
path: req.path,
query: req.query
});
next();
}
}
Deployment Strategies¶
Container Orchestration with Kubernetes¶
# Deployment configuration for championship service
apiVersion: apps/v1
kind: Deployment
metadata:
name: championship-service
spec:
replicas: 3
selector:
matchLabels:
app: championship-service
template:
metadata:
labels:
app: championship-service
spec:
containers:
- name: championship
image: easychamp/championship-service:v2.1.0
ports:
- containerPort: 3000
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: db-secret
key: host
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
Blue-Green Deployment¶
// Traffic switching for blue-green deployment
class TrafficSwitcher {
async switchToGreen() {
// Update load balancer
await this.loadBalancer.updateBackend({
service: 'championship-service',
backend: 'green',
weight: 100
});
// Verify green is healthy
await this.healthChecker.verify('championship-service-green');
// Remove blue from rotation
await this.loadBalancer.removeBackend('championship-service-blue');
}
}
Testing Microservices¶
Contract Testing¶
// Pact consumer test
describe('Championship Consumer', () => {
it('should get championship details', async () => {
await provider.addInteraction({
state: 'championship exists',
uponReceiving: 'a request for championship',
withRequest: {
method: 'GET',
path: '/api/championships/123'
},
willRespondWith: {
status: 200,
body: {
id: '123',
name: 'Premier League',
teams: Matchers.eachLike({
id: Matchers.string(),
name: Matchers.string()
})
}
}
});
const response = await championshipClient.getChampionship('123');
expect(response.name).toBe('Premier League');
});
});
Integration Testing¶
// Test containers for integration testing
import { GenericContainer } from 'testcontainers';
describe('Championship Service Integration', () => {
let postgresContainer;
let redisContainer;
beforeAll(async () => {
postgresContainer = await new GenericContainer('postgres:14')
.withExposedPorts(5432)
.withEnv('POSTGRES_PASSWORD', 'test')
.start();
redisContainer = await new GenericContainer('redis:7')
.withExposedPorts(6379)
.start();
process.env.DB_HOST = postgresContainer.getHost();
process.env.DB_PORT = postgresContainer.getMappedPort(5432);
process.env.REDIS_HOST = redisContainer.getHost();
process.env.REDIS_PORT = redisContainer.getMappedPort(6379);
});
it('should create and retrieve championship', async () => {
const service = new ChampionshipService();
const created = await service.create({
name: 'Test Championship',
startDate: new Date()
});
const retrieved = await service.findById(created.id);
expect(retrieved.name).toBe('Test Championship');
});
});
Best Practices¶
- Service Granularity: Not too fine (chatty) or coarse (monolithic)
- API Versioning: Support multiple versions during migration
- Idempotency: Make operations safe to retry
- Health Checks: Implement comprehensive health endpoints
- Documentation: Use OpenAPI/Swagger for API documentation
- Security: Implement zero-trust networking, mTLS
- Monitoring: Instrument everything, use distributed tracing
- Testing: Contract tests, integration tests, chaos engineering
References¶
Last updated: 2025-09-09