Skip to content

joeltadeu/customer-service-openapi-spec

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

3 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Java Spring Boot Karate License

Customer Service

A customer management microservice built with Java 25 and Spring Boot 4, using a contract-first approach driven by an OpenAPI specification.

πŸ“‹ Overview

The primary goal of this project is to study and understand how to build a RESTful microservice using a contract-first approach with the OpenAPI Generator Maven Plugin. The API contract is defined in src/main/resources/static/openapi.yaml, and both the model classes and controller interfaces are generated automatically at build time β€” the implementation only needs to fulfil the generated interface.

Key Technical Features:

  • Contract-First API: Models and controller interfaces are auto-generated from openapi.yaml using the OpenAPI Generator Maven Plugin.
  • Database: Customer data is persisted in MySQL 8 using Spring Data JDBC. Schema and seed scripts are applied automatically on startup from src/main/resources.
  • API Documentation: Interactive documentation is provided via springdoc-openapi (Swagger UI), pointing directly at the OpenAPI spec file.
  • API Testing: End-to-end API tests are written with Karate 2.0, running against a live Spring Boot context spun up by JUnit 5.

πŸ—οΈ Architecture

Entity Relationship Diagram

Alt text

A Customer is the aggregate root. Each customer can have multiple Address, Email, and Document records. All child entities are linked via customer_id and cascade-deleted when the customer is removed.

ℹ️ Database scripts are in src/main/resources:

File Description
schema.sql Creates all tables
data.sql Seeds initial data

πŸ› οΈ Technology Stack

  • Language: Java 25
  • Framework: Spring Boot 4.0.6
  • Data: Spring Data JDBC, MySQL 8
  • API Contract: OpenAPI Generator Maven Plugin 7.22.0
  • API Documentation: springdoc-openapi 3.0.3 (Swagger UI)
  • API Testing: Karate 2.0.0 (JUnit 5)
  • Validation: Hibernate Validator 9, Jakarta Bean Validation 4
  • Infrastructure: Docker, Docker Compose (Spring Boot Docker Compose integration)

πŸ“š API Endpoints

Customer

Retrieve Customer by Id

  • Endpoint: GET /v1/customers/{id}

Request example:

curl --location 'http://localhost:9081/v1/customers/1'

Response example 200 OK:

{
  "id": 1,
  "firstName": "Aoife",
  "lastName": "Murphy",
  "birthday": "1980-12-13",
  "addresses": [
    { "id": 1, "street": "6 Bridge St.", "eircode": "N36RP84", "city": "Cork", "county": "County Cork", "country": "Ireland" }
  ],
  "emails": [
    { "id": 1, "type": "WORK", "email": "aoife.murphy@aib.ie" },
    { "id": 2, "type": "PERSONAL", "email": "aoife.murphy@gmail.com" }
  ],
  "documents": [
    { "id": 1, "type": "PPS", "documentNumber": "48378IA" },
    { "id": 2, "type": "PASSPORT", "documentNumber": "FA3891IU" }
  ]
}
Status Description
200 OK Customer found
404 Not Found No customer for the given id

Retrieve Customers List

  • Endpoint: GET /v1/customers
Parameter Description Example
pageNumber Page number (zero-based) 0
pageSize Records per page 10
firstName Filter by first name Aoife
lastName Filter by last name Murphy

Request example:

curl --location 'http://localhost:9081/v1/customers?pageSize=10&pageNumber=0&firstName=Aoife&lastName=Murphy'

Response example 200 OK:

{
  "totalElements": 1,
  "totalPages": 1,
  "size": 10,
  "content": [
    { "id": 1, "firstName": "Aoife", "lastName": "Murphy", "birthday": "1980-12-13" }
  ],
  "number": 0,
  "first": true,
  "last": true,
  "empty": false
}

Create Customer

  • Endpoint: POST /v1/customers

Request example:

curl --location 'http://localhost:9081/v1/customers' \
--header 'Content-Type: application/json' \
--data '{
    "firstName": "Irwin",
    "lastName": "Streich",
    "birthday": "1977-09-23"
}'

Response example 201 Created:

{ "id": 5, "firstName": "Irwin", "lastName": "Streich", "birthday": "1977-09-23" }

Update Customer

  • Endpoint: PUT /v1/customers/{id}

Request example:

curl --location --request PUT 'http://localhost:9081/v1/customers/1' \
--header 'Content-Type: application/json' \
--data '{
    "firstName": "Irwin",
    "lastName": "Streich",
    "birthday": "1980-05-12"
}'
Status Description
200 OK Customer updated
404 Not Found No customer for the given id

Delete Customer

  • Endpoint: DELETE /v1/customers/{id}

Request example:

curl --location --request DELETE 'http://localhost:9081/v1/customers/1'
Status Description
204 No Content Customer deleted
404 Not Found No customer for the given id

Address

Retrieve Address by Id

  • Endpoint: GET /v1/customers/{customerId}/addresses/{id}

Request example:

curl --location 'http://localhost:9081/v1/customers/1/addresses/1'

Response example 200 OK:

{ "id": 1, "street": "6 Bridge St.", "eircode": "N36RP84", "city": "Cork", "county": "County Cork", "country": "Ireland" }

Retrieve Addresses List by Customer Id

  • Endpoint: GET /v1/customers/{customerId}/addresses
Parameter Description Example
pageNumber Page number 0
pageSize Records per page 10
street Filter by street Bridge
city Filter by city Cork
county Filter by county County Cork
country Filter by country Ireland
eircode Filter by eircode N36RP84

Create Address

  • Endpoint: POST /v1/customers/{customerId}/addresses

Request example:

curl --location 'http://localhost:9081/v1/customers/1/addresses' \
--header 'Content-Type: application/json' \
--data '{
    "street": "Hammes Highway",
    "complement": "103",
    "eircode": "N21RP11",
    "city": "Mosciskifort",
    "county": "Roscommon",
    "country": "Ireland"
}'

Response example 201 Created:

{ "id": 5, "street": "Hammes Highway", "complement": "103", "eircode": "N21RP11", "city": "Mosciskifort", "county": "Roscommon", "country": "Ireland" }

Update Address

  • Endpoint: PUT /v1/customers/{customerId}/addresses/{id}

Request example:

curl --location --request PUT 'http://localhost:9081/v1/customers/1/addresses/1' \
--header 'Content-Type: application/json' \
--data '{
    "street": "Hammes Highway",
    "complement": "105",
    "eircode": "N21RP11",
    "city": "Dublin",
    "county": "Co. Dublin",
    "country": "Ireland"
}'

Delete Address

  • Endpoint: DELETE /v1/customers/{customerId}/addresses/{id}

Request example:

curl --location --request DELETE 'http://localhost:9081/v1/customers/1/addresses/1'

Email

Retrieve Email by Id

  • Endpoint: GET /v1/customers/{customerId}/emails/{id}

Request example:

curl --location 'http://localhost:9081/v1/customers/1/emails/1'

Response example 200 OK:

{ "id": 1, "type": "WORK", "email": "aoife.murphy@aib.ie" }

Create Email

  • Endpoint: POST /v1/customers/{customerId}/emails

Request example:

curl --location 'http://localhost:9081/v1/customers/1/emails' \
--header 'Content-Type: application/json' \
--data '{
    "type": "WORK",
    "email": "aoife.murphy@guiness.ie"
}'

Response example 201 Created:

{ "id": 10, "type": "WORK", "email": "aoife.murphy@guiness.ie" }

Update Email

  • Endpoint: PUT /v1/customers/{customerId}/emails/{id}

Request example:

curl --location --request PUT 'http://localhost:9081/v1/customers/1/emails/1' \
--header 'Content-Type: application/json' \
--data '{
    "type": "PERSONAL",
    "email": "aoife.murphy@hotmail.com"
}'

Delete Email

  • Endpoint: DELETE /v1/customers/{customerId}/emails/{id}

Request example:

curl --location --request DELETE 'http://localhost:9081/v1/customers/1/emails/1'

Document

Retrieve Document by Id

  • Endpoint: GET /v1/customers/{customerId}/documents/{id}

Request example:

curl --location 'http://localhost:9081/v1/customers/1/documents/1'

Response example 200 OK:

{ "id": 1, "type": "PASSPORT", "documentNumber": "FU129837" }

Create Document

  • Endpoint: POST /v1/customers/{customerId}/documents

Request example:

curl --location 'http://localhost:9081/v1/customers/1/documents' \
--header 'Content-Type: application/json' \
--data '{
    "type": "PPS",
    "documentNumber": "46751M"
}'

Response example 201 Created:

{ "id": 10, "type": "PPS", "documentNumber": "46751M" }

Update Document

  • Endpoint: PUT /v1/customers/{customerId}/documents/{id}

Request example:

curl --location --request PUT 'http://localhost:9081/v1/customers/1/documents/1' \
--header 'Content-Type: application/json' \
--data '{
    "type": "PASSPORT",
    "documentNumber": "FU4673761"
}'

Delete Document

  • Endpoint: DELETE /v1/customers/{customerId}/documents/{id}

Request example:

curl --location --request DELETE 'http://localhost:9081/v1/customers/1/documents/1'

πŸ“˜ Documentation & Testing

Swagger UI

Once the service is running, access the interactive API documentation at:

Alt text

Postman Collection

A Postman collection is provided to test all APIs:

ℹ️ Location: _assets/postman/customer-service.postman_collection.json


πŸ§ͺ Tests

Unit Tests

Execute unit tests (service and controller layers, no Spring context loaded):

mvn test

Karate API Tests

Karate tests spin up a live Spring Boot context on a random port and run end-to-end API scenarios. They are organised as follows:

File Purpose
CustomerSteps.feature Reusable step definitions for Customer CRUD
AddressSteps.feature Reusable step definitions for Address CRUD
EmailSteps.feature Reusable step definitions for Email CRUD
DocumentSteps.feature Reusable step definitions for Document CRUD
SavingCustomerTest.feature Customer create / update / get / delete scenarios
SavingAddressTest.feature Address create / update / get / delete scenarios
SavingEmailTest.feature Email create / update / get / delete scenarios
SavingDocumentTest.feature Document create / update / get / delete scenarios
Actuator.feature Spring Boot Actuator health check

Execute the Karate tests:

mvn test -Dkarate.env=local -Dtest=br.com.customer.karate.KarateTestRunner

Results are generated in the target/karate-reports/ folder.

Alt text

Alt text


πŸš€ Build & Run

Build

mvn clean package

Running Locally

Ensure MySQL is running and accessible at localhost:3306 (database: customer_cascade, user: root, password: root), then:

mvn spring-boot:run -Dspring-boot.run.profiles=local

The service starts on port 9081.

Running with Docker

Build the image:

docker build -f Dockerfile -t customer-openapi-service:1.0.0 .

Running as a Container

The service includes a Dockerfile to build a lightweight, secure container image using Distroless. The Distroless image contains only the JRE and the application itself β€” no shell, no package manager, and a significantly smaller attack surface.

FROM gcr.io/distroless/java25-debian13

COPY target/customer-openapi-service.jar app.jar

EXPOSE 8081

ENV SPRING_PROFILES_ACTIVE=local

ENTRYPOINT ["java", "-jar", "/app.jar"]

Build and run the container image:

# Build the image
docker build -t customer-openapi-service .

# Run the container (requires Redis to be running and reachable)
docker run -e SPRING_PROFILES_ACTIVE=local -p 9081:9081 customer-openapi-service

Running with Docker Compose

Spring Boot's Docker Compose integration (spring-boot-docker-compose) will automatically start the required services defined in docker-compose.yml when the application boots.

πŸ“„ License

This project is licensed under the MIT License β€” see the LICENSE file for details.


About

Contract-first REST microservice built with Java 25 and Spring Boot 4. Models and controller interfaces are auto-generated from an OpenAPI spec using the OpenAPI Generator Plugin. Tested with unit tests and end-to-end API tests using Karate 2.0.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages