Skip to content

wallaceespindola/contract-first-integrations

Repository files navigation

Contract-First Integrations

Java Junit5 Spring Maven Swagger OpenAPI Kafka Avro CI CodeQL License

Introduction

Reference implementation demonstrating contract-first (API-first / schema-first) development patterns for systems integration.

For that purpose, we will build a simple order management system with the following technologies: Apache Kafka for event streaming, PostgreSQL for data persistence, Apache Avro for event serialization, Confluent Schema Registry for schema governance, and Spring Boot for the application layer.

The project emphasizes best practices in API design, schema evolution, idempotency, and error handling.

🎯 What is Contract-First?

Contract-first is an approach where you define the integration boundary first (the contract), then implement code that conforms to it. This repository demonstrates three types of contracts:

  1. REST API Contracts (OpenAPI 3.0)
  2. Kafka Event Contracts (Apache Avro + Schema Registry)
  3. Database Contracts (Flyway migrations)

Key Principle: The contract is the single source of truth. Code, documentation, SDKs, mocks, and tests are all derived from contracts.

graph LR
    subgraph Contracts["📄 Contracts — Source of Truth"]
        OpenAPI["OpenAPI 3.0 YAML\norders-api.v1.yaml"]
        Avro["Avro Schema\nOrderCreated.v1.avsc"]
        SQL["Flyway SQL\nV1__create_orders.sql"]
    end

    subgraph Generated["⚡ Generated Artifacts"]
        Controller["Java Interface\n(Server Stub)"]
        DTO["Java Records\n(Request / Response DTOs)"]
        AvroClass["Java Classes\n(Avro POJOs)"]
        DBSchema["DB Schema\n(Tables & Indexes)"]
    end

    subgraph Teams["👥 Parallel Development"]
        Provider["🏭 Provider Team\nImplements stub"]
        Consumer["🛒 Consumer Team\nUses client SDK"]
    end

    OpenAPI -->|"openapi-generator"| Controller
    OpenAPI -->|"openapi-generator"| DTO
    Avro -->|"avro-maven-plugin"| AvroClass
    SQL -->|"Flyway migrate"| DBSchema

    Controller --> Provider
    DTO --> Provider
    DTO --> Consumer
    AvroClass --> Provider
    AvroClass --> Consumer

    classDef contractNode fill:#f3d9fa,stroke:#9c36b5,color:#000,stroke-width:2px
    classDef genNode fill:#d3f9d8,stroke:#2f9e44,color:#000,stroke-width:2px
    classDef teamNode fill:#dbe4ff,stroke:#3b5bdb,color:#000,stroke-width:2px

    class OpenAPI,Avro,SQL contractNode
    class Controller,DTO,AvroClass,DBSchema genNode
    class Provider,Consumer teamNode
Loading

🚀 Quick Start

Prerequisites

  • Java 21+
  • Maven 3.8+
  • Docker & Docker Compose

Run with Docker Compose

# Start full stack (PostgreSQL, Kafka, Zookeeper, Schema Registry, App)
make compose

# Access the application
open http://localhost:8080/
open http://localhost:8080/swagger-ui.html

Local Development

# 1. Install dependencies and generate Avro classes
make setup

# 2. Start infrastructure (PostgreSQL, Kafka, Schema Registry)
docker compose up postgres kafka schema-registry -d

# 3. Run application
make dev

# 4. Test the API
curl -X POST http://localhost:8080/v1/orders \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "CUST-123",
    "idempotencyKey": "test-key-001",
    "items": [{"sku": "SKU-001", "quantity": 2}]
  }'

📋 Features

✅ REST API (OpenAPI Contracts)

  • POST /v1/orders - Create order with idempotency support
  • GET /v1/orders/{orderId} - Retrieve order by ID
  • Idempotency: Safe request retries using idempotencyKey
  • Error Handling: Standardized ErrorResponse with correlation IDs
  • Swagger UI: Interactive API documentation

✅ Kafka Events (Avro Schemas)

  • OrderCreated events with backward-compatible schema evolution
  • Schema Registry: Confluent Schema Registry for schema governance
  • Idempotent Consumers: Event deduplication using eventId
  • Dead Letter Queue: Failed message handling with debugging info

✅ Database (Flyway Migrations)

  • Versioned migrations: V1 (initial schema), V2 (add source column)
  • Schema evolution: Expand/migrate/contract pattern
  • Idempotency tracking: Tables for REST and Kafka idempotency

🏗️ Architecture

graph TD
    Client["🖥️ Client / Consumer"]

    subgraph AppLayer["⚙️ Spring Boot Application"]
        REST["📋 REST Controller<br/>POST /v1/orders · GET /v1/orders/{id}"]
        OrderService["🔧 OrderService<br/>Idempotency · Order Creation · Events"]
        KafkaConsumer["📨 Kafka Consumer<br/>OrderCreatedListener"]
    end

    subgraph DataLayer["💾 Infrastructure"]
        PostgreSQL[("🗄️ PostgreSQL 17<br/>orders · order_items<br/>idempotency_keys · processed_events")]
        KafkaBroker[("📡 Kafka Broker<br/>+ Confluent Schema Registry")]
    end

    Client -->|"HTTP POST / GET"| REST
    REST --> OrderService
    OrderService -->|"persist"| PostgreSQL
    OrderService -->|"publish OrderCreated"| KafkaBroker
    KafkaBroker -.->|"consume"| KafkaConsumer
    KafkaConsumer --> OrderService

    classDef clientNode fill:#dbe4ff,stroke:#3b5bdb,color:#1a1a2e,stroke-width:2px
    classDef apiNode fill:#d3f9d8,stroke:#2f9e44,color:#1a1a2e,stroke-width:2px
    classDef serviceNode fill:#c3fae8,stroke:#0ca678,color:#1a1a2e,stroke-width:2px
    classDef consumerNode fill:#fff3bf,stroke:#f59f00,color:#1a1a2e,stroke-width:2px
    classDef dbNode fill:#e8eaf6,stroke:#3949ab,color:#1a1a2e,stroke-width:2px
    classDef brokerNode fill:#fff3e0,stroke:#e67700,color:#1a1a2e,stroke-width:2px

    class Client clientNode
    class REST apiNode
    class OrderService serviceNode
    class KafkaConsumer consumerNode
    class PostgreSQL dbNode
    class KafkaBroker brokerNode
Loading

📁 Project Structure

contract-first-integrations/
├── contracts/                   # First-class contract artifacts
│   ├── openapi/
│   │   └── orders-api.v1.yaml   # REST API contract
│   ├── events/
│   │   ├── avro/
│   │   │   ├── OrderCreated.v1.avsc
│   │   │   └── DeadLetterEnvelope.v1.avsc
│   │   ├── topics.md            # Topic semantics
│   │   └── asyncapi.yaml        # AsyncAPI documentation
│   └── db/
│       └── flyway/
│           ├── V1__create_orders.sql
│           └── V2__add_order_source.sql
├── src/main/java/
│   └── com/example/contractfirst/
│       ├── config/              # Spring configuration
│       ├── controller/          # REST controllers
│       ├── service/             # Business logic
│       ├── repository/          # Data access
│       ├── entity/              # JPA entities
│       ├── dto/                 # Java Record DTOs
│       ├── kafka/               # Kafka producers/consumers
│       ├── mapper/              # Entity/DTO mappers
│       └── exception/           # Custom exceptions
├── docker-compose.yml           # Full stack infrastructure
├── Makefile                     # Build commands
└── README.md

🛠️ Makefile Commands

make setup       # Install dependencies and generate Avro sources
make contracts   # Generate Java classes from Avro schemas
make dev         # Run application locally
make test        # Run tests
make test-cov    # Run tests with coverage report
make build       # Build JAR file
make docker      # Build Docker image
make compose     # Start full stack with docker-compose
make down        # Stop docker-compose stack
make clean       # Clean build artifacts

Development Helpers

make logs              # View application logs
make kafka-topics      # List Kafka topics
make kafka-consume     # Consume OrderCreated events
make db-connect        # Connect to PostgreSQL
make db-migrate        # Run Flyway migrations
make db-info           # Show migration status

🧪 Testing

Run Tests

# Run all tests
make test

# Run with coverage
make test-cov
open target/site/jacoco/index.html

Test the REST API

