Cloud Architecture Patterns: Microservices and Serverless

Modern cloud applications require architectures that can scale, be resilient, and adapt to changing requirements. This guide explores key cloud architecture patterns and their implementation.

Microservices Architecture

Service Decomposition

Break down monolithic applications into smaller, independent services:

// User Service
class UserService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly eventPublisher: EventPublisher,
  ) {}

  async createUser(userData: CreateUserRequest): Promise<User> {
    const user = await this.userRepository.create(userData);
    await this.eventPublisher.publish("UserCreated", {
      userId: user.id,
      email: user.email,
    });
    return user;
  }
}

// Order Service
class OrderService {
  constructor(
    private readonly orderRepository: OrderRepository,
    private readonly userService: UserServiceClient,
    private readonly inventoryService: InventoryServiceClient,
  ) {}

  async createOrder(orderData: CreateOrderRequest): Promise<Order> {
    // Validate user exists
    const user = await this.userService.getUser(orderData.userId);

    // Check inventory
    await this.inventoryService.reserveItems(orderData.items);

    // Create order
    const order = await this.orderRepository.create({
      ...orderData,
      status: "pending",
    });

    return order;
  }
}

API Gateway Pattern

Centralize API management and routing:

// API Gateway configuration
const gateway = new APIGateway();

gateway
  .route("/api/users", "user-service:3001")
  .route("/api/orders", "order-service:3002")
  .middleware(authenticate)
  .middleware(rateLimit)
  .middleware(logRequest);

// Service discovery
class ServiceRegistry {
  private services = new Map<string, ServiceEndpoint>();

  register(serviceName: string, endpoint: ServiceEndpoint) {
    this.services.set(serviceName, endpoint);
  }

  discover(serviceName: string): ServiceEndpoint | undefined {
    return this.services.get(serviceName);
  }
}

Serverless Computing

Function as a Service (FaaS)

Build applications using serverless functions:

// AWS Lambda function
export const handler = async (
  event: APIGatewayEvent,
): Promise<APIGatewayProxyResult> => {
  try {
    const { userId } = event.pathParameters || {};

    if (!userId) {
      return {
        statusCode: 400,
        body: JSON.stringify({ error: "User ID is required" }),
      };
    }

    const user = await getUserFromDatabase(userId);

    return {
      statusCode: 200,
      headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*",
      },
      body: JSON.stringify(user),
    };
  } catch (error) {
    console.error("Error processing request:", error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: "Internal server error" }),
    };
  }
};

Serverless Orchestration

Coordinate multiple serverless functions:

# AWS Step Functions definition
Comment: "User registration workflow"
StartAt: ValidateUser
States:
  ValidateUser:
    Type: Task
    Resource: arn:aws:lambda:us-east-1:123456789012:function:validate-user
    Next: CreateUser
    Catch:
      - ErrorEquals: ["ValidationError"]
        Next: SendValidationError

  CreateUser:
    Type: Task
    Resource: arn:aws:lambda:us-east-1:123456789012:function:create-user
    Next: SendWelcomeEmail
    ResultPath: "$.user"

  SendWelcomeEmail:
    Type: Task
    Resource: arn:aws:lambda:us-east-1:123456789012:function:send-welcome-email
    End: true
    InputPath: "$.user"

Event-Driven Architecture

Event Sourcing

Store state changes as events:

interface DomainEvent {
  eventId: string;
  aggregateId: string;
  eventType: string;
  data: Record<string, any>;
  timestamp: Date;
  version: number;
}

class EventStore {
  async saveEvents(aggregateId: string, events: DomainEvent[]): Promise<void> {
    for (const event of events) {
      await this.db.collection("events").insertOne({
        ...event,
        aggregateId,
      });
    }
  }

  async getEvents(aggregateId: string): Promise<DomainEvent[]> {
    return await this.db.collection("events")
      .find({ aggregateId })
      .sort({ version: 1 })
      .toArray();
  }
}

class Aggregate {
  private events: DomainEvent[] = [];
  private version = 0;

  protected applyEvent(event: DomainEvent) {
    this.events.push(event);
    this.version = event.version;
    this.apply(event);
  }

  protected abstract apply(event: DomainEvent): void;
}

Event Streaming

Process events in real-time:

// Apache Kafka producer
class EventProducer {
  constructor(private kafka: Kafka) {}

  async publishEvent(topic: string, event: DomainEvent) {
    const producer = this.kafka.producer();
    await producer.connect();

    await producer.send({
      topic,
      messages: [{
        key: event.aggregateId,
        value: JSON.stringify(event),
      }],
    });

    await producer.disconnect();
  }
}

// Kafka consumer
class EventConsumer {
  constructor(private kafka: Kafka) {}

  async consume(topic: string, handler: (event: DomainEvent) => Promise<void>) {
    const consumer = this.kafka.consumer({ groupId: "event-handlers" });
    await consumer.connect();

    await consumer.subscribe({ topic, fromBeginning: true });

    await consumer.run({
      eachMessage: async ({ message }) => {
        const event = JSON.parse(message.value!.toString());
        await handler(event);
      },
    });
  }
}

Cloud-Native Patterns

Circuit Breaker Pattern

Handle service failures gracefully:

class CircuitBreaker {
  private failureCount = 0;
  private state: "CLOSED" | "OPEN" | "HALF_OPEN" = "CLOSED";

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === "OPEN") {
      throw new Error("Circuit breaker is OPEN");
    }

    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    this.failureCount = 0;
    this.state = "CLOSED";
  }

  private onFailure() {
    this.failureCount++;
    if (this.failureCount >= this.failureThreshold) {
      this.state = "OPEN";
      setTimeout(() => {
        this.state = "HALF_OPEN";
      }, this.timeout);
    }
  }
}

Saga Pattern

Manage distributed transactions:

interface SagaStep {
  execute: (context: SagaContext) => Promise<void>;
  compensate: (context: SagaContext) => Promise<void>;
}

class SagaOrchestrator {
  async executeSaga(saga: SagaStep[], context: SagaContext) {
    const executedSteps: SagaStep[] = [];

    try {
      for (const step of saga) {
        await step.execute(context);
        executedSteps.push(step);
      }
    } catch (error) {
      // Compensate in reverse order
      for (const step of executedSteps.reverse()) {
        await step.compensate(context);
      }
      throw error;
    }
  }
}

// Usage
const orderSaga: SagaStep[] = [
  {
    execute: async (ctx) => await inventoryService.reserveItems(ctx.items),
    compensate: async (ctx) => await inventoryService.releaseItems(ctx.items),
  },
  {
    execute: async (ctx) => await paymentService.chargeCard(ctx.payment),
    compensate: async (ctx) => await paymentService.refundCharge(ctx.payment),
  },
  {
    execute: async (ctx) => await shippingService.createShipment(ctx.order),
    compensate: async (ctx) => await shippingService.cancelShipment(ctx.order),
  },
];

Scalability Patterns

Database Sharding

Distribute data across multiple databases:

class DatabaseSharder {
  private shards: DatabaseConnection[] = [];

  getShard(key: string): DatabaseConnection {
    const shardIndex = this.hash(key) % this.shards.length;
    return this.shards[shardIndex];
  }

  private hash(key: string): number {
    let hash = 0;
    for (let i = 0; i < key.length; i++) {
      const char = key.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // Convert to 32-bit integer
    }
    return Math.abs(hash);
  }
}

Caching Strategies

Implement multi-level caching:

class CacheManager {
  constructor(
    private l1Cache: Cache, // In-memory
    private l2Cache: Cache, // Redis
    private database: Database,
  ) {}

  async get<T>(key: string): Promise<T | null> {
    // Check L1 cache first
    let value = await this.l1Cache.get<T>(key);
    if (value) return value;

    // Check L2 cache
    value = await this.l2Cache.get<T>(key);
    if (value) {
      // Populate L1 cache
      await this.l1Cache.set(key, value);
      return value;
    }

    // Fetch from database
    value = await this.database.get<T>(key);
    if (value) {
      // Populate both caches
      await Promise.all([
        this.l1Cache.set(key, value),
        this.l2Cache.set(key, value),
      ]);
    }

    return value;
  }
}

Monitoring and Observability

Distributed Tracing

Track requests across services:

// OpenTelemetry tracing
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { JaegerExporter } from "@opentelemetry/exporter-jaeger";

const provider = new NodeTracerProvider();
const exporter = new JaegerExporter();
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();

const tracer = provider.getTracer("my-service");

// Usage
const span = tracer.startSpan("process-order");
try {
  await processOrder(orderData);
  span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
  span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
  throw error;
} finally {
  span.end();
}

Service Mesh

Manage service-to-service communication:

# Istio VirtualService
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: user-service
spec:
  http:
    - match:
        - uri:
            prefix: "/api/users"
      route:
        - destination:
            host: user-service
      timeout: 10s
      retries:
        attempts: 3
        perTryTimeout: 2s
    - match:
        - uri:
            prefix: "/api/admin"
      route:
        - destination:
            host: user-service
      corsPolicy:
        allowOrigins:
          - exact: https://admin.myapp.com

Cost Optimization

Auto-scaling

Scale resources based on demand:

// AWS Application Auto Scaling
const autoScaling = new AutoScalingClient({ region: "us-east-1" });

await autoScaling.putScalingPolicy({
  PolicyName: "cpu-scaling",
  ServiceNamespace: "ecs",
  ResourceId: "service/my-cluster/my-service",
  ScalableDimension: "ecs🐕‍🦺DesiredCount",
  PolicyType: "TargetTrackingScaling",
  TargetTrackingScalingPolicyConfiguration: {
    TargetValue: 70.0,
    PredefinedMetricSpecification: {
      PredefinedMetricType: "ECSServiceAverageCPUUtilization",
    },
  },
});

Conclusion

Cloud architecture patterns provide the foundation for building scalable, resilient, and maintainable applications. Choose patterns that align with your business requirements, team capabilities, and long-term goals. Remember that architecture is evolutionary - start simple and evolve as your needs grow.