API Gateway (Stripe-Style)¶
Problem Statement¶
Design an API Gateway for a payment platform like Stripe. The gateway should handle authentication, rate limiting, request routing, versioning, and provide a consistent, developer-friendly API experience while protecting backend services.
Requirements¶
Functional Requirements¶
- Authenticate API requests (API keys, OAuth)
- Route requests to appropriate backend services
- Rate limit per customer/tier
- Support API versioning
- Transform requests/responses
- Validate request schemas
- Provide consistent error handling
- Support idempotency
- Handle webhooks (outbound)
- Provide test mode vs live mode separation
Non-Functional Requirements¶
- Latency: < 20ms added overhead
- Availability: 99.99% uptime
- Scalability: Handle 100K+ requests/second
- Security: PCI-DSS compliant, DDoS protection
- Observability: Complete request tracing
High-Level Architecture¶
Core Components¶
1. Edge Layer¶
- DDoS protection (Cloudflare, AWS Shield)
- TLS termination
- Web Application Firewall (WAF)
- Geographic routing
2. Authentication Module¶
- API key validation
- OAuth 2.0 support
- Test vs Live mode detection
- Customer identification
3. Rate Limiter¶
- Per-customer limits
- Per-endpoint limits
- Tier-based quotas
- Burst handling
4. Version Handler¶
- API version routing
- Version transformation
- Deprecation handling
- Default version management
5. Request Validator¶
- Schema validation
- Required fields check
- Type validation
- Custom business rules
6. Router¶
- Service discovery
- Load balancing
- Circuit breaker
- Retry logic
7. Response Transformer¶
- Consistent error format
- Response filtering
- Pagination handling
- HATEOAS links
Request Processing Pipeline¶
Authentication¶
API Key Structure¶
Authentication Flow¶
@Component
public class AuthenticationFilter {
public AuthResult authenticate(HttpRequest request) {
String authHeader = request.getHeader("Authorization");
// 1. Parse API key
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new AuthenticationException("Missing or invalid Authorization header");
}
String apiKey = authHeader.substring(7);
// 2. Validate key format
ApiKeyType keyType = parseKeyType(apiKey); // sk_, pk_, rk_
boolean isTestMode = apiKey.contains("_test_");
// 3. Lookup key in database/cache
ApiKeyRecord keyRecord = apiKeyService.lookup(apiKey);
if (keyRecord == null) {
throw new AuthenticationException("Invalid API key");
}
// 4. Check key status
if (keyRecord.isRevoked()) {
throw new AuthenticationException("API key has been revoked");
}
// 5. Build auth context
return AuthResult.builder()
.customerId(keyRecord.getCustomerId())
.accountId(keyRecord.getAccountId())
.keyType(keyType)
.isTestMode(isTestMode)
.permissions(keyRecord.getPermissions())
.build();
}
}
Data Model¶
CREATE TABLE api_keys (
id UUID PRIMARY KEY,
key_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 of key
key_prefix VARCHAR(20) NOT NULL, -- sk_test_, pk_live_, etc.
-- Ownership
account_id UUID NOT NULL,
-- Key properties
key_type VARCHAR(20) NOT NULL, -- secret, publishable, restricted
mode VARCHAR(10) NOT NULL, -- test, live
name VARCHAR(255), -- User-defined name
-- Permissions (for restricted keys)
permissions JSONB,
-- Status
status VARCHAR(20) DEFAULT 'active', -- active, revoked
revoked_at TIMESTAMP,
-- Timestamps
created_at TIMESTAMP NOT NULL,
last_used_at TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts(id)
);
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash);
CREATE INDEX idx_api_keys_account ON api_keys(account_id, status);
API Versioning¶
Versioning Strategy¶
Version Transformation¶
@Component
public class VersionTransformer {
private final Map<String, List<Migration>> migrations;
public Object transformRequest(Object request, String fromVersion, String toVersion) {
if (fromVersion.equals(toVersion)) {
return request;
}
List<Migration> applicableMigrations = migrations.values().stream()
.flatMap(List::stream)
.filter(m -> m.appliesTo(fromVersion, toVersion))
.sorted(Comparator.comparing(Migration::getVersion))
.collect(toList());
Object transformed = request;
for (Migration migration : applicableMigrations) {
transformed = migration.transformRequest(transformed);
}
return transformed;
}
public Object transformResponse(Object response, String toVersion, String fromVersion) {
// Reverse transformations for response
List<Migration> applicableMigrations = migrations.values().stream()
.flatMap(List::stream)
.filter(m -> m.appliesTo(fromVersion, toVersion))
.sorted(Comparator.comparing(Migration::getVersion).reversed())
.collect(toList());
Object transformed = response;
for (Migration migration : applicableMigrations) {
transformed = migration.transformResponse(transformed);
}
return transformed;
}
}
// Example migration
public class Migration_2024_01_15 implements Migration {
@Override
public String getVersion() {
return "2024-01-15";
}
@Override
public Object transformResponse(Object response) {
// In 2024-01-15, we renamed "source" to "payment_method"
if (response instanceof ChargeResponse charge) {
// For older versions, include "source" for backwards compatibility
charge.setSource(charge.getPaymentMethod());
}
return response;
}
}
Request Routing¶
Route Configuration¶
routes:
- path: /v1/charges
methods: [GET, POST]
service: payment-service
timeout: 30s
retries: 2
circuit_breaker:
threshold: 5
timeout: 60s
- path: /v1/charges/{id}
methods: [GET, POST]
service: payment-service
timeout: 30s
- path: /v1/customers
methods: [GET, POST]
service: customer-service
timeout: 10s
- path: /v1/subscriptions
methods: [GET, POST, DELETE]
service: billing-service
timeout: 30s
- path: /v1/files
methods: [POST]
service: file-service
timeout: 120s
max_body_size: 50MB
Service Discovery & Load Balancing¶
@Component
public class ServiceRouter {
private final ServiceDiscovery serviceDiscovery;
private final LoadBalancer loadBalancer;
private final CircuitBreakerRegistry circuitBreakers;
public HttpResponse route(RouteConfig route, HttpRequest request) {
// 1. Get healthy instances
List<ServiceInstance> instances = serviceDiscovery.getInstances(route.getService());
if (instances.isEmpty()) {
throw new ServiceUnavailableException("No instances available for " + route.getService());
}
// 2. Check circuit breaker
CircuitBreaker cb = circuitBreakers.get(route.getService());
if (cb.isOpen()) {
throw new ServiceUnavailableException("Service temporarily unavailable");
}
// 3. Select instance (round-robin, least connections, etc.)
ServiceInstance instance = loadBalancer.select(instances, request);
// 4. Make request with retry
int attempts = 0;
Exception lastException = null;
while (attempts < route.getRetries() + 1) {
try {
HttpResponse response = makeRequest(instance, request, route.getTimeout());
if (response.getStatusCode() >= 500) {
cb.recordFailure();
throw new BackendException(response);
}
cb.recordSuccess();
return response;
} catch (TimeoutException | ConnectionException e) {
cb.recordFailure();
lastException = e;
attempts++;
// Try different instance
instance = loadBalancer.select(instances, request);
}
}
throw new ServiceUnavailableException("Failed after " + attempts + " attempts", lastException);
}
}
Error Handling¶
Consistent Error Format¶
{
"error": {
"type": "invalid_request_error",
"code": "parameter_missing",
"message": "The 'amount' parameter is required.",
"param": "amount",
"doc_url": "https://stripe.com/docs/api/errors#missing_required_param"
}
}
Error Types¶
public enum ErrorType {
API_ERROR("api_error"), // Internal server error
AUTHENTICATION_ERROR("authentication_error"), // Invalid API key
CARD_ERROR("card_error"), // Card declined
IDEMPOTENCY_ERROR("idempotency_error"), // Idempotency key issue
INVALID_REQUEST_ERROR("invalid_request_error"), // Bad request
RATE_LIMIT_ERROR("rate_limit_error"); // Rate limited
}
public enum ErrorCode {
// Authentication
API_KEY_EXPIRED("api_key_expired"),
API_KEY_REQUIRED("api_key_required"),
// Request validation
PARAMETER_MISSING("parameter_missing"),
PARAMETER_INVALID("parameter_invalid"),
PARAMETER_UNKNOWN("parameter_unknown"),
// Rate limiting
RATE_LIMIT_EXCEEDED("rate_limit_exceeded"),
// Idempotency
IDEMPOTENCY_KEY_IN_USE("idempotency_key_in_use"),
// Card errors
CARD_DECLINED("card_declined"),
INSUFFICIENT_FUNDS("insufficient_funds"),
EXPIRED_CARD("expired_card");
}
Error Handler¶
@ControllerAdvice
public class GlobalErrorHandler {
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ApiError> handleValidation(ValidationException e) {
return ResponseEntity
.status(400)
.body(ApiError.builder()
.type(ErrorType.INVALID_REQUEST_ERROR)
.code(ErrorCode.PARAMETER_INVALID)
.message(e.getMessage())
.param(e.getParam())
.build());
}
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ApiError> handleAuth(AuthenticationException e) {
return ResponseEntity
.status(401)
.body(ApiError.builder()
.type(ErrorType.AUTHENTICATION_ERROR)
.message(e.getMessage())
.build());
}
@ExceptionHandler(RateLimitException.class)
public ResponseEntity<ApiError> handleRateLimit(RateLimitException e) {
return ResponseEntity
.status(429)
.header("Retry-After", String.valueOf(e.getRetryAfter()))
.body(ApiError.builder()
.type(ErrorType.RATE_LIMIT_ERROR)
.code(ErrorCode.RATE_LIMIT_EXCEEDED)
.message("Rate limit exceeded. Please retry after " + e.getRetryAfter() + " seconds.")
.build());
}
}
Test Mode vs Live Mode¶
Mode Separation¶
Request Validation¶
Schema Validation¶
# OpenAPI-style schema
paths:
/v1/charges:
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- amount
- currency
properties:
amount:
type: integer
minimum: 1
description: Amount in cents
currency:
type: string
enum: [usd, eur, gbp]
source:
type: string
pattern: ^tok_[a-zA-Z0-9]+$
description:
type: string
maxLength: 500
metadata:
type: object
additionalProperties:
type: string
maxProperties: 50
@Component
public class RequestValidator {
public void validate(String path, String method, Object body) {
Schema schema = schemaRegistry.getSchema(path, method);
List<ValidationError> errors = new ArrayList<>();
// Check required fields
for (String required : schema.getRequired()) {
if (!hasField(body, required)) {
errors.add(new ValidationError(
ErrorCode.PARAMETER_MISSING,
"Missing required parameter: " + required,
required
));
}
}
// Validate field types and constraints
for (Map.Entry<String, FieldSchema> entry : schema.getProperties().entrySet()) {
String field = entry.getKey();
FieldSchema fieldSchema = entry.getValue();
Object value = getField(body, field);
if (value != null) {
validateField(field, value, fieldSchema, errors);
}
}
// Check for unknown fields (if strict mode)
if (schema.isStrict()) {
for (String field : getFields(body)) {
if (!schema.getProperties().containsKey(field)) {
errors.add(new ValidationError(
ErrorCode.PARAMETER_UNKNOWN,
"Unknown parameter: " + field,
field
));
}
}
}
if (!errors.isEmpty()) {
throw new ValidationException(errors);
}
}
}
Observability¶
Request Tracing¶
public class TracingFilter {
public void doFilter(HttpRequest request, HttpResponse response) {
// Generate or extract request ID
String requestId = request.getHeader("X-Request-Id");
if (requestId == null) {
requestId = "req_" + generateId();
}
// Add to response
response.setHeader("Request-Id", requestId);
// Create trace context
TraceContext context = TraceContext.builder()
.requestId(requestId)
.customerId(authContext.getCustomerId())
.startTime(Instant.now())
.build();
// Set in thread-local/context
TraceContext.setCurrent(context);
try {
// Process request
filterChain.doFilter(request, response);
} finally {
// Log request
RequestLog log = RequestLog.builder()
.requestId(requestId)
.customerId(context.getCustomerId())
.method(request.getMethod())
.path(request.getPath())
.statusCode(response.getStatusCode())
.durationMs(Duration.between(context.getStartTime(), Instant.now()).toMillis())
.build();
requestLogService.log(log);
}
}
}
Metrics¶
@Component
public class MetricsCollector {
private final MeterRegistry registry;
public void recordRequest(String endpoint, int statusCode, long durationMs) {
// Request count
registry.counter("api.requests",
"endpoint", endpoint,
"status", String.valueOf(statusCode / 100) + "xx"
).increment();
// Latency
registry.timer("api.latency",
"endpoint", endpoint
).record(durationMs, TimeUnit.MILLISECONDS);
}
public void recordRateLimit(String customerId, String endpoint) {
registry.counter("api.rate_limit",
"customer", customerId,
"endpoint", endpoint
).increment();
}
}
Scalability¶
Horizontal Scaling¶
Technology Choices¶
| Component | Technology Options |
|---|---|
| Gateway Framework | Kong, Envoy, Custom (Netty/Vert.x) |
| Cache | Redis Cluster |
| Config Store | etcd, Consul |
| Load Balancer | AWS ALB, nginx |
| Metrics | Prometheus, Datadog |
| Tracing | Jaeger, Zipkin |
| WAF | Cloudflare, AWS WAF |
Interview Discussion Points¶
- How do you handle API key security?
-
Store hashed, rotate regularly, rate limit failed attempts
-
How do you manage API versions over time?
-
Date-based versions, transformation layer, deprecation policy
-
How do you protect against DDoS attacks?
-
Edge layer, rate limiting, IP reputation, auto-scaling
-
How do you achieve low latency?
-
Caching, connection pooling, async processing, edge locations
-
How do you handle backend service failures?
-
Circuit breakers, retries, graceful degradation
-
How do you ensure consistency across gateway instances?
- Stateless design, centralized stores for state