# Create order
curl -X POST http://localhost:8080/v1/orders \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "CUST-123",
    "idempotencyKey": "unique-key-001",
    "items": [
      {"sku": "SKU-001", "quantity": 2},
      {"sku": "SKU-002", "quantity": 1}
    ]
  }'

# Get order
curl http://localhost:8080/v1/orders/ORD-XXXXX

# Test idempotency (same key, same payload → returns cached result)
curl -X POST http://localhost:8080/v1/orders \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "CUST-123",
    "idempotencyKey": "unique-key-001",
    "items": [
      {"sku": "SKU-001", "quantity": 2},
      {"sku": "SKU-002", "quantity": 1}
    ]
  }'

# Test idempotency conflict (same key, different payload → 409 Conflict)
curl -X POST http://localhost:8080/v1/orders \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "CUST-999",
    "idempotencyKey": "unique-key-001",
    "items": [{"sku": "SKU-999", "quantity": 5}]
  }'

Verify Kafka Events

# List topics
make kafka-topics

# Consume OrderCreated events
make kafka-consume

# Check Schema Registry
curl http://localhost:8081/subjects
curl http://localhost:8081/subjects/orders.order-created.v1-value/versions

Verify Database

# Connect to PostgreSQL
make db-connect

# Query tables
SELECT * FROM orders;
SELECT * FROM order_items;
SELECT * FROM idempotency_keys;
SELECT * FROM processed_events;

🔑 Key Patterns Demonstrated

1. REST API Idempotency

  • Client sends idempotencyKey with POST requests
  • Service hashes request body and stores with key
  • Duplicate key + same hash → return cached response (safe retry)
  • Duplicate key + different hash → return 409 Conflict
sequenceDiagram
    participant C as Client
    participant A as REST API
    participant S as OrderService
    participant D as PostgreSQL

    C->>A: POST /v1/orders (idempotencyKey: "key-001")
    activate A
    A->>S: createOrder(request)
    activate S
    S->>D: SELECT WHERE idempotency_key = 'key-001'
    D-->>S: ∅ Not found
    S->>D: INSERT order + idempotency record
    D-->>S: ✓ Saved
    S-->>A: OrderResponse
    deactivate S
    A-->>C: 201 Created
    deactivate A

    Note over C,D: Safe retry — same key replayed

    C->>A: POST /v1/orders (idempotencyKey: "key-001")
    activate A
    A->>S: createOrder(request)
    activate S
    S->>D: SELECT WHERE idempotency_key = 'key-001'
    D-->>S: ✓ Cached response found
    S-->>A: Cached OrderResponse
    deactivate S
    A-->>C: 200 OK (cached)
    deactivate A
Loading

2. Kafka Consumer Idempotency

  • Events include eventId (UUID)
  • Consumer checks processed_events table before processing
  • Already processed → skip (prevents duplicate billing, etc.)

3. Schema Evolution (Backward Compatible)

  • Avro field source is nullable with default
  • Old consumers ignore new field (forward compatible)
  • New consumers handle missing field (backward compatible)
  • Schema Registry enforces compatibility

4. Error Handling

  • All ErrorResponse includes traceId for correlation
  • Standardized error codes: VALIDATION_ERROR, NOT_FOUND, CONFLICT, INTERNAL_ERROR
  • Dead Letter Queue for poison messages

📚 API Documentation

🔧 Technology Stack

  • Java 21 (Stable LTS)
  • Spring Boot 3.5.10 (Web, Data JPA, Actuator, Kafka)
  • PostgreSQL 17 (Database)
  • Apache Kafka 7.8.1 (Event streaming)
  • Apache Avro 1.12.1 (Event serialization)
  • Confluent Schema Registry 7.8.1 (Schema governance)
  • Flyway (Database migrations)
  • Lombok 1.18.46 (Boilerplate reduction)
  • springdoc-openapi 2.8.1 (Swagger UI)
  • TestContainers 1.21.4 (Integration testing)

📖 Further Reading

📄 License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

👤 Author

🤝 Contributing

Contributions, issues, and feature requests are welcome!

⭐ Show your support

Give a ⭐️ if this project helped you understand contract-first development!

About

Contract First Integrations

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors