Skip to content

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

  1. Service Granularity: Not too fine (chatty) or coarse (monolithic)
  2. API Versioning: Support multiple versions during migration
  3. Idempotency: Make operations safe to retry
  4. Health Checks: Implement comprehensive health endpoints
  5. Documentation: Use OpenAPI/Swagger for API documentation
  6. Security: Implement zero-trust networking, mTLS
  7. Monitoring: Instrument everything, use distributed tracing
  8. Testing: Contract tests, integration tests, chaos engineering

References


Last updated: 2025-09-09