Back to Observability: Logging, Tracing, Metrics
Q7Question 7 of 9

What is contract-first (spec-first) API development, and how does it differ from code-first

Key Concepts
  • Contract-first writes the OpenAPI spec before any implementation; it is the single source of truth.
  • Code generation from the spec produces server stubs and typed client SDKs, eliminating drift by construction.
  • Spec diffing in CI classifies changes as compatible or breaking; breaking changes fail the build.
  • Contract-first enables parallel development: frontend mocks from the spec while backend implements.
  • Code-first lets the contract emerge from the controllers; it can change by accident on any refactor.
  • Code-first is convenient for small internal services; contract-first shines for cross-team or public APIs.
  • The goal is that breaking changes require a deliberate, reviewed decision rather than slipping through accidentally.

Definition

Contract-first writes the spec first as the single source of truth, generates code and SDKs from it, and diffs it in CI to catch breaks early; code-first lets the contract emerge from the controllers where it can change by accident.
  • Contract-first – Author the OpenAPI spec before implementation; generate server stubs and client SDKs from it.
  • Code generation – openapi-generator produces server interfaces and typed clients from the spec, eliminating drift.
  • Spec diffing – Compare new spec to baseline in CI; adding optional fields passes, removing fields fails the build.
  • Code-first – Write controllers first; the spec is derived from annotations, implicit and changeable by accident.
  • Parallel development – Frontend and backend teams build simultaneously from the agreed spec.

Imagine National Bank of India is building a new transfer service that many teams will call. In contract-first, the teams agree on an exact written blueprint, and everyone builds from that same drawing. In code-first, one team just starts pouring concrete, and the blueprint is whatever they happened to build, so when they move a wall nobody notices until the neighboring building's door no longer lines up.

Understanding the Concepts

What is contract-first API development?

The OpenAPI document is a real artifact in version control, reviewed like code, and exists before any implementation.

YAML
# transfer-api.yaml - the single source of truth
openapi: 3.0.3
info:
  title: Transfer Service API
  version: 1.0.0
paths:
  /api/v1/transfers:
    post:
      summary: Initiate a transfer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferRequest'
      responses:
        '201':
          description: Transfer initiated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransferResponse'
components:
  schemas:
    TransferRequest:
      type: object
      required: [fromAccountId, toAccountId, amount]
      properties:
        fromAccountId:
          type: string
        toAccountId:
          type: string
        amount:
          type: number
          format: double
    TransferResponse:
      type: object
      properties:
        transferId:
          type: string
        status:
          type: string
TEXT
Contract-first workflow:
  1. Teams agree on the OpenAPI spec
  2. Generate server stubs + client SDKs
  3. Backend implements the generated interfaces
  4. Frontend builds against generated client or mock
  5. Diff the spec in CI to catch breaking changes

How does code generation eliminate drift?

Both server and client are generated from the same spec. If the spec changes, regeneration immediately surfaces compile errors.

JAVA
package com.bank.api;

// Generated interface - developer implements this
public interface TransferApi {
    TransferResponse createTransfer(TransferRequest request);
}

// Developer's implementation
import org.springframework.stereotype.Service;

@Service
public class TransferApiImpl implements TransferApi {
    @Override
    public TransferResponse createTransfer(TransferRequest request) {
        // Business logic here
        return new TransferResponse("TXN-001", "INITIATED");
    }
}
TEXT
If spec removes 'status' from TransferResponse:
  1. Regenerate -> TransferResponse no longer has 'status'
  2. TransferApiImpl: compile error where it sets 'status'
  3. Developer must fix before commit
  4. Drift eliminated BY CONSTRUCTION

How does spec diffing catch breaking changes in CI?

A diff tool compares the new spec to the baseline and classifies changes as compatible or breaking.

JAVA
package com.bank.api;

public class SpecDiffing {
    public static void main(String[] args) {
        System.out.println("CI spec diff results:");
        System.out.println("  COMPATIBLE: added optional field 'description' to TransferRequest");
        System.out.println("  COMPATIBLE: added new endpoint GET /transfers/{id}");
        System.out.println("  BREAKING: removed field 'status' from TransferResponse -> BUILD FAILS");
        System.out.println("  BREAKING: changed 'amount' type from number to string -> BUILD FAILS");
        System.out.println();
        System.out.println("Breaking changes caught BEFORE code is written");
    }
}
TEXT
Expected output:
CI spec diff results:
  COMPATIBLE: added optional field 'description' to TransferRequest
  COMPATIBLE: added new endpoint GET /transfers/{id}
  BREAKING: removed field 'status' from TransferResponse -> BUILD FAILS
  BREAKING: changed 'amount' type from number to string -> BUILD FAILS

Breaking changes caught BEFORE code is written

How does code-first differ?

You write controllers and the spec is derived from annotations. The contract is implicit and can change by accident.

JAVA
package com.bank.api;

public class CodeFirstProblem {
    // Code-first: developer renames a field during refactoring
    // Before: public String holderName;
    // After:  public String customerName;
    // springdoc regenerates spec -> field silently renamed
    // Mobile team's client breaks at runtime
    // Nobody noticed until production!

    public static void main(String[] args) {
        System.out.println("Code-first weakness:");
        System.out.println("  Developer renames holderName -> customerName");
        System.out.println("  Spec auto-generated -> field renamed silently");
        System.out.println("  No CI check caught it");
        System.out.println("  Mobile app breaks in production");
        System.out.println();
        System.out.println("Contract-first solution:");
        System.out.println("  Spec is the source of truth");
        System.out.println("  CI diff would have caught the rename as BREAKING");
        System.out.println("  Build would have FAILED before deployment");
    }
}
TEXT
Expected output:
Code-first weakness:
  Developer renames holderName -> customerName
  Spec auto-generated -> field renamed silently
  Mobile app breaks in production

Contract-first solution:
  CI diff would have caught the rename as BREAKING
  Build would have FAILED before deployment

How does parallel development work?

Frontend generates a client and builds against a mock server. Backend implements the generated interfaces. Both work from the same spec.

JAVA
package com.bank.api;

public class ParallelDevelopment {
    public static void main(String[] args) {
        System.out.println("Parallel development with contract-first:");
        System.out.println("  Day 1: Teams agree on OpenAPI spec");
        System.out.println("  Day 2-5:");
        System.out.println("    Mobile team: generates client SDK, codes against mock server");
        System.out.println("    Backend team: implements generated server interfaces");
        System.out.println("    Partner team: generates their client SDK");
        System.out.println("  Day 6: Both sides connect -> works because same spec");
        System.out.println();
        System.out.println("Without contract-first:");
        System.out.println("  Mobile team waits for backend to finish");
        System.out.println("  Then discovers the API shape is different from what they assumed");
    }
}
TEXT
Expected output:
Parallel development with contract-first:
  Day 1: Teams agree on OpenAPI spec
  Day 2-5: All teams build in parallel from same spec
  Day 6: Both sides connect -> works

When do you use code-first vs contract-first?

| Scenario | Approach | Why |
|---|---|---|
| Public or cross-team API | Contract-first | Many consumers, stability critical |
| Internal service, one team, rapid change | Code-first | Convenience, few consumers |
| Partner integration | Contract-first | Contractual stability required |
| Prototype or MVP | Code-first | Speed over stability |

JAVA
package com.bank.api;

public class WhenToUseWhich {
    public static void main(String[] args) {
        System.out.println("Contract-first for:");
        System.out.println("  Public APIs, cross-team APIs, partner integrations");
        System.out.println("  Where breaking changes cost real money");
        System.out.println();
        System.out.println("Code-first for:");
        System.out.println("  Small internal services, single team, rapid iteration");
        System.out.println("  Where the contract is simple and consumers are few");
        System.out.println();
        System.out.println("Middle ground: code-first with spec committed and diffed in CI");
    }
}
TEXT
Expected output:
Contract-first for: public/cross-team APIs, partner integrations
Code-first for: small internal services, rapid iteration
Middle ground: code-first with spec committed and diffed in CI

What is the pragmatic middle ground?

Code-first with the generated spec committed to version control and diffed in CI gives some guardrails without full spec authoring.

JAVA
package com.bank.api;

public class MiddleGround {
    public static void main(String[] args) {
        System.out.println("Pragmatic middle ground:");
        System.out.println("  1. Write controllers normally (code-first)");
        System.out.println("  2. springdoc generates OpenAPI spec");
        System.out.println("  3. Commit the generated spec to version control");
        System.out.println("  4. CI diffs the spec against the previous version");
        System.out.println("  5. Breaking changes still fail the build");
        System.out.println();
        System.out.println("  Less ceremony than full contract-first");
        System.out.println("  More safety than pure code-first");
    }
}
TEXT
Expected output:
Pragmatic middle ground:
  1. Write controllers (code-first)
  2. Generate spec
  3. Commit spec
  4. Diff in CI
  5. Breaking changes still caught

What should a senior engineer know?

The real goal is that breaking changes require a deliberate, reviewed decision rather than slipping through as an ordinary code edit.

TEXT
Senior guidelines:
  [x] Contract-first for public/cross-team APIs
  [x] Code-first with spec diffing for internal APIs
  [x] openapi-generator for server stubs and client SDKs
  [x] Spec diff in CI: adding optional = pass, removing = fail
  [x] Version the API only when absolutely necessary
  [x] The goal: breaking changes are deliberate decisions, not accidents

Visual Overview

DIAGRAM
  Contract-First Workflow:

  ┌──────────────────┐
  │ OpenAPI Spec      │  Single source of truth
  │ (transfer-api.yml)│  Reviewed like code
  └────────┬─────────┘
           │
     ┌─────┼──────────────────┐
     │     │                  │
     ▼     ▼                  ▼
  ┌──────────┐  ┌──────────┐  ┌──────────┐
  │ Server   │  │ Client   │  │ Mock     │
  │ Stubs    │  │ SDK      │  │ Server   │
  │ (Java    │  │ (TS for  │  │ (for     │
  │ interface│  │  mobile) │  │  testing)│
  └──────────┘  └──────────┘  └──────────┘

  CI Pipeline:
  ┌────────────┐  diff  ┌──────────────┐
  │ New Spec   │───────▶│ Spec Differ  │
  │            │        │              │
  └────────────┘        │ Compatible?  │
  ┌────────────┐        │ Yes -> PASS  │
  │ Baseline   │───────▶│ No -> FAIL   │
  │ Spec       │        └──────────────┘
  └────────────┘

Key Takeaway

Contract-first API development means writing the OpenAPI spec as the single source of truth before any implementation. From that spec you generate server interfaces and typed client SDKs, so both sides are always in sync. Spec diffing in CI classifies every change as compatible or breaking: adding an optional field passes, removing a field fails the build. This catches breaking changes before code exists.

Code-first is the common default where controllers are annotated and the spec is derived from code. It is fast and convenient but the contract is implicit: a developer renaming a field during a refactor silently changes the contract, and consumers only discover it at runtime. At National Bank of India, a code-first refactor renamed a response field and broke a partner integration in production.

For cross-team or public APIs where many consumers depend on stability, contract-first is clearly better. For small internal services with rapid iteration and few consumers, code-first with the spec committed and diffed in CI is a pragmatic middle ground. Whatever the approach, the real goal is that breaking changes require a deliberate, reviewed decision rather than slipping through accidentally.

Remember one sentence: contract-first writes the spec first, generates server stubs and client SDKs from it, and diffs it in CI so breaking changes are caught before code exists, while code-first lets the contract emerge implicitly where it can change by accident.

---

Share
Use ← → arrow keys to navigate