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.
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:
- REST API Contracts (OpenAPI 3.0)
- Kafka Event Contracts (Apache Avro + Schema Registry)
- 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.
- Java 21+
- Maven 3.8+
- Docker & 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# 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}]
}'- 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
ErrorResponsewith correlation IDs - Swagger UI: Interactive API documentation
- 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
- Versioned migrations: V1 (initial schema), V2 (add source column)
- Schema evolution: Expand/migrate/contract pattern
- Idempotency tracking: Tables for REST and Kafka idempotency
┌─────────────────────────────────────────────────────────┐
│ Client/Consumer │
└────────────┬────────────────────────────┬───────────────┘
│ │
┌─────────▼──────────┐ ┌─────────▼─────────────┐
│ REST API (OpenAPI) │ │ Kafka Consumer │
│ POST /v1/orders │ │ OrderCreatedListener │
└─────────┬──────────┘ └─────────▲─────────────┘
│ │
┌─────────▼────────────────────────────┴─────────┐
│ Spring Boot Application │
│ ┌──────────────────────────────────────────┐ │
│ │ OrderService (Business Logic) │ │
│ │ - Idempotency checking │ │
│ │ - Order creation │ │
│ │ - Event publishing │ │
│ └──────────┬─────────────────┬─────────────┘ │
│ │ │ │
│ ┌─────────▼──────┐ ┌─────▼──────────┐ │
│ │ PostgreSQL │ │ Kafka Broker │ │
│ │ - orders │ │ + Schema │ │
│ │ - order_items │ │ Registry │ │
│ │ - idempotency │ │ │ │
│ └────────────────┘ └────────────────┘ │
└────────────────────────────────────────────────┘
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
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 artifactsmake 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# Run all tests
make test
# Run with coverage
make test-cov
open target/site/jacoco/index.html# 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}]
}'# 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# Connect to PostgreSQL
make db-connect
# Query tables
SELECT * FROM orders;
SELECT * FROM order_items;
SELECT * FROM idempotency_keys;
SELECT * FROM processed_events;- Client sends
idempotencyKeywith 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
- Events include
eventId(UUID) - Consumer checks
processed_eventstable before processing - Already processed → skip (prevents duplicate billing, etc.)
- Avro field
sourceis nullable with default - Old consumers ignore new field (forward compatible)
- New consumers handle missing field (backward compatible)
- Schema Registry enforces compatibility
- All
ErrorResponseincludestraceIdfor correlation - Standardized error codes:
VALIDATION_ERROR,NOT_FOUND,CONFLICT,INTERNAL_ERROR - Dead Letter Queue for poison messages
- Swagger UI: http://localhost:8080/swagger-ui.html
- OpenAPI Spec: http://localhost:8080/v3/api-docs
- Health Check: http://localhost:8080/actuator/health
- Info: http://localhost:8080/actuator/info
- Static Page: http://localhost:8080/
- 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.0 (Event serialization)
- Confluent Schema Registry 7.8.1 (Schema governance)
- Flyway (Database migrations)
- Lombok (Boilerplate reduction)
- springdoc-openapi (Swagger UI)
- TestContainers (Integration testing)
- OpenAPI Specification
- Apache Avro Documentation
- Flyway Migrations
- Kafka Idempotent Producer
- Schema Registry
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
- Name: Wallace Espindola - Software Engineer Sr. / Solution Architect / Java & Python Dev
- Contact: wallace.espindola@gmail.com
- LinkedIn: linkedin.com/in/wallaceespindola
- GitHub: github.com/wallaceespindola
- Speaker Deck: speakerdeck.com/wallacese
Contributions, issues, and feature requests are welcome!
Give a ⭐️ if this project helped you understand contract-first development!