Skip to content

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

API Gateway 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

Request Processing Pipeline


Authentication

API Key Structure

API Key Format

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

API Versioning

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

Test vs Live Mode


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

Scaling Architecture


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

  1. How do you handle API key security?
  2. Store hashed, rotate regularly, rate limit failed attempts

  3. How do you manage API versions over time?

  4. Date-based versions, transformation layer, deprecation policy

  5. How do you protect against DDoS attacks?

  6. Edge layer, rate limiting, IP reputation, auto-scaling

  7. How do you achieve low latency?

  8. Caching, connection pooling, async processing, edge locations

  9. How do you handle backend service failures?

  10. Circuit breakers, retries, graceful degradation

  11. How do you ensure consistency across gateway instances?

  12. Stateless design, centralized stores for state