Microservices Architecture: Design Patterns and Implementation Strategies
February 14, 2024•4 min read
MicroservicesArchitectureDistributed SystemsDesign Patterns
# Microservices Architecture: Design Patterns and Implementation Strategies
Microservices architecture has become the standard for building large-scale, distributed applications. This architectural style breaks down applications into small, independent services that communicate over well-defined APIs.
## Core Principles
### Service Independence
Each microservice should be independently deployable and scalable:
```typescript
// User Service
@Module({
imports: [DatabaseModule],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
// Order Service
@Module({
imports: [DatabaseModule],
controllers: [OrderController],
providers: [OrderService],
})
export class OrderModule {}
```
### Database Per Service
Each service owns its database:
```typescript
// User Service Database
interface User {
id: string;
email: string;
name: string;
}
// Order Service Database
interface Order {
id: string;
userId: string; // Reference, not foreign key
items: OrderItem[];
}
```
## Communication Patterns
### Synchronous Communication (REST)
Use REST for request-response patterns:
```typescript
@Injectable()
export class OrderService {
constructor(
private readonly httpService: HttpService,
@Inject('USER_SERVICE_URL') private readonly userServiceUrl: string,
) {}
async createOrder(orderData: CreateOrderDto): Promise<Order> {
// Verify user exists
const user = await this.httpService
.get(`${this.userServiceUrl}/users/${orderData.userId}`)
.toPromise();
if (!user) {
throw new NotFoundException('User not found');
}
return this.orderRepository.create(orderData);
}
}
```
### Asynchronous Communication (Message Queue)
Use message queues for event-driven communication:
```typescript
// Publisher
@Injectable()
export class OrderService {
constructor(
@Inject('MESSAGE_QUEUE') private readonly messageQueue: MessageQueue,
) {}
async createOrder(orderData: CreateOrderDto): Promise<Order> {
const order = await this.orderRepository.create(orderData);
// Publish event
await this.messageQueue.publish('order.created', {
orderId: order.id,
userId: order.userId,
amount: order.total,
});
return order;
}
}
// Subscriber
@Injectable()
export class NotificationService {
@MessagePattern('order.created')
async handleOrderCreated(data: OrderCreatedEvent) {
// Send notification to user
await this.sendEmail(data.userId, 'Order confirmed');
}
}
```
## Service Discovery
### Service Registry Pattern
Implement service discovery for dynamic service location:
```typescript
@Injectable()
export class ServiceRegistry {
private services: Map<string, ServiceInfo> = new Map();
register(serviceName: string, url: string) {
this.services.set(serviceName, { url, lastHeartbeat: Date.now() });
}
discover(serviceName: string): string | null {
const service = this.services.get(serviceName);
if (!service || Date.now() - service.lastHeartbeat > 30000) {
return null; // Service unavailable
}
return service.url;
}
}
```
## API Gateway Pattern
Implement an API Gateway for unified entry point:
```typescript
@Controller()
export class ApiGatewayController {
constructor(
private readonly userService: UserService,
private readonly orderService: OrderService,
private readonly productService: ProductService,
) {}
@Get('users/:id/orders')
async getUserOrders(@Param('id') userId: string) {
const [user, orders] = await Promise.all([
this.userService.findById(userId),
this.orderService.findByUserId(userId),
]);
return { user, orders };
}
}
```
## Resilience Patterns
### Circuit Breaker
Implement circuit breaker for fault tolerance:
```typescript
class CircuitBreaker {
private failures = 0;
private state: 'closed' | 'open' | 'half-open' = 'closed';
private readonly threshold = 5;
private readonly timeout = 60000;
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailure > 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++;
if (this.failures >= this.threshold) {
this.state = 'open';
this.lastFailure = Date.now();
}
}
}
```
### Retry Pattern
Implement exponential backoff retry:
```typescript
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await sleep(Math.pow(2, i) * 1000); // Exponential backoff
}
}
throw new Error('Max retries exceeded');
}
```
## Data Consistency
### Saga Pattern
Implement saga for distributed transactions:
```typescript
class OrderSaga {
async execute(orderData: CreateOrderDto) {
const steps = [
{ name: 'reserveInventory', compensate: 'releaseInventory' },
{ name: 'chargePayment', compensate: 'refundPayment' },
{ name: 'createOrder', compensate: 'cancelOrder' },
];
const executedSteps: string[] = [];
try {
for (const step of steps) {
await this.executeStep(step.name, orderData);
executedSteps.push(step.name);
}
} catch (error) {
// Compensate in reverse order
for (const step of executedSteps.reverse()) {
await this.compensate(step, orderData);
}
throw error;
}
}
}
```
## Monitoring and Observability
### Distributed Tracing
Implement distributed tracing:
```typescript
import { trace, context } from '@opentelemetry/api';
@Injectable()
export class OrderService {
async createOrder(orderData: CreateOrderDto) {
const tracer = trace.getTracer('order-service');
return tracer.startActiveSpan('createOrder', async (span) => {
try {
span.setAttribute('user.id', orderData.userId);
const order = await this.orderRepository.create(orderData);
span.setAttribute('order.id', order.id);
return order;
} finally {
span.end();
}
});
}
}
```
## Conclusion
Microservices architecture provides scalability and flexibility but requires careful design. By implementing proper communication patterns, resilience strategies, and observability, you can build robust distributed systems that scale effectively.