API Design Patterns for Enterprise Microservices: Lessons from Production Systems

API Design Patterns for Enterprise Microservices: Lessons from Production Systems

The Cost of Bad API Design

A poorly designed API creates debt that compounds over time. Every consumer that integrates with that API becomes invested in its quirks. Every new feature requires workarounds for past mistakes. Every breaking change requires coordinated deployments across teams that may not even work for your organisation anymore.

I’ve seen enterprise API landscapes where teams spend more time managing integration complexity than building features. Where “simple” changes require months of coordination. Where the fear of breaking something prevents any improvement.

The Cost of Bad API Design Infographic

Good API design prevents this. It creates interfaces that are intuitive to use, flexible to extend, and stable over time. It treats APIs as products with their own requirements, documentation, and evolution strategy.

This post examines the patterns that separate APIs teams want to integrate with from APIs teams dread.

Design Principles

Principle 1: Design for the Consumer

APIs exist to serve consumers, not to expose internal implementation. Yet the most common API design mistake is building interfaces that mirror database schemas or internal domain models.

Internal-Centric (Anti-pattern):

GET /customer_accounts
{
  "customer_account_id": 12345,
  "acct_type_cd": "PREM",
  "cust_id_fk": 67890,
  "status_cd": "A",
  "created_ts": "2024-01-15T10:30:00",
  "modified_ts": "2024-02-10T14:22:00"
}

This API leaks implementation details: database column naming conventions, foreign key references, internal status codes. Consumers must understand your schema to use your API.

Consumer-Centric:

GET /customers/67890/accounts/12345
{
  "id": "12345",
  "type": "premium",
  "status": "active",
  "createdAt": "2024-01-15T10:30:00Z",
  "updatedAt": "2024-02-10T14:22:00Z",
  "customer": {
    "id": "67890",
    "href": "/customers/67890"
  }
}

This API uses clear naming, meaningful values, and includes relationships consumers need. The internal implementation is invisible.

Principle 2: Consistency Above All

Inconsistent APIs create cognitive load. Developers must remember which patterns apply to which endpoints. Documentation becomes essential for every call rather than predictable from understanding one.

Consistency Dimensions:

Naming:

  • Resources: plural nouns (/customers, /orders)
  • Actions: HTTP verbs (GET, POST, PUT, DELETE)
  • Query parameters: camelCase or snake_case, but pick one
  • Response fields: same case convention throughout

Design Principles Infographic

Resource Structure:

/resources                    # List
/resources/{id}               # Get single
/resources/{id}/subresources  # Related resources

Response Format:

{
  "data": { ... },           // Always present for success
  "meta": {                  // Pagination, totals
    "pagination": { ... }
  },
  "links": {                 // HATEOAS links
    "self": "...",
    "next": "..."
  }
}

Error Format:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      }
    ]
  }
}

Principle 3: Evolvability Over Perfection

You will not get the API right the first time. Design for evolution.

Additive Changes Only: Add new fields, new endpoints, new optional parameters. Never remove or rename without versioning.

Version from Day One: Include version in URL or header:

  • URL versioning: /v1/customers
  • Header versioning: Accept: application/vnd.company.v1+json

Deprecation Strategy: Communicate deprecation through headers and documentation long before removal:

Deprecation: true
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Link: </v2/customers>; rel="successor-version"

Resource Design Patterns

Pattern 1: Resource Modelling

RESTful APIs model business resources, not database tables or internal operations.

Identify Resources: Ask “what things does this API manage?” not “what tables does this API access?”

Example: Order Management

Poor: Mirrors database tables

/orders
/order_items
/order_status_history
/shipping_info

Better: Models business resources

/orders
/orders/{id}
/orders/{id}/items
/orders/{id}/shipments
/orders/{id}/timeline

Resource Relationships:

Embedded: Include related data in response

GET /orders/123
{
  "id": "123",
  "items": [
    { "productId": "456", "quantity": 2 }
  ]
}

Linked: Reference related resources

GET /orders/123
{
  "id": "123",
  "links": {
    "items": "/orders/123/items",
    "customer": "/customers/789"
  }
}

Expanded: Consumer controls embedding

GET /orders/123?expand=items,customer

Pattern 2: Collection Resources

Collections have their own design considerations.

Pagination: Never return unbounded collections. Implement pagination from day one.

Resource Design Patterns Infographic

Offset-based (simple, limited scale):

GET /orders?offset=0&limit=20

Cursor-based (scalable, consistent):

GET /orders?cursor=eyJpZCI6MTIzfQ&limit=20

Response includes next cursor:

{
  "data": [...],
  "meta": {
    "pagination": {
      "cursor": "eyJpZCI6MTQzfQ",
      "hasMore": true
    }
  },
  "links": {
    "next": "/orders?cursor=eyJpZCI6MTQzfQ&limit=20"
  }
}

Filtering: Standard approach for common filters:

GET /orders?status=pending&customerId=123

Complex filtering with filter parameter:

GET /orders?filter=status:pending,createdAt:>2024-01-01

Sorting:

GET /orders?sort=createdAt:desc,total:asc

Field Selection: Allow consumers to request only needed fields:

GET /orders?fields=id,status,total

Pattern 3: Actions on Resources

Not everything maps cleanly to CRUD. Some operations are actions.

Action as Sub-resource:

POST /orders/123/cancel
POST /orders/123/ship
POST /accounts/456/suspend

Action with Request Body:

POST /orders/123/refund
{
  "amount": 50.00,
  "reason": "Customer request"
}

Avoid:

POST /cancel-order?orderId=123  # RPC-style, not RESTful
PUT /orders/123/status  # Status change may have side effects

Data Patterns

Pattern 1: Request Validation

Validate thoroughly; respond helpfully.

Validation Response:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "Must be a valid email address"
      },
      {
        "field": "quantity",
        "code": "OUT_OF_RANGE",
        "message": "Must be between 1 and 100"
      }
    ]
  }
}

Validation Layers:

  1. Schema validation: correct types, required fields
  2. Format validation: email format, date format
  3. Business validation: inventory availability, account status

Return all validation errors at once; don’t make consumers fix one error at a time.

Pattern 2: Partial Updates

Full resource replacement (PUT) is often impractical. Support partial updates.

JSON Merge Patch (RFC 7396):

PATCH /customers/123
Content-Type: application/merge-patch+json

{
  "email": "[email protected]"
}

Simple but limited: can’t explicitly null fields (null means remove).

JSON Patch (RFC 6902):

PATCH /customers/123
Content-Type: application/json-patch+json

[
  { "op": "replace", "path": "/email", "value": "[email protected]" },
  { "op": "remove", "path": "/middleName" }
]

More powerful but more complex.

Pattern 3: Bulk Operations

Sometimes consumers need to operate on multiple resources.

Batch Endpoint:

POST /orders/batch
{
  "operations": [
    { "method": "POST", "path": "/orders", "body": {...} },
    { "method": "PATCH", "path": "/orders/123", "body": {...} },
    { "method": "DELETE", "path": "/orders/456" }
  ]
}

Response includes per-operation results:

{
  "results": [
    { "status": 201, "body": {...} },
    { "status": 200, "body": {...} },
    { "status": 204 }
  ]
}

Bulk Create:

POST /orders/bulk
{
  "orders": [
    {...},
    {...}
  ]
}

Important: Define clear semantics for partial failure. All-or-nothing? Best-effort?

Error Handling Patterns

Error Response Structure

Errors should be:

  • Machine-readable: error codes for programmatic handling
  • Human-readable: messages for debugging
  • Actionable: what can the consumer do?
{
  "error": {
    "code": "INSUFFICIENT_FUNDS",
    "message": "Account balance is insufficient for this transaction",
    "details": {
      "required": 150.00,
      "available": 75.50
    },
    "documentation": "https://docs.example.com/errors/INSUFFICIENT_FUNDS"
  }
}

HTTP Status Codes

Use status codes correctly:

2xx Success:

  • 200: Success with body
  • 201: Created (return created resource)
  • 202: Accepted (async processing started)
  • 204: Success, no content

4xx Client Errors:

  • 400: Bad request (validation failure)
  • 401: Unauthenticated
  • 403: Forbidden (authenticated but not authorised)
  • 404: Not found
  • 409: Conflict (e.g., version mismatch)
  • 422: Unprocessable entity (valid syntax, invalid semantics)
  • 429: Too many requests (rate limited)

5xx Server Errors:

  • 500: Internal server error
  • 502: Bad gateway
  • 503: Service unavailable (temporary)
  • 504: Gateway timeout

Error Categories

Design error codes around categories:

VALIDATION_*    - Request validation errors
AUTH_*          - Authentication/authorisation errors
RESOURCE_*      - Resource state errors
BUSINESS_*      - Business rule violations
SYSTEM_*        - Internal system errors

This helps consumers implement error handling logic:

if (error.code.startsWith('VALIDATION_')) {
  showFormErrors(error.details);
} else if (error.code.startsWith('AUTH_')) {
  redirectToLogin();
} else {
  showGenericError();
}

Authentication and Authorisation

Authentication Patterns

API Keys: Simple, suitable for server-to-server.

Authorization: ApiKey abc123

Limitations: no expiration, difficult to rotate, no user identity.

OAuth 2.0 / JWT: Standard for user-context APIs.

Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

JWT contains claims; server validates signature without database lookup.

Mutual TLS: Certificate-based authentication for high-security scenarios.

Authorisation Patterns

Scope-Based: Token includes scopes; API checks required scopes:

Token scopes: ["read:orders", "write:orders"]

GET /orders    # Requires read:orders
POST /orders   # Requires write:orders

Resource-Based: Check permissions against specific resources:

Can user 123 read order 456?
Can user 123 write order 456?

Attribute-Based (ABAC): Complex policies considering multiple attributes:

Allow if:
  user.department == resource.owner_department AND
  user.role in ["manager", "admin"] AND
  resource.status != "sealed"

Performance Patterns

Pattern 1: Caching

HTTP caching reduces load and improves response times.

Cache-Control Headers:

Cache-Control: max-age=3600, private
ETag: "abc123"
Last-Modified: Wed, 15 Feb 2024 10:30:00 GMT

Conditional Requests:

GET /products/123
If-None-Match: "abc123"

# Response if unchanged:
304 Not Modified

Cache Invalidation: The hard problem. Options:

  • Short TTLs (simple but less effective)
  • Event-driven invalidation (complex but precise)
  • Stale-while-revalidate (balance)

Pattern 2: Compression

Compress responses for bandwidth efficiency:

Accept-Encoding: gzip, deflate
Content-Encoding: gzip

Pattern 3: Connection Management

Keep-Alive: Reuse TCP connections:

Connection: keep-alive

HTTP/2: Multiplexed requests over single connection. Significant improvement for APIs making many small requests.

Pattern 4: Rate Limiting

Protect APIs from abuse and ensure fair usage:

Response Headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 750
X-RateLimit-Reset: 1707984000

When Limited:

HTTP/1.1 429 Too Many Requests
Retry-After: 60

Rate Limit Strategies:

  • Fixed window: X requests per minute
  • Sliding window: X requests in any 60-second period
  • Token bucket: burst capacity with steady refill

Documentation and Discovery

API Documentation

Documentation is part of the API product.

OpenAPI Specification: Machine-readable API definition:

openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0
paths:
  /orders:
    get:
      summary: List orders
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, shipped, delivered]
      responses:
        '200':
          description: List of orders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderList'

Benefits:

  • Auto-generated documentation
  • Client SDK generation
  • Request validation
  • Testing tools

Documentation Content:

  • Quick start guide
  • Authentication setup
  • Common use cases with examples
  • Error reference
  • Changelog and migration guides

API Discovery

For large API portfolios, help developers find what they need:

API Catalogue: Central registry of all APIs with:

  • Description and purpose
  • Owner and contact
  • Documentation links
  • Health status

Well-Known Endpoints:

GET /.well-known/api-catalog
{
  "apis": [
    {
      "name": "Orders API",
      "spec": "/openapi.yaml",
      "docs": "/docs"
    }
  ]
}

Versioning Strategies

When to Version

Version when making breaking changes:

  • Removing fields or endpoints
  • Changing field types
  • Changing semantic meaning
  • Modifying required fields

Don’t version for:

  • Adding optional fields
  • Adding new endpoints
  • Adding new optional parameters

Versioning Approaches

URL Path Versioning:

/v1/orders
/v2/orders

Clear, simple, cacheable. Some argue it violates REST principles (same resource, different URLs).

Header Versioning:

Accept: application/vnd.company.v2+json

Cleaner URLs but harder to test in browser.

Query Parameter:

/orders?version=2

Simple but pollutes query strings.

Version Lifecycle

  1. Current: Active development, full support
  2. Supported: No new features, security/bug fixes only
  3. Deprecated: Announced sunset date, migration guidance
  4. Retired: No longer available

Communicate lifecycle status clearly:

X-API-Version: 1
X-API-Deprecated: true
X-API-Sunset: 2025-06-01

Testing Patterns

Contract Testing

Ensure API contract is maintained:

// Consumer contract test
describe('Orders API', () => {
  it('returns order with expected structure', async () => {
    const order = await api.get('/orders/123');
    expect(order).toMatchSchema(orderSchema);
  });
});

Pact/Contract Testing: Consumer defines expectations; provider verifies against them. Catches breaking changes before deployment.

Integration Testing

Test against running API:

describe('Orders Integration', () => {
  it('creates and retrieves order', async () => {
    const created = await api.post('/orders', orderData);
    expect(created.status).toBe(201);

    const retrieved = await api.get(`/orders/${created.data.id}`);
    expect(retrieved.data).toMatchObject(orderData);
  });
});

Performance Testing

Establish and monitor baselines:

performance_requirements:
  /orders:
    p50_latency: 50ms
    p99_latency: 200ms
    throughput: 1000/s

Operational Considerations

Observability

APIs need comprehensive observability:

Logging:

  • Request/response (sampling for volume)
  • Error details
  • Performance metrics

Metrics:

  • Request rate by endpoint
  • Error rate by type
  • Latency distributions
  • Active connections

Tracing:

  • Request trace IDs across services
  • Correlation with downstream calls

Health Checks

GET /health
{
  "status": "healthy",
  "checks": {
    "database": "healthy",
    "cache": "healthy",
    "downstream_service": "degraded"
  }
}

Use for load balancer health checks and monitoring.

Graceful Degradation

When dependencies fail:

  • Return cached data with freshness indicator
  • Exclude failed features from response
  • Return partial success for batch operations
X-Degraded: cache=stale, recommendations=unavailable

Building API Culture

Good APIs don’t happen by accident. They require:

Design Reviews: Peer review of API designs before implementation Style Guides: Documented conventions that all teams follow Shared Tooling: Common libraries, templates, and generators API Governance: Standards enforcement and quality gates

The organisations with the best APIs treat them as products. They have owners who care about consumer experience. They measure adoption and satisfaction. They invest in documentation and developer experience.

That investment pays dividends in reduced integration friction, faster partner onboarding, and systems that remain maintainable as they grow.


Ash Ganda advises enterprise technology leaders on API strategy, microservices architecture, and digital transformation. Connect on LinkedIn for ongoing insights.