MicroservicesArchitectureBackendBackend EngineeringScalability

Microservices Architecture: A Complete Guide

Satyam Parmar
January 8, 2025
8 min read

Microservices Architecture: A Complete Guide

Microservices architecture has become the go-to approach for building large-scale, distributed systems. This comprehensive guide covers everything you need to know to implement microservices effectively.

What are Microservices?

Microservices are an architectural approach where applications are built as a collection of loosely coupled, independently deployable services. Each service is responsible for a specific business capability.

Key Characteristics

  • Single Responsibility: Each service has one business function
  • Independence: Services can be developed, deployed, and scaled independently
  • Decentralized: No single point of failure
  • Technology Diversity: Each service can use different technologies

When to Use Microservices

Benefits

Scalability: Scale individual services based on demand ✅ Technology Flexibility: Use the best tool for each job ✅ Team Autonomy: Independent development teams ✅ Fault Isolation: Failure in one service doesn't bring down the entire system

Drawbacks

Complexity: Increased operational overhead ❌ Network Latency: Inter-service communication adds latency ❌ Data Consistency: Distributed transactions are challenging ❌ Testing: End-to-end testing becomes more complex

Microservices Patterns

API Gateway Pattern

// API Gateway implementation
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');

const app = express();

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100 // limit each IP to 100 requests per windowMs
});

app.use(limiter);

// Service routing
app.use('/api/users', createProxyMiddleware({
  target: 'http://user-service:3001',
  changeOrigin: true,
  pathRewrite: {
    '^/api/users': '', // remove /api/users prefix
  },
}));

app.use('/api/orders', createProxyMiddleware({
  target: 'http://order-service:3002',
  changeOrigin: true,
  pathRewrite: {
    '^/api/orders': '',
  },
}));

app.listen(3000, () => {
  console.log('API Gateway running on port 3000');
});

Java/Spring Boot:

// API Gateway with Spring Cloud Gateway
@RestController
@RequestMapping("/api")
public class ApiGatewayController {
    
    @Autowired
    private UserServiceClient userServiceClient;
    
    @Autowired
    private OrderServiceClient orderServiceClient;
    
    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        User user = userServiceClient.getUser(id);
        return ResponseEntity.ok(user);
    }
    
    @GetMapping("/orders/{id}")
    public ResponseEntity<Order> getOrder(@PathVariable Long id) {
        Order order = orderServiceClient.getOrder(id);
        return ResponseEntity.ok(order);
    }
}

// Gateway configuration
@Configuration
public class GatewayConfig {
    
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user-service", r -> r.path("/api/users/**")
                .filters(f -> f.stripPrefix(2))
                .uri("http://user-service:3001"))
            .route("order-service", r -> r.path("/api/orders/**")
                .filters(f -> f.stripPrefix(2))
                .uri("http://order-service:3002"))
            .build();
    }
}

Service Discovery

// Service registry implementation
const express = require('express');
const app = express();

const services = new Map();

// Register service
app.post('/register', (req, res) => {
  const { serviceName, serviceUrl, healthCheck } = req.body;
  
  services.set(serviceName, {
    url: serviceUrl,
    healthCheck,
    lastHeartbeat: Date.now(),
  });
  
  res.json({ message: 'Service registered successfully' });
});

// Discover service
app.get('/discover/:serviceName', (req, res) => {
  const service = services.get(req.params.serviceName);
  
  if (!service) {
    return res.status(404).json({ error: 'Service not found' });
  }
  
  res.json(service);
});

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'healthy', services: services.size });
});

Circuit Breaker Pattern

// Circuit breaker implementation
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.threshold = threshold;
    this.timeout = timeout;
    this.failureCount = 0;
    this.lastFailureTime = null;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
  }
  
  async call(serviceFunction) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime > this.timeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }
    
    try {
      const result = await serviceFunction();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  onSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }
  
  onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
    }
  }
}

// Usage
const userServiceBreaker = new CircuitBreaker(3, 30000);

app.get('/api/user/:id', async (req, res) => {
  try {
    const user = await userServiceBreaker.call(() => 
      fetch(`http://user-service:3001/users/${req.params.id}`)
    );
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: 'Service temporarily unavailable' });
  }
});

Data Management

Database per Service

// User service with its own database
const express = require('express');
const { PrismaClient } = require('@prisma/client');

const app = express();
const prisma = new PrismaClient();

// User service endpoints
app.get('/users/:id', async (req, res) => {
  try {
    const user = await prisma.user.findUnique({
      where: { id: req.params.id },
      select: {
        id: true,
        email: true,
        name: true,
        // Don't expose internal fields
      }
    });
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.post('/users', async (req, res) => {
  try {
    const user = await prisma.user.create({
      data: req.body,
    });
    
    res.status(201).json(user);
  } catch (error) {
    res.status(400).json({ error: 'Invalid user data' });
  }
});

Event Sourcing

// Event store implementation
const EventStore = {
  events: [],
  
  append(streamId, event) {
    const eventRecord = {
      streamId,
      eventId: generateId(),
      eventType: event.constructor.name,
      eventData: event,
      timestamp: new Date(),
      version: this.getNextVersion(streamId),
    };
    
    this.events.push(eventRecord);
    return eventRecord;
  },
  
  getEvents(streamId) {
    return this.events.filter(e => e.streamId === streamId);
  },
  
  getNextVersion(streamId) {
    const streamEvents = this.getEvents(streamId);
    return streamEvents.length;
  }
};

// Event classes
class UserCreated {
  constructor(userId, email, name) {
    this.userId = userId;
    this.email = email;
    this.name = name;
  }
}

class UserUpdated {
  constructor(userId, changes) {
    this.userId = userId;
    this.changes = changes;
  }
}

// Usage in user service
app.post('/users', async (req, res) => {
  const userId = generateId();
  const event = new UserCreated(userId, req.body.email, req.body.name);
  
  EventStore.append('users', event);
  
  res.status(201).json({ userId, message: 'User created' });
});

Communication Patterns

Synchronous Communication

// HTTP client with retry logic
const axios = require('axios');

class ServiceClient {
  constructor(baseURL, retries = 3) {
    this.baseURL = baseURL;
    this.retries = retries;
  }
  
  async request(method, path, data = null) {
    let lastError;
    
    for (let i = 0; i < this.retries; i++) {
      try {
        const response = await axios({
          method,
          url: `${this.baseURL}${path}`,
          data,
          timeout: 5000,
        });
        
        return response.data;
      } catch (error) {
        lastError = error;
        
        if (i < this.retries - 1) {
          await this.delay(Math.pow(2, i) * 1000); // Exponential backoff
        }
      }
    }
    
    throw lastError;
  }
  
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Usage
const userService = new ServiceClient('http://user-service:3001');
const orderService = new ServiceClient('http://order-service:3002');

Asynchronous Communication

// Message queue with Redis
const redis = require('redis');
const client = redis.createClient();

class MessageQueue {
  async publish(topic, message) {
    await client.lpush(`queue:${topic}`, JSON.stringify(message));
  }
  
  async subscribe(topic, handler) {
    while (true) {
      const message = await client.brpop(`queue:${topic}`, 0);
      if (message) {
        const data = JSON.parse(message[1]);
        await handler(data);
      }
    }
  }
}

// Event handlers
const messageQueue = new MessageQueue();

// Order service publishes events
app.post('/orders', async (req, res) => {
  const order = await createOrder(req.body);
  
  // Publish order created event
  await messageQueue.publish('order.created', {
    orderId: order.id,
    userId: order.userId,
    total: order.total,
  });
  
  res.json(order);
});

// User service subscribes to events
messageQueue.subscribe('order.created', async (data) => {
  // Update user's order history
  await updateUserOrderHistory(data.userId, data.orderId);
});

Monitoring and Observability

Distributed Tracing

// OpenTelemetry setup
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');

const sdk = new NodeSDK({
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

// Custom tracing
const { trace } = require('@opentelemetry/api');

app.get('/api/orders/:id', async (req, res) => {
  const tracer = trace.getTracer('order-service');
  
  return tracer.startActiveSpan('get-order', async (span) => {
    try {
      span.setAttributes({
        'order.id': req.params.id,
        'user.id': req.user.id,
      });
      
      const order = await getOrder(req.params.id);
      
      span.setStatus({ code: 1 }); // OK
      res.json(order);
    } catch (error) {
      span.setStatus({ code: 2, message: error.message }); // ERROR
      res.status(500).json({ error: 'Internal server error' });
    } finally {
      span.end();
    }
  });
});

Health Checks

// Comprehensive health check
app.get('/health', async (req, res) => {
  const health = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    services: {}
  };
  
  // Check database
  try {
    await prisma.$queryRaw`SELECT 1`;
    health.services.database = 'healthy';
  } catch (error) {
    health.services.database = 'unhealthy';
    health.status = 'unhealthy';
  }
  
  // Check Redis
  try {
    await client.ping();
    health.services.redis = 'healthy';
  } catch (error) {
    health.services.redis = 'unhealthy';
    health.status = 'unhealthy';
  }
  
  // Check external services
  try {
    await userService.request('GET', '/health');
    health.services.userService = 'healthy';
  } catch (error) {
    health.services.userService = 'unhealthy';
    health.status = 'unhealthy';
  }
  
  const statusCode = health.status === 'healthy' ? 200 : 503;
  res.status(statusCode).json(health);
});

Deployment Strategies

Docker Containerization

# Dockerfile for microservice
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

Docker Compose

# docker-compose.yml
version: '3.8'

services:
  api-gateway:
    build: ./api-gateway
    ports:
      - "3000:3000"
    environment:
      - USER_SERVICE_URL=http://user-service:3001
      - ORDER_SERVICE_URL=http://order-service:3002
    depends_on:
      - user-service
      - order-service
  
  user-service:
    build: ./user-service
    ports:
      - "3001:3001"
    environment:
      - DATABASE_URL=postgresql://user:password@postgres:5432/users
    depends_on:
      - postgres
  
  order-service:
    build: ./order-service
    ports:
      - "3002:3002"
    environment:
      - DATABASE_URL=postgresql://user:password@postgres:5432/orders
    depends_on:
      - postgres
  
  postgres:
    image: postgres:15
    environment:
      - POSTGRES_DB=microservices
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Best Practices

1. Service Boundaries

  • Design services around business capabilities
  • Keep services loosely coupled
  • Avoid shared databases
  • Use events for communication

2. Data Consistency

  • Implement eventual consistency
  • Use saga pattern for distributed transactions
  • Handle failures gracefully
  • Monitor data consistency

3. Security

  • Implement service-to-service authentication
  • Use API gateways for external access
  • Encrypt data in transit and at rest
  • Regular security audits

4. Testing

  • Unit tests for each service
  • Integration tests for service interactions
  • Contract testing between services
  • End-to-end tests for critical paths

Conclusion

Microservices architecture offers significant benefits for large-scale applications, but it also introduces complexity. Success requires careful planning, proper tooling, and a team that understands distributed systems.

Remember: Start with a monolith, identify natural boundaries, and extract services gradually. Microservices are not a silver bullet—they're a tool for specific problems.

Related Articles

Home