CrackedRuby CrackedRuby

Microservices Architecture

Overview

Microservices architecture decomposes applications into small, autonomous services that communicate over network protocols. Each service runs in its own process, manages its own data, and can be developed, deployed, and scaled independently. This contrasts with monolithic architectures where all functionality exists within a single deployable unit.

The pattern emerged from service-oriented architecture (SOA) principles but emphasizes smaller service granularity, decentralized data management, and organizational alignment. Companies like Netflix, Amazon, and Uber adopted microservices to achieve independent service scaling, technology diversity, and faster deployment cycles.

A microservice typically represents a single business capability. An e-commerce system might decompose into separate services for user management, product catalog, inventory, ordering, payment processing, and shipping. Each service exposes APIs for other services to consume, operates its own database, and maintains clear boundaries from other services.

# Monolithic approach - all capabilities in one application
class OrdersController < ApplicationController
  def create
    user = User.find(params[:user_id])
    product = Product.find(params[:product_id])
    
    # Check inventory
    return unless product.in_stock?
    
    # Process payment
    payment = PaymentProcessor.charge(user, product.price)
    
    # Create order
    order = Order.create(user: user, product: product)
    
    # Update inventory
    product.decrement_stock
    
    # Notify shipping
    ShippingService.schedule_delivery(order)
  end
end

# Microservices approach - separate services communicate via APIs
class OrdersController < ApplicationController
  def create
    # Each concern handled by independent service
    inventory_response = InventoryService.check_availability(params[:product_id])
    return unless inventory_response['available']
    
    payment_response = PaymentService.process(
      user_id: params[:user_id],
      amount: inventory_response['price']
    )
    return unless payment_response['success']
    
    order = Order.create(
      user_id: params[:user_id],
      product_id: params[:product_id],
      payment_id: payment_response['transaction_id']
    )
    
    InventoryService.reserve(params[:product_id], order.id)
    ShippingService.schedule(order.id)
    
    render json: order
  end
end

The transition from monoliths to microservices introduces distributed system complexity. Network calls replace in-process method invocations, requiring failure handling, retry logic, and service discovery mechanisms. Data consistency becomes challenging as transactions can no longer span multiple services through database ACID guarantees.

Key Principles

Service Independence: Each microservice operates autonomously with its own codebase, deployment pipeline, and runtime environment. Teams can modify and deploy services without coordinating with other teams. This independence extends to technology choices—one service might use PostgreSQL while another uses MongoDB, or one might run on Ruby while another uses Go.

Bounded Contexts: Services align with domain-driven design bounded contexts, representing specific business capabilities with clear boundaries. A bounded context defines the scope within which a domain model applies. The "Order" concept in an order management service differs from "Order" in an inventory service—each service maintains its own model without sharing database schemas.

Decentralized Data Management: Microservices avoid shared databases. Each service owns its data and exposes it only through its API. This prevents tight coupling through shared database schemas and allows services to choose appropriate data storage technologies. Services synchronize data through event propagation or API calls rather than direct database access.

Smart Endpoints, Dumb Pipes: Microservices favor lightweight communication mechanisms over complex middleware. Services contain business logic and communicate through simple protocols like HTTP/REST or lightweight message queues. This contrasts with enterprise service bus (ESB) architectures that place transformation and routing logic in middleware.

Design for Failure: Distributed systems experience network failures, service crashes, and latency spikes. Microservices architectures incorporate circuit breakers, timeouts, bulkheads, and retry mechanisms. Services degrade gracefully when dependencies fail rather than cascading failures throughout the system.

Evolutionary Design: Microservices enable incremental system evolution. New services can be added without modifying existing services. Legacy functionality can be extracted into separate services gradually. This supports continuous delivery and experimentation.

Service size remains subjective. A microservice should be small enough for a single team to maintain but large enough to provide meaningful business value. Teams determine boundaries based on business domains, change frequency, and deployment requirements rather than arbitrary size metrics.

Design Considerations

Microservices introduce operational complexity in exchange for development and scaling flexibility. Organizations must evaluate whether the benefits justify the costs. Small teams building simple applications rarely benefit from microservices—the overhead of distributed system management outweighs the advantages.

When Microservices Make Sense: Organizations with multiple development teams building complex applications benefit most. Independent service deployment prevents team blocking, allowing parallel development. Systems requiring different scaling characteristics for different components benefit from independent service scaling. Applications needing technology diversity can adopt different tools for different services. Systems demanding high availability can isolate failures to individual services.

Monolith Advantages: Monolithic architectures offer simpler deployment, testing, and debugging. In-process communication eliminates network latency and failure modes. Transactions span multiple entities through database ACID properties. Refactoring crosses boundaries without service coordination. Small teams maintain monoliths more easily than distributed systems.

Team Structure Impact: Conway's Law states organizations design systems mirroring their communication structure. Microservices require organizational alignment—each service should have a dedicated team with full lifecycle ownership. Shared services across teams reintroduce coordination overhead that microservices aim to eliminate.

Data Consistency Challenges: Distributed transactions across services introduce consistency challenges. Two-phase commit protocols add complexity and reduce availability. Most microservices architectures accept eventual consistency, where services synchronize data through events over time rather than maintaining immediate consistency. This requires careful business logic design to handle temporary inconsistencies.

Testing Complexity: Testing microservices requires service virtualization, contract testing, and end-to-end test environments. Integration tests become more complex as services interact over networks. Test data management spans multiple databases. Teams invest heavily in test automation and continuous integration infrastructure.

Migration Strategy: Extracting microservices from monoliths requires incremental migration. The strangler fig pattern gradually replaces monolith functionality with services while maintaining system operation. Teams identify service boundaries, extract functionality, route traffic to new services, and deprecate old code. Premature decomposition before understanding domain boundaries leads to incorrect service boundaries requiring costly refactoring.

Implementation Approaches

Domain-Driven Design Decomposition: Identify bounded contexts through domain analysis. Collaborate with domain experts to understand business capabilities. Map aggregates, entities, and domain events to service boundaries. Services should align with business capabilities rather than technical layers. An order service manages order lifecycle, a pricing service handles price calculations, and an inventory service tracks stock levels.

Strangler Fig Migration: Extract services incrementally from existing monoliths. Place a proxy between clients and the monolith. Route specific requests to new services while others remain in the monolith. Gradually migrate functionality until the monolith becomes unnecessary. This approach minimizes risk compared to complete rewrites.

# Proxy routing requests to services or monolith
class ServiceRouter
  def route_request(request)
    case request.path
    when /^\/api\/orders/
      # Migrated to microservice
      forward_to_service('orders-service', request)
    when /^\/api\/inventory/
      # Migrated to microservice
      forward_to_service('inventory-service', request)
    else
      # Still in monolith
      forward_to_monolith(request)
    end
  end
  
  def forward_to_service(service_name, request)
    url = service_registry.lookup(service_name)
    HTTP.post("#{url}#{request.path}", json: request.body)
  end
end

API Gateway Pattern: Implement a single entry point for client requests. The gateway routes requests to appropriate services, aggregates responses, and handles cross-cutting concerns like authentication, rate limiting, and request logging. This shields clients from service topology changes and simplifies client implementation.

Backend for Frontend (BFF): Create separate gateway services for different client types. Mobile apps, web browsers, and third-party integrations have different requirements. A mobile BFF might aggregate multiple service calls into a single response to minimize network requests, while a web BFF provides different data shapes. Each BFF optimizes for its client's needs.

Database Per Service: Each service maintains its own database schema and technology. Services expose data only through APIs, preventing direct database access from other services. This requires careful data synchronization strategies. Services publish domain events when data changes, allowing other services to maintain local copies of needed data.

Event-Driven Architecture: Services communicate through domain events published to a message broker. Events represent facts about business state changes. Services subscribe to relevant events and update their state accordingly. This decouples services from each other—publishers don't know about subscribers. Event-driven patterns enable eventual consistency and complex workflow orchestration.

# Publishing domain events
class OrderService
  def create_order(user_id, items)
    order = Order.create(user_id: user_id, items: items)
    
    # Publish event for other services
    EventBus.publish('order.created', {
      order_id: order.id,
      user_id: user_id,
      items: items,
      total: order.total,
      created_at: Time.now.iso8601
    })
    
    order
  end
end

# Subscribing to events in another service
class InventoryService
  def self.setup_subscriptions
    EventBus.subscribe('order.created') do |event|
      event['items'].each do |item|
        reserve_inventory(item['product_id'], item['quantity'])
      end
    end
  end
  
  def self.reserve_inventory(product_id, quantity)
    # Update inventory levels
    product = Product.find(product_id)
    product.reserved_quantity += quantity
    product.save
    
    # Publish confirmation event
    EventBus.publish('inventory.reserved', {
      product_id: product_id,
      quantity: quantity,
      reserved_at: Time.now.iso8601
    })
  end
end

Ruby Implementation

Ruby provides multiple frameworks for building microservices, each with different characteristics. Rails remains popular for larger services, while lightweight frameworks excel for focused services with minimal overhead.

Rails API Mode: Rails 5+ includes API-only mode, removing view rendering and asset pipeline overhead. This creates lightweight services while retaining ActiveRecord, routing, and middleware capabilities.

# Generating a Rails API application
# rails new payment-service --api

class ApplicationController < ActionController::API
  include ActionController::HttpAuthentication::Token::ControllerMethods
  
  before_action :authenticate
  
  private
  
  def authenticate
    authenticate_or_request_with_http_token do |token, options|
      # Verify JWT token from API gateway
      @current_user = JWTService.decode_token(token)
    end
  end
end

class PaymentsController < ApplicationController
  def create
    payment = Payment.create!(
      user_id: @current_user.id,
      amount: params[:amount],
      currency: params[:currency],
      payment_method: params[:payment_method]
    )
    
    result = StripeService.charge(
      amount: payment.amount,
      currency: payment.currency,
      source: params[:payment_method]
    )
    
    payment.update!(
      external_id: result.id,
      status: 'completed'
    )
    
    render json: payment, status: :created
  rescue Stripe::CardError => e
    render json: { error: e.message }, status: :unprocessable_entity
  end
end

Hanami: A lightweight framework designed for microservices. Hanami enforces clean architecture principles with explicit dependencies, repository patterns, and entity separation from persistence. Services remain smaller and more focused than typical Rails applications.

# Hanami service structure
module Inventory
  module Actions
    module Products
      class Check < Inventory::Action
        include Deps['repositories.product_repository']
        
        def handle(request, response)
          product = product_repository.find(request.params[:id])
          
          response.body = {
            available: product.quantity > 0,
            quantity: product.quantity,
            reserved: product.reserved_quantity
          }.to_json
        end
      end
    end
  end
end

Roda: A routing tree web framework optimized for performance. Roda's plugin system allows adding only needed functionality, creating minimal services. Its routing tree approach provides clear request flow visualization.

class UserService < Roda
  plugin :json
  plugin :halt
  plugin :type_routing
  
  route do |r|
    r.on 'users' do
      r.get Integer do |user_id|
        user = User[user_id]
        request.halt(404, error: 'User not found') unless user
        
        {
          id: user.id,
          email: user.email,
          created_at: user.created_at
        }
      end
      
      r.post do
        user = User.create(r.params)
        response.status = 201
        { id: user.id }
      end
    end
  end
end

Service Communication Gems: Ruby gems facilitate inter-service communication. The faraday gem provides HTTP client capabilities with middleware support. The bunny gem enables RabbitMQ integration for message-based communication. The gruf gem implements gRPC servers and clients for high-performance RPC.

# HTTP communication with Faraday
class OrderServiceClient
  def initialize(base_url)
    @conn = Faraday.new(url: base_url) do |f|
      f.request :json
      f.request :retry, max: 3, interval: 0.5
      f.response :json
      f.adapter Faraday.default_adapter
    end
  end
  
  def create_order(user_id, items)
    response = @conn.post('/orders') do |req|
      req.body = { user_id: user_id, items: items }
      req.headers['Authorization'] = "Bearer #{auth_token}"
    end
    
    response.body
  rescue Faraday::Error => e
    Rails.logger.error("Order service error: #{e.message}")
    nil
  end
end

# Message queue communication with Bunny
class EventPublisher
  def initialize
    @connection = Bunny.new(ENV['RABBITMQ_URL'])
    @connection.start
    @channel = @connection.create_channel
    @exchange = @channel.topic('events', durable: true)
  end
  
  def publish(event_type, payload)
    @exchange.publish(
      payload.to_json,
      routing_key: event_type,
      persistent: true,
      content_type: 'application/json'
    )
  end
  
  def close
    @connection.close
  end
end

Service Discovery: Ruby applications integrate with service discovery tools like Consul or Eureka. The diplomat gem provides Consul integration, allowing services to register themselves and discover other services dynamically.

# Consul service registration
require 'diplomat'

class ServiceRegistry
  def register_service(name, port)
    Diplomat::Service.register(
      name: name,
      id: "#{name}-#{Socket.gethostname}-#{port}",
      address: local_ip,
      port: port,
      check: {
        http: "http://#{local_ip}:#{port}/health",
        interval: '10s'
      }
    )
  end
  
  def discover_service(name)
    services = Diplomat::Service.get(name, :all)
    return nil if services.empty?
    
    # Simple load balancing - random selection
    service = services.sample
    "http://#{service.Address}:#{service.ServicePort}"
  end
  
  private
  
  def local_ip
    Socket.ip_address_list.find(&:ipv4_private?).ip_address
  end
end

Common Patterns

Circuit Breaker: Prevents cascading failures by monitoring service call failures. When failures exceed a threshold, the circuit opens, immediately failing calls without attempting requests. After a timeout, the circuit enters half-open state, allowing limited requests to test service recovery.

class CircuitBreaker
  FAILURE_THRESHOLD = 5
  TIMEOUT = 60
  
  def initialize
    @failures = 0
    @last_failure_time = nil
    @state = :closed
  end
  
  def call
    case @state
    when :open
      if Time.now - @last_failure_time > TIMEOUT
        @state = :half_open
        attempt_call
      else
        raise CircuitOpenError, "Circuit breaker is open"
      end
    when :half_open
      attempt_call
    when :closed
      attempt_call
    end
  end
  
  private
  
  def attempt_call
    result = yield
    on_success
    result
  rescue StandardError => e
    on_failure
    raise e
  end
  
  def on_success
    @failures = 0
    @state = :closed
  end
  
  def on_failure
    @failures += 1
    @last_failure_time = Time.now
    @state = :open if @failures >= FAILURE_THRESHOLD
  end
end

# Usage
payment_circuit = CircuitBreaker.new
payment_circuit.call do
  PaymentService.charge(amount: 100)
end

Saga Pattern: Manages distributed transactions across services through coordinated sequences of local transactions. Each step publishes events triggering the next step. Compensating transactions handle failures by undoing completed steps.

class OrderSaga
  def execute(order_params)
    order = create_order(order_params)
    
    begin
      reserve_inventory(order)
      process_payment(order)
      schedule_shipping(order)
      order.mark_completed!
    rescue StandardError => e
      compensate(order, e)
      raise
    end
  end
  
  private
  
  def create_order(params)
    Order.create!(params)
  end
  
  def reserve_inventory(order)
    response = InventoryService.reserve(order.items)
    raise "Inventory unavailable" unless response['success']
    
    order.update!(inventory_reservation_id: response['reservation_id'])
  end
  
  def process_payment(order)
    response = PaymentService.charge(
      amount: order.total,
      user_id: order.user_id
    )
    raise "Payment failed" unless response['success']
    
    order.update!(payment_id: response['payment_id'])
  end
  
  def schedule_shipping(order)
    ShippingService.schedule(order.id)
  end
  
  def compensate(order, error)
    # Undo completed steps in reverse order
    ShippingService.cancel(order.id) if order.shipping_scheduled?
    PaymentService.refund(order.payment_id) if order.payment_id
    InventoryService.release(order.inventory_reservation_id) if order.inventory_reservation_id
    
    order.update!(status: 'failed', error_message: error.message)
  end
end

API Gateway: Centralizes client request handling, routing, and cross-cutting concerns. The gateway handles authentication, rate limiting, request validation, and response aggregation.

class APIGateway < Sinatra::Base
  use Rack::Auth::JWT
  use Rack::Limiter
  
  get '/api/orders/:id' do
    # Aggregate data from multiple services
    order = OrderService.get(params[:id])
    user = UserService.get(order['user_id'])
    payment = PaymentService.get(order['payment_id'])
    
    {
      order: order,
      user: user.slice('id', 'email', 'name'),
      payment: payment.slice('id', 'status', 'amount')
    }.to_json
  end
  
  post '/api/orders' do
    # Validate request
    halt 400, { error: 'Invalid params' }.to_json unless valid_params?
    
    # Forward to service
    response = OrderService.create(JSON.parse(request.body.read))
    
    status response['status']
    response.to_json
  end
  
  private
  
  def valid_params?
    params['user_id'] && params['items']&.any?
  end
end

Service Mesh: Offloads service communication concerns to infrastructure. Service mesh implementations like Istio or Linkerd handle traffic routing, load balancing, failure recovery, and observability through sidecar proxies deployed alongside services. Ruby services communicate through these proxies without implementing communication logic.

Event Sourcing: Stores state changes as sequence of events rather than current state. Services reconstruct state by replaying events. This provides complete audit trails and enables temporal queries.

class OrderEventStore
  def append_event(order_id, event_type, data)
    Event.create!(
      aggregate_id: order_id,
      aggregate_type: 'Order',
      event_type: event_type,
      data: data,
      version: next_version(order_id)
    )
  end
  
  def load_aggregate(order_id)
    events = Event.where(aggregate_id: order_id)
                  .order(:version)
    
    events.reduce(Order.new(id: order_id)) do |order, event|
      order.apply_event(event.event_type, event.data)
      order
    end
  end
  
  private
  
  def next_version(order_id)
    Event.where(aggregate_id: order_id).maximum(:version).to_i + 1
  end
end

class Order
  attr_accessor :id, :user_id, :items, :status, :total
  
  def initialize(id:)
    @id = id
    @items = []
    @status = 'pending'
  end
  
  def apply_event(event_type, data)
    case event_type
    when 'order_created'
      @user_id = data['user_id']
      @items = data['items']
      @total = data['total']
    when 'payment_processed'
      @status = 'paid'
    when 'order_shipped'
      @status = 'shipped'
    end
  end
end

Integration & Interoperability

REST APIs: Most microservices expose REST APIs for synchronous communication. REST provides standardized HTTP methods, status codes, and resource representations. Services document APIs using OpenAPI specifications, enabling client code generation and API testing tools.

# REST API implementation
class ProductsController < ApplicationController
  def index
    products = Product.page(params[:page]).per(params[:per_page] || 20)
    
    render json: {
      data: products.map(&:as_json),
      pagination: {
        current_page: products.current_page,
        total_pages: products.total_pages,
        total_count: products.total_count
      }
    }
  end
  
  def show
    product = Product.find(params[:id])
    render json: product
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'Product not found' }, status: :not_found
  end
  
  def create
    product = Product.new(product_params)
    
    if product.save
      render json: product, status: :created, location: product_url(product)
    else
      render json: { errors: product.errors }, status: :unprocessable_entity
    end
  end
  
  private
  
  def product_params
    params.require(:product).permit(:name, :description, :price, :category)
  end
end

gRPC: Provides high-performance RPC using Protocol Buffers for serialization. gRPC supports bidirectional streaming, strong typing through schema definitions, and multiple language implementations. Services define interfaces in .proto files, generating client and server code.

# Protocol buffer definition (products.proto)
# syntax = "proto3";
# 
# service ProductService {
#   rpc GetProduct (ProductRequest) returns (Product);
#   rpc ListProducts (ListRequest) returns (stream Product);
# }
# 
# message Product {
#   int32 id = 1;
#   string name = 2;
#   double price = 3;
# }

# Ruby gRPC service implementation
class ProductService < Products::ProductService::Service
  def get_product(request, _call)
    product = Product.find(request.id)
    
    Products::Product.new(
      id: product.id,
      name: product.name,
      price: product.price
    )
  rescue ActiveRecord::RecordNotFound
    raise GRPC::NotFound.new('Product not found')
  end
  
  def list_products(request, _call)
    Product.find_each do |product|
      yield Products::Product.new(
        id: product.id,
        name: product.name,
        price: product.price
      )
    end
  end
end

Message Queues: Asynchronous communication through message brokers decouples services temporally and spatially. Services publish messages without knowing consumers. RabbitMQ and Apache Kafka provide message persistence, delivery guarantees, and message ordering.

# RabbitMQ producer
class EventPublisher
  def self.publish_order_event(order)
    connection = Bunny.new(ENV['RABBITMQ_URL'])
    connection.start
    
    channel = connection.create_channel
    exchange = channel.topic('order.events', durable: true)
    
    exchange.publish(
      {
        event: 'order.created',
        order_id: order.id,
        user_id: order.user_id,
        timestamp: Time.now.iso8601
      }.to_json,
      routing_key: 'order.created',
      persistent: true
    )
    
    connection.close
  end
end

# RabbitMQ consumer
class OrderEventConsumer
  def self.start
    connection = Bunny.new(ENV['RABBITMQ_URL'])
    connection.start
    
    channel = connection.create_channel
    exchange = channel.topic('order.events', durable: true)
    queue = channel.queue('shipping.orders', durable: true)
    queue.bind(exchange, routing_key: 'order.created')
    
    queue.subscribe(manual_ack: true) do |delivery_info, properties, body|
      process_order_event(JSON.parse(body))
      channel.ack(delivery_info.delivery_tag)
    rescue StandardError => e
      Rails.logger.error("Failed to process event: #{e.message}")
      channel.nack(delivery_info.delivery_tag, false, true)
    end
    
    connection
  end
  
  def self.process_order_event(event)
    order_id = event['order_id']
    ShippingJob.perform_later(order_id)
  end
end

Service Contracts: Services define contracts specifying request/response formats and behavior. Contract testing verifies services honor their contracts without requiring running dependencies. The Pact framework enables consumer-driven contract testing where consumers specify expectations and providers verify compliance.

# Consumer contract test
require 'pact/consumer/rspec'

Pact.service_consumer 'OrderService' do
  has_pact_with 'InventoryService' do
    mock_service :inventory_service do
      port 1234
    end
  end
end

describe InventoryServiceClient do
  subject { described_class.new('http://localhost:1234') }
  
  describe '#check_availability' do
    before do
      inventory_service.given('product 123 exists with quantity 5')
        .upon_receiving('a request for product availability')
        .with(
          method: :get,
          path: '/products/123/availability'
        )
        .will_respond_with(
          status: 200,
          body: {
            available: true,
            quantity: 5
          }
        )
    end
    
    it 'returns availability' do
      result = subject.check_availability(123)
      expect(result['available']).to be true
      expect(result['quantity']).to eq(5)
    end
  end
end

API Versioning: Services evolve independently requiring API versioning strategies. URL versioning embeds versions in paths (/v1/orders, /v2/orders). Header versioning uses custom headers to specify versions. Accept header versioning uses content negotiation. Services support multiple versions simultaneously during transition periods.

Real-World Applications

Container Deployment: Microservices deploy as containers providing consistent runtime environments. Docker images package services with dependencies, ensuring identical behavior across development, testing, and production environments.

# Dockerfile for Ruby microservice
# FROM ruby:3.2-alpine
# 
# WORKDIR /app
# 
# COPY Gemfile Gemfile.lock ./
# RUN bundle install --without development test
# 
# COPY . .
# 
# EXPOSE 3000
# 
# CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

# docker-compose.yml for local development
# version: '3.8'
# services:
#   order-service:
#     build: ./order-service
#     ports:
#       - "3001:3000"
#     environment:
#       DATABASE_URL: postgresql://postgres:password@postgres:5432/orders
#       RABBITMQ_URL: amqp://rabbitmq
#     depends_on:
#       - postgres
#       - rabbitmq
# 
#   inventory-service:
#     build: ./inventory-service
#     ports:
#       - "3002:3000"
#     environment:
#       DATABASE_URL: postgresql://postgres:password@postgres:5432/inventory

Kubernetes Orchestration: Kubernetes manages containerized microservices at scale. Deployments define desired state, services provide load balancing and service discovery, and ingress controllers route external traffic. Kubernetes handles service scaling, health checks, and rolling updates.

# Kubernetes deployment manifest
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
      - name: order-service
        image: registry.example.com/order-service:1.2.3
        ports:
        - containerPort: 3000
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: order-db-secret
              key: url
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"

Observability: Distributed systems require comprehensive monitoring and logging. Services emit structured logs, metrics, and distributed traces. Centralized logging aggregates logs from all services. Metrics track service health, performance, and business KPIs. Distributed tracing follows requests across service boundaries.

# Structured logging with semantic_logger
class OrderService
  include SemanticLogger::Loggable
  
  def create_order(params)
    logger.tagged(correlation_id: params[:correlation_id]) do
      logger.info("Creating order", user_id: params[:user_id], items_count: params[:items].size)
      
      order = Order.create!(params)
      
      logger.info("Order created", order_id: order.id, total: order.total)
      
      order
    rescue StandardError => e
      logger.error("Order creation failed", error: e.message, backtrace: e.backtrace.first(5))
      raise
    end
  end
end

# Prometheus metrics
require 'prometheus/client'

class MetricsCollector
  def self.prometheus
    @prometheus ||= Prometheus::Client.registry
  end
  
  def self.setup
    @order_counter = prometheus.counter(
      :orders_created_total,
      docstring: 'Total number of orders created'
    )
    
    @order_duration = prometheus.histogram(
      :order_creation_duration_seconds,
      docstring: 'Time spent creating orders'
    )
  end
  
  def self.record_order_created
    @order_counter.increment
  end
  
  def self.measure_order_creation
    start = Time.now
    result = yield
    duration = Time.now - start
    @order_duration.observe(duration)
    result
  end
end

Service Resilience: Production microservices implement timeout policies, retry strategies with exponential backoff, and graceful degradation. Services maintain fallback responses when dependencies fail. Health check endpoints report service status to orchestration platforms.

# Health check endpoint
class HealthController < ApplicationController
  def liveness
    # Basic check - is service running?
    render json: { status: 'ok' }
  end
  
  def readiness
    # Comprehensive check - is service ready to handle traffic?
    checks = {
      database: check_database,
      redis: check_redis,
      rabbitmq: check_rabbitmq
    }
    
    all_healthy = checks.values.all? { |v| v == 'healthy' }
    status_code = all_healthy ? :ok : :service_unavailable
    
    render json: { status: all_healthy ? 'ready' : 'not_ready', checks: checks }, status: status_code
  end
  
  private
  
  def check_database
    ActiveRecord::Base.connection.execute('SELECT 1')
    'healthy'
  rescue StandardError
    'unhealthy'
  end
  
  def check_redis
    Redis.current.ping == 'PONG' ? 'healthy' : 'unhealthy'
  rescue StandardError
    'unhealthy'
  end
  
  def check_rabbitmq
    connection = Bunny.new(ENV['RABBITMQ_URL'])
    connection.start
    connection.close
    'healthy'
  rescue StandardError
    'unhealthy'
  end
end

Zero-Downtime Deployment: Blue-green deployments maintain two production environments, switching traffic between them. Rolling updates gradually replace service instances. Canary releases route small traffic percentages to new versions, monitoring for issues before full rollout. Kubernetes handles these patterns through deployment strategies and gradual traffic shifting.

Reference

Service Communication Patterns

Pattern Use Case Latency Coupling
REST API Synchronous request-response Medium Medium
gRPC High-performance RPC Low Medium
Message Queue Asynchronous event-driven High Low
GraphQL Flexible client queries Medium Medium
WebSocket Real-time bidirectional Low High

Decomposition Strategies

Strategy Description Best For
By Business Capability Align with business functions Domain-driven designs
By Subdomain Match DDD bounded contexts Complex domains
By Use Case Group related user actions User-centric applications
By Volatility Separate frequently changing code High-change environments
Self-Contained Systems Minimal inter-service dependencies Independent teams

Data Management Patterns

Pattern Description Consistency Complexity
Database per Service Each service owns data Eventual High
Shared Database Multiple services share schema Strong Low
API Composition Query multiple services Eventual Medium
CQRS Separate read/write models Eventual High
Event Sourcing Store state as events Eventual High
Saga Coordinate distributed transactions Eventual High

Service Discovery Mechanisms

Mechanism Type Description
Consul Client-side Service registry with health checks
Eureka Client-side Netflix service discovery
Kubernetes Service Server-side Built-in DNS-based discovery
Envoy Server-side Service mesh proxy
ZooKeeper Client-side Distributed coordination

Observability Tools

Tool Purpose Integration
Prometheus Metrics collection Pull-based scraping
Grafana Metrics visualization Dashboard platform
Jaeger Distributed tracing OpenTracing compatible
ELK Stack Log aggregation Elasticsearch-based
Datadog APM monitoring Agent-based collection

Ruby Microservices Gems

Gem Purpose
faraday HTTP client with middleware
bunny RabbitMQ client
gruf gRPC server and client
diplomat Consul service discovery
circuitbox Circuit breaker implementation
wisper Pub/sub events
dry-rb Functional programming tools
sneakers RabbitMQ background jobs

Common HTTP Status Codes

Code Meaning Usage
200 OK Successful GET, PUT, PATCH
201 Created Successful POST creating resource
204 No Content Successful DELETE
400 Bad Request Invalid request format
401 Unauthorized Missing or invalid authentication
403 Forbidden Authenticated but insufficient permissions
404 Not Found Resource does not exist
409 Conflict Request conflicts with current state
422 Unprocessable Entity Valid format but semantic errors
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Unhandled server error
503 Service Unavailable Service temporarily unavailable

Resilience Pattern Parameters

Pattern Configuration Typical Values
Circuit Breaker Failure threshold 5-10 failures
Circuit Breaker Timeout period 30-60 seconds
Retry Max attempts 3-5 retries
Retry Backoff multiplier 2x exponential
Timeout Request timeout 5-30 seconds
Bulkhead Thread pool size 10-50 threads
Rate Limit Requests per second 100-1000 req/s