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 |