Overview
The API Gateway pattern addresses the challenges that arise when client applications need to interact with multiple microservices in a distributed system. Rather than requiring clients to maintain connections to numerous services and understand their individual protocols and endpoints, the API Gateway pattern introduces a single intermediary component that sits between clients and backend services.
This pattern emerged as microservices architectures became prevalent. In monolithic applications, clients interact with a single application interface. When systems decompose into microservices, clients would otherwise need to manage multiple service endpoints, handle different protocols, implement retry logic for each service, and aggregate data from multiple sources. The API Gateway consolidates these responsibilities into a single component.
The gateway receives all client requests, performs necessary preprocessing, routes requests to appropriate backend services, aggregates responses when needed, and returns consolidated results to clients. This abstraction shields clients from the complexity of the backend architecture and provides a stable interface even as backend services evolve.
Consider an e-commerce application where a mobile client needs to display a product page. The client requires product details from the catalog service, pricing from the pricing service, inventory status from the warehouse service, and customer reviews from the reviews service. Without an API Gateway, the mobile app would make four separate HTTP requests, handle four different response formats, manage four sets of error conditions, and implement retry logic four times. With an API Gateway, the mobile app makes one request to /api/products/123, and the gateway handles all backend coordination.
# Without API Gateway - client makes multiple requests
product = http_get("https://catalog.service/products/123")
pricing = http_get("https://pricing.service/products/123")
inventory = http_get("https://warehouse.service/inventory/123")
reviews = http_get("https://reviews.service/products/123")
# With API Gateway - single request
product_page = http_get("https://api.gateway/products/123")
# Gateway handles all backend calls internally
Key Principles
The API Gateway operates on several foundational principles that define its behavior and responsibilities within a distributed system architecture.
Single Entry Point: All client requests flow through the gateway regardless of their destination backend service. This centralization applies to both public-facing APIs and internal service-to-service communication in some implementations. The gateway maintains routing tables that map incoming request paths to backend service endpoints.
Request Routing: The gateway examines incoming requests and directs them to appropriate backend services based on URL paths, HTTP methods, headers, or request content. Routing rules can include complex logic such as version-based routing, geographic routing, or canary deployments where a percentage of traffic goes to new service versions.
Protocol Translation: Backend services may use different communication protocols than clients expect. The gateway translates between protocols, accepting HTTP/REST requests from web clients while communicating with backend services via gRPC, GraphQL, or message queues. This translation extends to data formats, converting between JSON, Protocol Buffers, or XML as needed.
Request and Response Transformation: The gateway modifies request and response payloads to match client expectations. This includes field mapping, data aggregation from multiple services, filtering sensitive information, and reformatting data structures. The gateway might combine three service responses into a single JSON object or extract specific fields from a complex backend response.
Cross-Cutting Concerns: The gateway implements functionality required by all services rather than duplicating it across individual services. These concerns include authentication verification, authorization checks, rate limiting, request logging, metrics collection, and response caching. Centralizing these concerns reduces code duplication and ensures consistent implementation across services.
Response Aggregation: When a client request requires data from multiple backend services, the gateway fetches data from all necessary services, combines the responses according to business logic, and returns a unified response. This pattern reduces network round trips and simplifies client code.
# Gateway routing principle
class APIGateway
def route(request)
case request.path
when %r{^/products}
ProductService.handle(request)
when %r{^/orders}
OrderService.handle(request)
when %r{^/users}
UserService.handle(request)
else
[404, {}, ["Not Found"]]
end
end
end
# Response aggregation principle
class ProductGateway
def get_product_details(product_id)
product = catalog_service.get(product_id)
pricing = pricing_service.get(product_id)
inventory = warehouse_service.get(product_id)
{
id: product.id,
name: product.name,
price: pricing.current_price,
in_stock: inventory.quantity > 0,
available_quantity: inventory.quantity
}
end
end
Service Discovery Integration: The gateway maintains awareness of available backend services and their network locations. When services scale horizontally or move between hosts, the gateway updates its routing tables dynamically. This requires integration with service discovery mechanisms like Consul, Eureka, or Kubernetes services.
Circuit Breaking: The gateway monitors backend service health and prevents cascading failures. When a backend service fails repeatedly, the gateway opens a circuit breaker, immediately returning error responses without attempting to contact the failing service. After a timeout period, the gateway allows probe requests to determine if the service has recovered.
Design Considerations
Selecting the API Gateway pattern requires evaluating several architectural trade-offs and understanding when this pattern provides value versus adding unnecessary complexity.
When to Use an API Gateway: Microservices architectures with multiple client types benefit most from API Gateways. Mobile applications, web browsers, and third-party integrations often need different data shapes and levels of detail. A mobile app on a slow network requires minimal payloads, while an administrative web interface needs comprehensive data. The gateway tailors responses for each client type without requiring backend services to maintain multiple interfaces.
Systems exposing public APIs to external developers need consistent versioning, authentication, and rate limiting. The gateway enforces these policies uniformly rather than implementing them in each service. Organizations with compliance requirements for logging, audit trails, or data privacy find gateways simplify meeting these requirements by centralizing control points.
When to Avoid an API Gateway: Simple applications with few services or a single client type gain little from adding a gateway layer. The gateway introduces network latency, additional infrastructure complexity, and a potential single point of failure. Applications with latency-sensitive operations where every millisecond matters should minimize intermediary hops.
Systems where clients can directly access backend services without transformation or aggregation don't benefit from gateway overhead. Internal service-to-service communication within a microservices cluster often bypasses the gateway to reduce latency, using service mesh technologies instead.
Complexity Trade-offs: The API Gateway adds a component that requires development, deployment, monitoring, and maintenance. Teams must decide whether to build a custom gateway, adopt an open-source solution, or use a managed cloud service. Custom gateways offer maximum flexibility but require significant engineering investment. Open-source options like Kong, Tyk, or Traefik provide extensive features but need operational expertise. Managed services like AWS API Gateway or Google Cloud Endpoints reduce operational burden but introduce vendor lock-in and potential cost concerns at scale.
Scalability Implications: The gateway handles all client traffic, making it a critical component for system scalability. Gateways must scale horizontally to handle increasing load, requiring stateless design and session management external to gateway instances. Load balancers distribute traffic across gateway instances, and auto-scaling policies add or remove instances based on traffic patterns.
Backend services can scale independently based on their individual load patterns, but the gateway must scale to handle aggregate traffic from all clients. Monitoring gateway performance metrics like request latency, throughput, and error rates becomes critical for maintaining system health.
Failure Impact: A failing gateway affects all client requests regardless of backend service health. This creates a single point of failure unless the architecture includes redundancy. Deploying multiple gateway instances behind load balancers provides failover capability, but coordinated failures or deployment problems can still cause system-wide outages.
Circuit breakers and timeout configurations require careful tuning. Aggressive timeouts reduce latency when services fail but may cause premature failures during temporary slowdowns. Conservative timeouts keep connections open longer, potentially exhausting connection pools during cascading failures.
# Design consideration: Multiple gateway instances
class GatewayCluster
def initialize(gateway_instances)
@instances = gateway_instances
@load_balancer = RoundRobinBalancer.new(gateway_instances)
end
def handle_request(request)
instance = @load_balancer.next_instance
instance.process(request)
rescue ServiceUnavailable
# Failover to another instance
@instances.reject { |i| i == instance }.sample.process(request)
end
end
Data Consistency: Gateways that cache responses improve performance but introduce eventual consistency concerns. Cached data becomes stale when backend services update their state. Cache invalidation strategies range from time-based expiration to event-driven updates. Applications requiring strong consistency should avoid caching or implement cache invalidation carefully.
Security Boundaries: The gateway represents a trust boundary between external clients and internal services. All security validations must occur at the gateway because backend services trust gateway requests. Deploying backend services in private networks unreachable from the internet reinforces this boundary. The gateway becomes the sole entry point, simplifying firewall rules and access control.
Ruby Implementation
Ruby offers several approaches for implementing API Gateways, from Rack-based middleware to full-featured frameworks. The choice depends on system requirements, team expertise, and integration needs.
Rack-Based Gateway: Rack provides the foundation for Ruby web applications and serves as an excellent base for lightweight gateways. A Rack application receives requests and returns responses, making it straightforward to implement routing and forwarding logic.
require 'rack'
require 'net/http'
require 'json'
class SimpleGateway
def initialize
@routes = {
'/api/products' => 'http://products-service:3000',
'/api/orders' => 'http://orders-service:3001',
'/api/users' => 'http://users-service:3002'
}
end
def call(env)
request = Rack::Request.new(env)
path = request.path
service_url = find_service_url(path)
return [404, {}, ['Service not found']] unless service_url
forward_request(service_url, request)
end
private
def find_service_url(path)
@routes.each do |route, url|
return url if path.start_with?(route)
end
nil
end
def forward_request(service_url, request)
uri = URI("#{service_url}#{request.path}")
uri.query = request.query_string unless request.query_string.empty?
http = Net::HTTP.new(uri.host, uri.port)
http_request = Net::HTTP::Get.new(uri)
# Forward headers
request.each_header do |key, value|
http_request[key] = value if key.start_with?('HTTP_')
end
response = http.request(http_request)
[response.code.to_i, response.to_hash, [response.body]]
rescue StandardError => e
[503, {'Content-Type' => 'application/json'},
[JSON.generate({error: 'Service unavailable', details: e.message})]]
end
end
# Run with: rackup -p 8080
run SimpleGateway.new
Sinatra-Based Gateway: Sinatra provides routing DSL and middleware support, making it suitable for gateways requiring more sophisticated routing logic and request processing.
require 'sinatra/base'
require 'faraday'
require 'circuit_breaker'
class APIGateway < Sinatra::Base
configure do
set :service_clients, {
products: create_client('http://products-service:3000'),
orders: create_client('http://orders-service:3001'),
users: create_client('http://users-service:3002')
}
set :circuit_breakers, Hash.new { |h, k|
h[k] = CircuitBreaker.new(threshold: 5, timeout: 30)
}
end
before do
authenticate_request
check_rate_limit
end
get '/api/products/:id' do
product_id = params[:id]
# Aggregate data from multiple services
circuit_breaker = settings.circuit_breakers[:products]
result = circuit_breaker.call do
product = fetch_product(product_id)
pricing = fetch_pricing(product_id)
inventory = fetch_inventory(product_id)
{
product: product,
pricing: pricing,
inventory: inventory
}
end
json result
rescue CircuitBreaker::OpenError
status 503
json error: 'Product service temporarily unavailable'
end
get '/api/orders' do
client = settings.service_clients[:orders]
response = client.get('/orders', user_id: current_user.id)
status response.status
response.body
end
private
def authenticate_request
token = request.env['HTTP_AUTHORIZATION']
halt 401, json(error: 'Unauthorized') unless valid_token?(token)
@current_user = User.from_token(token)
end
def check_rate_limit
limiter = RateLimiter.new(redis: Redis.current)
allowed = limiter.allow?(current_user.id, limit: 100, period: 60)
halt 429, json(error: 'Rate limit exceeded') unless allowed
end
def fetch_product(id)
client = settings.service_clients[:products]
response = client.get("/products/#{id}")
JSON.parse(response.body)
end
def fetch_pricing(id)
client = settings.service_clients[:products]
response = client.get("/pricing/#{id}")
JSON.parse(response.body)
end
def fetch_inventory(id)
client = settings.service_clients[:products]
response = client.get("/inventory/#{id}")
JSON.parse(response.body)
end
def current_user
@current_user
end
def self.create_client(base_url)
Faraday.new(url: base_url) do |conn|
conn.request :json
conn.response :json
conn.adapter Faraday.default_adapter
conn.options.timeout = 5
conn.options.open_timeout = 2
end
end
end
Rails-Based Gateway: Rails applications can act as API Gateways, particularly when the gateway requires complex business logic, database access, or integration with existing Rails infrastructure.
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
resources :products, only: [:index, :show]
resources :orders, only: [:index, :show, :create]
end
end
# app/controllers/api/base_controller.rb
class Api::BaseController < ApplicationController
before_action :authenticate_api_request
before_action :check_rate_limit
rescue_from ServiceUnavailableError, with: :service_unavailable
rescue_from CircuitBreakerOpenError, with: :circuit_breaker_open
private
def authenticate_api_request
token = request.headers['Authorization']
@current_user = UserAuthenticator.authenticate(token)
render json: {error: 'Unauthorized'}, status: 401 unless @current_user
end
def check_rate_limit
limiter = RateLimiter.new(key: "user:#{@current_user.id}")
unless limiter.allow?(limit: 1000, period: 1.hour)
render json: {error: 'Rate limit exceeded'}, status: 429
end
end
def service_unavailable(exception)
render json: {error: 'Service temporarily unavailable'}, status: 503
end
def circuit_breaker_open(exception)
render json: {error: 'Service circuit breaker open'}, status: 503
end
end
# app/controllers/api/products_controller.rb
class Api::ProductsController < Api::BaseController
def show
product_id = params[:id]
result = Rails.cache.fetch("product:#{product_id}", expires_in: 5.minutes) do
ProductServiceClient.get_product_details(product_id)
end
render json: result
end
end
# app/services/product_service_client.rb
class ProductServiceClient
def self.get_product_details(product_id)
circuit_breaker = CircuitBreakerRegistry.get(:products)
circuit_breaker.call do
product = fetch_from_service('/products', product_id)
pricing = fetch_from_service('/pricing', product_id)
inventory = fetch_from_service('/inventory', product_id)
reviews = fetch_from_service('/reviews', product_id)
aggregate_response(product, pricing, inventory, reviews)
end
end
private
def self.fetch_from_service(path, id)
response = Faraday.get("#{ENV['PRODUCT_SERVICE_URL']}#{path}/#{id}")
JSON.parse(response.body)
rescue Faraday::Error => e
Rails.logger.error("Service request failed: #{e.message}")
raise ServiceUnavailableError
end
def self.aggregate_response(product, pricing, inventory, reviews)
{
id: product['id'],
name: product['name'],
description: product['description'],
price: pricing['current_price'],
original_price: pricing['original_price'],
in_stock: inventory['quantity'] > 0,
quantity: inventory['quantity'],
rating: reviews['average_rating'],
review_count: reviews['count']
}
end
end
Kong Gateway with Ruby: Kong is an open-source API Gateway that can be extended with plugins written in Lua or run alongside Ruby services that handle custom business logic.
# Custom Ruby service that Kong forwards to
require 'sinatra/base'
require 'jwt'
class CustomAuthService < Sinatra::Base
post '/validate' do
token = request.env['HTTP_AUTHORIZATION']&.split(' ')&.last
begin
payload = JWT.decode(token, ENV['JWT_SECRET'], true, algorithm: 'HS256')
user_id = payload[0]['user_id']
# Add custom headers for downstream services
headers 'X-User-Id' => user_id.to_s
headers 'X-User-Role' => fetch_user_role(user_id)
status 200
json valid: true, user_id: user_id
rescue JWT::DecodeError
status 401
json valid: false, error: 'Invalid token'
end
end
private
def fetch_user_role(user_id)
# Query user database or cache
user = User.find(user_id)
user.role
end
end
Practical Examples
Mobile Backend Gateway: A mobile application requires optimized payloads and handles spotty network connections. The gateway adapts backend responses for mobile constraints.
class MobileAPIGateway < Sinatra::Base
# Mobile-optimized endpoint that returns minimal data
get '/mobile/v1/feed' do
user_id = current_user.id
# Fetch data from multiple services
posts = fetch_posts(user_id, limit: 20)
users = fetch_user_profiles(posts.map(&:author_id))
media = fetch_media_thumbnails(posts.map(&:media_ids).flatten)
# Transform to mobile-optimized format
feed = posts.map do |post|
{
id: post.id,
text: truncate(post.text, 200),
author: {
id: users[post.author_id].id,
name: users[post.author_id].name,
avatar_url: users[post.author_id].avatar_thumbnail
},
media: post.media_ids.map { |id| media[id].thumbnail_url },
timestamp: post.created_at.iso8601
}
end
# Cache for mobile offline support
headers 'Cache-Control' => 'max-age=300, stale-while-revalidate=600'
json feed: feed, next_page: posts.last&.id
end
# Batch endpoint to reduce round trips
post '/mobile/v1/batch' do
requests = JSON.parse(request.body.read)
results = requests.map do |req|
process_batch_request(req)
end
json results
end
private
def process_batch_request(req)
case req['endpoint']
when 'posts'
fetch_posts(current_user.id, limit: req['limit'])
when 'notifications'
fetch_notifications(current_user.id)
when 'messages'
fetch_messages(current_user.id)
end
rescue StandardError => e
{error: e.message, endpoint: req['endpoint']}
end
end
Backend for Frontend (BFF) Pattern: Different client types require different gateway implementations. A web dashboard needs comprehensive data while a mobile app needs minimal payloads.
# Web BFF - comprehensive data
class WebBFFGateway < Sinatra::Base
get '/web/dashboard' do
user_id = current_user.id
# Fetch comprehensive dashboard data
dashboard = {
user: fetch_complete_user_profile(user_id),
recent_orders: fetch_orders(user_id, limit: 50),
analytics: fetch_analytics(user_id),
recommendations: fetch_recommendations(user_id, limit: 20),
notifications: fetch_all_notifications(user_id)
}
json dashboard
end
end
# Mobile BFF - minimal data
class MobileBFFGateway < Sinatra::Base
get '/mobile/dashboard' do
user_id = current_user.id
# Fetch minimal dashboard data
dashboard = {
user: {
id: user_id,
name: fetch_user_name(user_id)
},
recent_orders: fetch_orders(user_id, limit: 5).map(&:summary),
unread_notifications: fetch_unread_count(user_id)
}
json dashboard
end
end
Microservices Orchestration: The gateway coordinates complex workflows spanning multiple services.
class CheckoutGateway < Sinatra::Base
post '/api/checkout' do
order_data = JSON.parse(request.body.read)
user_id = current_user.id
# Orchestrate checkout workflow
result = process_checkout(user_id, order_data)
if result[:success]
status 201
json order: result[:order], payment: result[:payment]
else
status 422
json errors: result[:errors]
end
end
private
def process_checkout(user_id, order_data)
# Step 1: Validate inventory
inventory_check = InventoryService.check_availability(order_data[:items])
return {success: false, errors: ['Items unavailable']} unless inventory_check[:available]
# Step 2: Calculate pricing with promotions
pricing = PricingService.calculate(order_data[:items], user_id)
# Step 3: Reserve inventory
reservation = InventoryService.reserve(order_data[:items], ttl: 600)
begin
# Step 4: Process payment
payment = PaymentService.charge(
user_id: user_id,
amount: pricing[:total],
method: order_data[:payment_method]
)
# Step 5: Create order
order = OrderService.create(
user_id: user_id,
items: order_data[:items],
payment_id: payment[:id],
total: pricing[:total]
)
# Step 6: Confirm inventory reservation
InventoryService.confirm_reservation(reservation[:id])
# Step 7: Send notifications
NotificationService.send_order_confirmation(user_id, order[:id])
{success: true, order: order, payment: payment}
rescue PaymentError => e
# Rollback inventory reservation
InventoryService.release_reservation(reservation[:id])
{success: false, errors: ["Payment failed: #{e.message}"]}
rescue StandardError => e
# Rollback all operations
InventoryService.release_reservation(reservation[:id])
PaymentService.refund(payment[:id]) if payment
{success: false, errors: ["Checkout failed: #{e.message}"]}
end
end
end
Security Implications
API Gateways serve as the primary security enforcement point in microservices architectures, requiring careful implementation of multiple security layers.
Authentication and Authorization: The gateway verifies client identity and checks permissions before forwarding requests to backend services. This prevents unauthorized access and ensures backend services can trust incoming requests.
class SecureGateway < Sinatra::Base
before do
authenticate_request
authorize_request
end
private
def authenticate_request
auth_header = request.env['HTTP_AUTHORIZATION']
if auth_header&.start_with?('Bearer ')
token = auth_header.split(' ').last
verify_jwt_token(token)
elsif auth_header&.start_with?('ApiKey ')
api_key = auth_header.split(' ').last
verify_api_key(api_key)
else
halt 401, json(error: 'Missing authentication')
end
end
def verify_jwt_token(token)
payload = JWT.decode(token, ENV['JWT_SECRET'], true, algorithm: 'HS256')
@current_user = User.find(payload[0]['user_id'])
@auth_context = {
user_id: @current_user.id,
roles: payload[0]['roles'],
scopes: payload[0]['scopes']
}
rescue JWT::DecodeError, JWT::ExpiredSignature
halt 401, json(error: 'Invalid or expired token')
end
def verify_api_key(key)
api_key = ApiKey.find_by(key: key)
halt 401, json(error: 'Invalid API key') unless api_key&.active?
@auth_context = {
api_key_id: api_key.id,
client_id: api_key.client_id,
scopes: api_key.scopes
}
end
def authorize_request
required_scope = route_scope(request.path, request.request_method)
has_scope = @auth_context[:scopes].include?(required_scope)
halt 403, json(error: 'Insufficient permissions') unless has_scope
end
def route_scope(path, method)
case [path, method]
when [%r{^/api/products}, 'GET']
'read:products'
when [%r{^/api/products}, 'POST']
'write:products'
when [%r{^/api/orders}, 'GET']
'read:orders'
when [%r{^/api/orders}, 'POST']
'write:orders'
else
'unknown'
end
end
end
Rate Limiting and Throttling: The gateway prevents abuse by limiting request rates per client, protecting backend services from overload.
class RateLimiter
def initialize(redis:)
@redis = redis
end
def allow?(client_id:, limit:, period:)
key = "rate_limit:#{client_id}:#{Time.now.to_i / period}"
count = @redis.incr(key)
@redis.expire(key, period * 2) if count == 1
count <= limit
end
def remaining(client_id:, limit:, period:)
key = "rate_limit:#{client_id}:#{Time.now.to_i / period}"
count = @redis.get(key).to_i
[limit - count, 0].max
end
end
class GatewayWithRateLimiting < Sinatra::Base
before do
limiter = RateLimiter.new(redis: Redis.current)
client_id = auth_context[:user_id] || auth_context[:api_key_id]
limit = determine_limit(auth_context)
period = 60 # 1 minute
unless limiter.allow?(client_id: client_id, limit: limit, period: period)
remaining = limiter.remaining(client_id: client_id, limit: limit, period: period)
headers 'X-RateLimit-Remaining' => remaining.to_s
headers 'X-RateLimit-Reset' => ((Time.now.to_i / period + 1) * period).to_s
halt 429, json(error: 'Rate limit exceeded')
end
end
def determine_limit(context)
return 1000 if context[:roles]&.include?('premium')
return 100 if context[:roles]&.include?('basic')
10 # Default for unauthenticated
end
end
Input Validation and Sanitization: The gateway validates all request data before forwarding to backend services, preventing injection attacks and malformed requests.
class InputValidator
def self.validate_product_id(id)
return nil unless id =~ /\A\d+\z/
id.to_i
end
def self.validate_email(email)
return nil unless email =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
email.downcase
end
def self.sanitize_search_query(query)
# Remove potentially dangerous characters
query.gsub(/[<>'"\\]/, '')
end
end
class ValidatingGateway < Sinatra::Base
get '/api/products/:id' do
product_id = InputValidator.validate_product_id(params[:id])
halt 400, json(error: 'Invalid product ID') unless product_id
# Forward validated request
response = ProductService.get(product_id)
json response
end
post '/api/users' do
data = JSON.parse(request.body.read)
email = InputValidator.validate_email(data['email'])
halt 400, json(error: 'Invalid email') unless email
# Additional validations
halt 400, json(error: 'Name too long') if data['name'].length > 100
halt 400, json(error: 'Invalid age') if data['age'] && (data['age'] < 0 || data['age'] > 150)
response = UserService.create(email: email, name: data['name'], age: data['age'])
status 201
json response
end
end
TLS Termination: The gateway handles SSL/TLS connections, decrypting incoming requests and encrypting responses, while communicating with backend services over a secure internal network.
CORS Management: The gateway implements Cross-Origin Resource Sharing policies for browser-based clients.
class CORSGateway < Sinatra::Base
before do
headers 'Access-Control-Allow-Origin' => allowed_origin
headers 'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS'
headers 'Access-Control-Allow-Headers' => 'Content-Type, Authorization'
headers 'Access-Control-Max-Age' => '86400'
end
options '*' do
200
end
private
def allowed_origin
origin = request.env['HTTP_ORIGIN']
whitelist = ENV['ALLOWED_ORIGINS'].split(',')
whitelist.include?(origin) ? origin : whitelist.first
end
end
Performance Considerations
Gateway performance directly impacts overall system responsiveness. Several strategies optimize gateway throughput and latency.
Caching Strategies: The gateway caches responses from backend services to reduce load and improve response times. Cache strategies include time-based expiration, cache invalidation on updates, and conditional requests.
class CachingGateway < Sinatra::Base
configure do
set :cache, Redis.new(url: ENV['REDIS_URL'])
end
get '/api/products/:id' do
product_id = params[:id]
cache_key = "product:#{product_id}"
# Try cache first
cached = settings.cache.get(cache_key)
if cached
headers 'X-Cache' => 'HIT'
return cached
end
# Fetch from service
response = ProductService.get(product_id)
# Cache for 5 minutes
settings.cache.setex(cache_key, 300, response.to_json)
headers 'X-Cache' => 'MISS'
json response
end
# Invalidate cache on updates
put '/api/products/:id' do
product_id = params[:id]
data = JSON.parse(request.body.read)
response = ProductService.update(product_id, data)
# Invalidate cache
cache_key = "product:#{product_id}"
settings.cache.del(cache_key)
json response
end
end
Connection Pooling: The gateway maintains connection pools to backend services, avoiding connection establishment overhead for each request.
class ConnectionPoolGateway
def initialize
@pools = {}
configure_pools
end
def configure_pools
@pools[:products] = ConnectionPool.new(size: 20, timeout: 5) do
Faraday.new(url: ENV['PRODUCT_SERVICE_URL']) do |conn|
conn.adapter :net_http_persistent
conn.options.timeout = 10
end
end
@pools[:orders] = ConnectionPool.new(size: 20, timeout: 5) do
Faraday.new(url: ENV['ORDER_SERVICE_URL']) do |conn|
conn.adapter :net_http_persistent
conn.options.timeout = 10
end
end
end
def get_product(id)
@pools[:products].with do |client|
response = client.get("/products/#{id}")
JSON.parse(response.body)
end
end
end
Parallel Requests: When aggregating data from multiple services, the gateway executes requests in parallel rather than sequentially.
require 'concurrent'
class ParallelGateway < Sinatra::Base
get '/api/dashboard' do
user_id = current_user.id
# Execute requests in parallel
futures = {
profile: Concurrent::Future.execute { fetch_profile(user_id) },
orders: Concurrent::Future.execute { fetch_orders(user_id) },
recommendations: Concurrent::Future.execute { fetch_recommendations(user_id) },
notifications: Concurrent::Future.execute { fetch_notifications(user_id) }
}
# Wait for all futures to complete
results = futures.transform_values(&:value)
json results
rescue StandardError => e
status 500
json error: 'Dashboard data unavailable'
end
end
Request Compression: The gateway compresses response bodies for bandwidth-constrained clients.
class CompressionGateway < Sinatra::Base
use Rack::Deflater
get '/api/large-dataset' do
data = fetch_large_dataset
# Rack::Deflater handles compression based on Accept-Encoding header
json data
end
end
Load Balancing: Multiple gateway instances distribute traffic, and the gateway distributes requests across backend service instances.
class LoadBalancedGateway
def initialize
@service_instances = {
products: ['http://products-1:3000', 'http://products-2:3000', 'http://products-3:3000'],
orders: ['http://orders-1:3001', 'http://orders-2:3001']
}
@current_index = Hash.new(0)
end
def route_request(service_name, path)
instances = @service_instances[service_name]
# Round-robin load balancing
index = @current_index[service_name]
instance = instances[index]
@current_index[service_name] = (index + 1) % instances.length
# Make request to selected instance
response = Faraday.get("#{instance}#{path}")
response.body
rescue Faraday::Error
# Try next instance on failure
retry_with_next_instance(service_name, path, instance)
end
def retry_with_next_instance(service_name, path, failed_instance)
instances = @service_instances[service_name].reject { |i| i == failed_instance }
return nil if instances.empty?
instance = instances.first
response = Faraday.get("#{instance}#{path}")
response.body
end
end
Common Patterns
Several recurring patterns emerge in API Gateway implementations, each addressing specific architectural needs.
Backend for Frontend (BFF): Each client type has a dedicated gateway optimized for its needs. Mobile apps, web applications, and third-party integrations each interact with separate BFF gateways.
# Shared base gateway
class BaseGateway < Sinatra::Base
helpers do
def fetch_user_data(user_id)
UserService.get(user_id)
end
def fetch_orders(user_id, limit: 10)
OrderService.list(user_id: user_id, limit: limit)
end
end
end
# Mobile BFF - minimal data, optimized payloads
class MobileBFF < BaseGateway
get '/home' do
user_data = fetch_user_data(current_user.id)
recent_orders = fetch_orders(current_user.id, limit: 5)
json(
user: {id: user_data.id, name: user_data.name},
orders: recent_orders.map { |o| {id: o.id, status: o.status, total: o.total} }
)
end
end
# Web BFF - comprehensive data
class WebBFF < BaseGateway
get '/home' do
user_data = fetch_user_data(current_user.id)
recent_orders = fetch_orders(current_user.id, limit: 20)
json(
user: user_data,
orders: recent_orders,
analytics: fetch_analytics(current_user.id),
recommendations: fetch_recommendations(current_user.id)
)
end
end
GraphQL Gateway: The gateway exposes a GraphQL API while backend services use REST or other protocols.
require 'graphql'
class Schema < GraphQL::Schema
class Product < GraphQL::Schema::Object
field :id, ID, null: false
field :name, String, null: false
field :price, Float, null: false
field :inventory, Integer, null: false
end
class Query < GraphQL::Schema::Object
field :product, Product, null: false do
argument :id, ID, required: true
end
def product(id:)
product_data = ProductService.get(id)
pricing_data = PricingService.get(id)
inventory_data = InventoryService.get(id)
{
id: product_data['id'],
name: product_data['name'],
price: pricing_data['current_price'],
inventory: inventory_data['quantity']
}
end
end
query Query
end
class GraphQLGateway < Sinatra::Base
post '/graphql' do
query = params[:query]
variables = JSON.parse(params[:variables] || '{}')
result = Schema.execute(query, variables: variables, context: {current_user: current_user})
json result
end
end
Aggregator Pattern: The gateway combines responses from multiple services into a single response, reducing client round trips.
class AggregatorGateway < Sinatra::Base
get '/api/order-details/:id' do
order_id = params[:id]
# Fetch from multiple services
order = OrderService.get(order_id)
customer = CustomerService.get(order['customer_id'])
products = order['items'].map { |item| ProductService.get(item['product_id']) }
shipping = ShippingService.get_status(order['shipping_id'])
# Aggregate into single response
json(
order: {
id: order['id'],
date: order['created_at'],
status: order['status'],
total: order['total']
},
customer: {
name: customer['name'],
email: customer['email']
},
items: products.map.with_index do |product, index|
{
product_name: product['name'],
quantity: order['items'][index]['quantity'],
price: order['items'][index]['price']
}
end,
shipping: {
status: shipping['status'],
estimated_delivery: shipping['estimated_delivery']
}
)
end
end
Chain Pattern: The gateway processes requests through a chain of handlers, each performing specific transformations or validations.
class Handler
attr_accessor :next_handler
def handle(request, response)
raise NotImplementedError
end
def call_next(request, response)
@next_handler&.handle(request, response)
end
end
class AuthenticationHandler < Handler
def handle(request, response)
unless authenticate(request)
response[:error] = 'Authentication failed'
return response
end
call_next(request, response)
end
end
class ValidationHandler < Handler
def handle(request, response)
unless validate(request)
response[:error] = 'Validation failed'
return response
end
call_next(request, response)
end
end
class RateLimitHandler < Handler
def handle(request, response)
unless check_rate_limit(request)
response[:error] = 'Rate limit exceeded'
return response
end
call_next(request, response)
end
end
class RouteHandler < Handler
def handle(request, response)
service_response = route_to_service(request)
response[:data] = service_response
response
end
end
class ChainGateway < Sinatra::Base
configure do
auth = AuthenticationHandler.new
validation = ValidationHandler.new
rate_limit = RateLimitHandler.new
route = RouteHandler.new
auth.next_handler = validation
validation.next_handler = rate_limit
rate_limit.next_handler = route
set :handler_chain, auth
end
post '/api/*' do
request_data = {path: params[:splat].first, body: request.body.read}
response_data = {}
result = settings.handler_chain.handle(request_data, response_data)
if result[:error]
status 400
json error: result[:error]
else
json result[:data]
end
end
end
Reference
Core Components
| Component | Description | Responsibility |
|---|---|---|
| Router | Maps requests to backend services | Path matching, service selection |
| Request Transformer | Modifies request format | Protocol translation, header manipulation |
| Response Aggregator | Combines multiple service responses | Data merging, format standardization |
| Authentication Module | Verifies client identity | Token validation, credential checking |
| Authorization Module | Checks access permissions | Role verification, scope validation |
| Rate Limiter | Controls request frequency | Traffic throttling, quota enforcement |
| Circuit Breaker | Prevents cascading failures | Failure detection, fallback handling |
| Cache Layer | Stores frequently accessed data | Response caching, cache invalidation |
| Load Balancer | Distributes traffic across instances | Instance selection, health checking |
Gateway Deployment Patterns
| Pattern | Use Case | Trade-offs |
|---|---|---|
| Single Gateway | Simple architectures, unified policies | Single point of failure, scaling limitations |
| Per-Client Gateway (BFF) | Multiple client types with different needs | Increased complexity, better optimization |
| Federated Gateway | Large organizations, multiple teams | Distributed ownership, coordination overhead |
| Service Mesh + Gateway | Complex microservices deployments | Advanced features, operational complexity |
Caching Strategies
| Strategy | Implementation | Invalidation |
|---|---|---|
| Time-Based | Cache with TTL expiration | Automatic after timeout |
| Event-Based | Cache until explicit invalidation | On update events |
| Conditional | Cache with ETag/Last-Modified | Client validates freshness |
| Distributed | Shared cache across gateway instances | Coordinated invalidation |
Authentication Methods
| Method | Format | Use Case |
|---|---|---|
| JWT | Bearer token in Authorization header | Stateless authentication |
| API Key | ApiKey token in Authorization header | Third-party integrations |
| OAuth 2.0 | Bearer access token | Delegated authorization |
| mTLS | Client certificate | Service-to-service communication |
Rate Limiting Algorithms
| Algorithm | Characteristics | Implementation |
|---|---|---|
| Token Bucket | Smooth rate limiting, burst handling | Tokens refill at constant rate |
| Leaky Bucket | Strict rate enforcement | Requests processed at fixed rate |
| Fixed Window | Simple counter per time window | Resets at window boundary |
| Sliding Window | Precise rate limiting | Tracks request timestamps |
Circuit Breaker States
| State | Behavior | Transition |
|---|---|---|
| Closed | Normal operation, requests forwarded | Opens on threshold failures |
| Open | Fails fast, no requests forwarded | Moves to half-open after timeout |
| Half-Open | Probe requests allowed | Closes on success, opens on failure |
Ruby Gateway Gems
| Gem | Purpose | Features |
|---|---|---|
| rack | Base HTTP interface | Middleware support, request/response handling |
| sinatra | Lightweight web framework | Routing DSL, middleware integration |
| rails | Full-featured framework | Complete ecosystem, extensive tooling |
| faraday | HTTP client | Connection pooling, middleware support |
| typhoeus | Parallel HTTP requests | Concurrent request execution |
| redis | Caching and rate limiting | Fast in-memory data store |
Monitoring Metrics
| Metric | Purpose | Threshold |
|---|---|---|
| Request Latency | Response time measurement | p95 < 200ms, p99 < 500ms |
| Throughput | Requests per second | Application dependent |
| Error Rate | Failed request percentage | < 1% target |
| Circuit Breaker State | Service health indicator | Track open circuits |
| Cache Hit Rate | Cache effectiveness | > 80% target |
| Connection Pool Usage | Resource utilization | < 80% average |