Ensuring Microservice Compatibility: Pact JVM for Contract Testing

Contract testing is a critical practice in microservices architecture that ensures services can communicate reliably without requiring full integration testing environments. Pact JVM is a Java implementation of the Pact contract testing framework that enables consumer-driven contracts between services.


What is Contract Testing?

Contract testing verifies the interactions between service consumers and providers at the boundary, ensuring that:

  • Consumers can correctly use provider APIs
  • Providers don't break existing consumer integrations
  • APIs evolve safely without breaking changes

Pact JVM Architecture

[Consumer Tests] → [Pact File] → [Pact Broker] ← [Provider Tests] ← [Provider API]
|                 |              |               |                 |
Generate pact     JSON contract   Share & version   Verify against   Actual service
from tests        specification   contracts        real API          implementation

Hands-On Tutorial: Complete Contract Testing Setup with Pact JVM

Let's build a complete contract testing solution for a user management system with multiple consumers and providers.

Step 1: Project Structure Setup

pact-contract-testing/
├── user-service-consumer/
├── user-service-provider/
├── notification-service-consumer/
├── pact-broker/
├── shared-contracts/
└── build-scripts/

Step 2: Consumer Project Setup

user-service-consumer/pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>user-service-consumer</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Pact JVM -->
<pact.version>4.6.8</pact.version>
<junit.version>5.10.1</junit.version>
<!-- HTTP Client -->
<rest-assured.version>5.3.2</rest-assured.version>
<jackson.version>2.16.1</jackson.version>
</properties>
<dependencies>
<!-- Pact Consumer JUnit 5 -->
<dependency>
<groupId>au.com.dius.pact.consumer</groupId>
<artifactId>junit5</artifactId>
<version>${pact.version}</version>
<scope>test</scope>
</dependency>
<!-- Pact Provider -->
<dependency>
<groupId>au.com.dius.pact.provider</groupId>
<artifactId>junit5</artifactId>
<version>${pact.version}</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- HTTP Client for tests -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>${rest-assured.version}</version>
<scope>test</scope>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
</project>

Step 3: Consumer Test Implementation

user-service-consumer/src/test/java/com/example/consumer/UserServiceConsumerTest.java:

package com.example.consumer;
import au.com.dius.pact.consumer.MockServer;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import au.com.dius.pact.core.model.RequestResponsePact;
import au.com.dius.pact.core.model.annotations.Pact;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import java.util.List;
import java.util.Map;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "UserService", port = "8080")
public class UserServiceConsumerTest {
private final ObjectMapper objectMapper = new ObjectMapper();
// Pact for getting a user by ID
@Pact(consumer = "WebApplication")
public RequestResponsePact getUserByIdPact(PactDslWithProvider builder) {
return builder
.given("user with ID 123 exists")
.uponReceiving("a request for user with ID 123")
.path("/api/users/123")
.method("GET")
.headers(Map.of(
"Accept", "application/json",
"X-Request-ID", "test-request-123"
))
.willRespondWith()
.status(200)
.headers(Map.of(
"Content-Type", "application/json",
"X-Rate-Limit-Remaining", "99"
))
.body(new au.com.dius.pact.consumer.dsl.PactDslJsonBody()
.stringType("id", "123")
.stringType("email", "[email protected]")
.stringType("firstName", "John")
.stringType("lastName", "Doe")
.stringType("status", "ACTIVE")
.datetime("createdAt", "yyyy-MM-dd'T'HH:mm:ss'Z'")
.minArrayLike("roles", 1)
.stringType("ADMIN")
.closeArray()
)
.toPact();
}
// Pact for creating a new user
@Pact(consumer = "WebApplication")
public RequestResponsePact createUserPact(PactDslWithProvider builder) {
return builder
.given("no user with email [email protected] exists")
.uponReceiving("a request to create a new user")
.path("/api/users")
.method("POST")
.headers(Map.of(
"Content-Type", "application/json",
"Accept", "application/json",
"X-Request-ID", "test-request-456"
))
.body(new au.com.dius.pact.consumer.dsl.PactDslJsonBody()
.stringType("email", "[email protected]")
.stringType("firstName", "Jane")
.stringType("lastName", "Smith")
.stringType("password", "securePassword123!")
)
.willRespondWith()
.status(201)
.headers(Map.of(
"Content-Type", "application/json",
"Location", "/api/users/456"
))
.body(new au.com.dius.pact.consumer.dsl.PactDslJsonBody()
.stringType("id", "456")
.stringType("email", "[email protected]")
.stringType("firstName", "Jane")
.stringType("lastName", "Smith")
.stringType("status", "PENDING_VERIFICATION")
.datetime("createdAt", "yyyy-MM-dd'T'HH:mm:ss'Z'")
)
.toPact();
}
// Pact for getting all users with pagination
@Pact(consumer = "WebApplication")
public RequestResponsePact getUsersWithPaginationPact(PactDslWithProvider builder) {
return builder
.given("users exist in the system")
.uponReceiving("a request for users with pagination")
.path("/api/users")
.method("GET")
.query("page=0&size=10")
.headers(Map.of(
"Accept", "application/json"
))
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(new au.com.dius.pact.consumer.dsl.PactDslJsonBody()
.numberType("page", 0)
.numberType("size", 10)
.numberType("totalPages", 5)
.numberType("totalElements", 50)
.minArrayLike("content", 2)
.stringType("id")
.stringType("email")
.stringType("firstName")
.stringType("lastName")
.stringType("status")
.datetime("createdAt", "yyyy-MM-dd'T'HH:mm:ss'Z'")
.closeArray()
)
.toPact();
}
// Pact for updating a user
@Pact(consumer = "WebApplication")
public RequestResponsePact updateUserPact(PactDslWithProvider builder) {
return builder
.given("user with ID 123 exists")
.uponReceiving("a request to update user with ID 123")
.path("/api/users/123")
.method("PUT")
.headers(Map.of(
"Content-Type", "application/json",
"Accept", "application/json"
))
.body(new au.com.dius.pact.consumer.dsl.PactDslJsonBody()
.stringType("firstName", "Johnny")
.stringType("lastName", "Doe Updated")
)
.willRespondWith()
.status(200)
.headers(Map.of("Content-Type", "application/json"))
.body(new au.com.dius.pact.consumer.dsl.PactDslJsonBody()
.stringType("id", "123")
.stringType("email", "[email protected]")
.stringType("firstName", "Johnny")
.stringType("lastName", "Doe Updated")
.stringType("status", "ACTIVE")
.datetime("updatedAt", "yyyy-MM-dd'T'HH:mm:ss'Z'")
)
.toPact();
}
// Pact for user not found
@Pact(consumer = "WebApplication")
public RequestResponsePact getUserNotFoundPact(PactDslWithProvider builder) {
return builder
.given("user with ID 999 does not exist")
.uponReceiving("a request for non-existent user with ID 999")
.path("/api/users/999")
.method("GET")
.headers(Map.of("Accept", "application/json"))
.willRespondWith()
.status(404)
.headers(Map.of("Content-Type", "application/json"))
.body(new au.com.dius.pact.consumer.dsl.PactDslJsonBody()
.stringType("error", "USER_NOT_FOUND")
.stringType("message", "User with ID 999 not found")
.stringType("path", "/api/users/999")
.numberType("status", 404)
.datetime("timestamp", "yyyy-MM-dd'T'HH:mm:ss'Z'")
)
.toPact();
}
@Test
@PactTestFor(pactMethod = "getUserByIdPact")
void testGetUserById(MockServer mockServer) throws JsonProcessingException {
log.info("Testing GET user by ID against mock server: {}", mockServer.getUrl());
User user = given()
.baseUri(mockServer.getUrl())
.header("Accept", "application/json")
.header("X-Request-ID", "test-request-123")
.when()
.get("/api/users/123")
.then()
.statusCode(200)
.header("Content-Type", "application/json")
.header("X-Rate-Limit-Remaining", not(emptyString()))
.body("id", equalTo("123"))
.body("email", equalTo("[email protected]"))
.body("firstName", equalTo("John"))
.body("lastName", equalTo("Doe"))
.body("status", equalTo("ACTIVE"))
.body("createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"))
.body("roles", hasSize(greaterThanOrEqualTo(1)))
.extract()
.as(User.class);
assertNotNull(user);
assertEquals("123", user.getId());
assertEquals("[email protected]", user.getEmail());
}
@Test
@PactTestFor(pactMethod = "createUserPact")
void testCreateUser(MockServer mockServer) throws JsonProcessingException {
log.info("Testing POST create user against mock server: {}", mockServer.getUrl());
CreateUserRequest createRequest = new CreateUserRequest(
"[email protected]",
"Jane",
"Smith",
"securePassword123!"
);
User createdUser = given()
.baseUri(mockServer.getUrl())
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("X-Request-ID", "test-request-456")
.body(objectMapper.writeValueAsString(createRequest))
.when()
.post("/api/users")
.then()
.statusCode(201)
.header("Content-Type", "application/json")
.header("Location", "/api/users/456")
.body("id", equalTo("456"))
.body("email", equalTo("[email protected]"))
.body("firstName", equalTo("Jane"))
.body("lastName", equalTo("Smith"))
.body("status", equalTo("PENDING_VERIFICATION"))
.extract()
.as(User.class);
assertNotNull(createdUser);
assertEquals("456", createdUser.getId());
assertEquals("[email protected]", createdUser.getEmail());
}
@Test
@PactTestFor(pactMethod = "getUsersWithPaginationPact")
void testGetUsersWithPagination(MockServer mockServer) {
log.info("Testing GET users with pagination against mock server: {}", mockServer.getUrl());
UserPage userPage = given()
.baseUri(mockServer.getUrl())
.header("Accept", "application/json")
.queryParam("page", 0)
.queryParam("size", 10)
.when()
.get("/api/users")
.then()
.statusCode(200)
.header("Content-Type", "application/json")
.body("page", equalTo(0))
.body("size", equalTo(10))
.body("totalPages", equalTo(5))
.body("totalElements", equalTo(50))
.body("content", hasSize(greaterThanOrEqualTo(2)))
.extract()
.as(UserPage.class);
assertNotNull(userPage);
assertEquals(0, userPage.getPage());
assertEquals(10, userPage.getSize());
assertTrue(userPage.getContent().size() >= 2);
}
@Test
@PactTestFor(pactMethod = "updateUserPact")
void testUpdateUser(MockServer mockServer) throws JsonProcessingException {
log.info("Testing PUT update user against mock server: {}", mockServer.getUrl());
UpdateUserRequest updateRequest = new UpdateUserRequest(
"Johnny",
"Doe Updated"
);
User updatedUser = given()
.baseUri(mockServer.getUrl())
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.body(objectMapper.writeValueAsString(updateRequest))
.when()
.put("/api/users/123")
.then()
.statusCode(200)
.header("Content-Type", "application/json")
.body("id", equalTo("123"))
.body("firstName", equalTo("Johnny"))
.body("lastName", equalTo("Doe Updated"))
.body("updatedAt", matchesPattern("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z"))
.extract()
.as(User.class);
assertNotNull(updatedUser);
assertEquals("Johnny", updatedUser.getFirstName());
assertEquals("Doe Updated", updatedUser.getLastName());
}
@Test
@PactTestFor(pactMethod = "getUserNotFoundPact")
void testGetUserNotFound(MockServer mockServer) {
log.info("Testing GET non-existent user against mock server: {}", mockServer.getUrl());
given()
.baseUri(mockServer.getUrl())
.header("Accept", "application/json")
.when()
.get("/api/users/999")
.then()
.statusCode(404)
.header("Content-Type", "application/json")
.body("error", equalTo("USER_NOT_FOUND"))
.body("message", equalTo("User with ID 999 not found"))
.body("status", equalTo(404));
}
// Domain classes for testing
public static class User {
private String id;
private String email;
private String firstName;
private String lastName;
private String status;
private String createdAt;
private String updatedAt;
private List<String> roles;
// Getters and setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getCreatedAt() { return createdAt; }
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
public String getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; }
public List<String> getRoles() { return roles; }
public void setRoles(List<String> roles) { this.roles = roles; }
}
public record CreateUserRequest(
String email,
String firstName,
String lastName,
String password
) {}
public record UpdateUserRequest(
String firstName,
String lastName
) {}
public static class UserPage {
private int page;
private int size;
private int totalPages;
private int totalElements;
private List<User> content;
// Getters and setters
public int getPage() { return page; }
public void setPage(int page) { this.page = page; }
public int getSize() { return size; }
public void setSize(int size) { this.size = size; }
public int getTotalPages() { return totalPages; }
public void setTotalPages(int totalPages) { this.totalPages = totalPages; }
public int getTotalElements() { return totalElements; }
public void setTotalElements(int totalElements) { this.totalElements = totalElements; }
public List<User> getContent() { return content; }
public void setContent(List<User> content) { this.content = content; }
}
}

Step 4: Provider Test Implementation

user-service-provider/pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>user-service-provider</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Spring Boot -->
<spring-boot.version>3.2.0</spring-boot.version>
<!-- Pact -->
<pact.version>4.6.8</pact.version>
<junit.version>5.10.1</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Pact Provider -->
<dependency>
<groupId>au.com.dius.pact.provider</groupId>
<artifactId>junit5</artifactId>
<version>${pact.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>au.com.dius.pact.provider</groupId>
<artifactId>spring</artifactId>
<version>${pact.version}</version>
<scope>test</scope>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

user-service-provider/src/test/java/com/example/provider/UserServiceProviderTest.java:

package com.example.provider;
import au.com.dius.pact.provider.junit5.HttpTestTarget;
import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junitsupport.Provider;
import au.com.dius.pact.provider.junitsupport.loader.PactBroker;
import au.com.dius.pact.provider.junitsupport.loader.PactBrokerAuth;
import au.com.dius.pact.provider.spring.junit5.PactVerificationSpringProvider;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
@Slf4j
@Provider("UserService")
@PactBroker(
host = "${PACT_BROKER_HOST:localhost}",
port = "${PACT_BROKER_PORT:9292}",
scheme = "${PACT_BROKER_SCHEME:http}",
authentication = @PactBrokerAuth(
username = "${PACT_BROKER_USERNAME:}",
password = "${PACT_BROKER_PASSWORD:}"
)
)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
public class UserServiceProviderTest {
@LocalServerPort
private int port;
@BeforeEach
void setUp(PactVerificationContext context) {
log.info("Setting up Pact verification context on port: {}", port);
context.setTarget(new HttpTestTarget("localhost", port));
}
@TestTemplate
@ExtendWith(PactVerificationSpringProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
log.info("Starting Pact verification for provider: UserService");
context.verifyInteraction();
}
}

user-service-provider/src/test/java/com/example/provider/UserServiceStateHandler.java:

package com.example.provider;
import au.com.dius.pact.provider.junitsupport.State;
import com.example.provider.model.User;
import com.example.provider.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class UserServiceStateHandler {
private final UserRepository userRepository;
@State("user with ID 123 exists")
@Transactional
public void setupUserWithId123() {
log.info("Setting up state: user with ID 123 exists");
// Clear existing data
userRepository.deleteAll();
// Create test user
User user = User.builder()
.id("123")
.email("[email protected]")
.firstName("John")
.lastName("Doe")
.status(User.Status.ACTIVE)
.roles(List.of("ADMIN", "USER"))
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
userRepository.save(user);
log.info("Created test user with ID 123");
}
@State("no user with email [email protected] exists")
@Transactional
public void setupNoUserWithTestEmail() {
log.info("Setting up state: no user with email [email protected] exists");
// Delete any existing user with test email
userRepository.findByEmail("[email protected]")
.ifPresent(user -> userRepository.delete(user));
log.info("Ensured no user exists with email [email protected]");
}
@State("users exist in the system")
@Transactional
public void setupUsersExist() {
log.info("Setting up state: users exist in the system");
// Clear and create multiple users
userRepository.deleteAll();
List<User> users = List.of(
User.builder()
.id("1")
.email("[email protected]")
.firstName("Alice")
.lastName("Johnson")
.status(User.Status.ACTIVE)
.roles(List.of("USER"))
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build(),
User.builder()
.id("2")
.email("[email protected]")
.firstName("Bob")
.lastName("Smith")
.status(User.Status.ACTIVE)
.roles(List.of("USER"))
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build(),
User.builder()
.id("3")
.email("[email protected]")
.firstName("Carol")
.lastName("Williams")
.status(User.Status.INACTIVE)
.roles(List.of("USER"))
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build()
);
userRepository.saveAll(users);
log.info("Created {} test users", users.size());
}
@State("user with ID 999 does not exist")
@Transactional
public void setupUser999DoesNotExist() {
log.info("Setting up state: user with ID 999 does not exist");
// Ensure user 999 doesn't exist
userRepository.findById("999")
.ifPresent(user -> userRepository.delete(user));
log.info("Ensured user with ID 999 does not exist");
}
}

Step 5: Spring Boot Provider Implementation

user-service-provider/src/main/java/com/example/provider/UserServiceApplication.java:

package com.example.provider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}

user-service-provider/src/main/java/com/example/provider/controller/UserController.java:

package com.example.provider.controller;
import com.example.provider.model.User;
import com.example.provider.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(
@PathVariable String id,
@RequestHeader(value = "X-Request-ID", required = false) String requestId) {
log.info("Get user by ID: {}, Request-ID: {}", id, requestId);
return userService.findById(id)
.map(user -> ResponseEntity.ok()
.header("X-Rate-Limit-Remaining", "99")
.body(user))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(
@Valid @RequestBody CreateUserRequest request,
@RequestHeader(value = "X-Request-ID", required = false) String requestId) {
log.info("Create user: {}, Request-ID: {}", request.email(), requestId);
User user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED)
.header("Location", "/api/users/" + user.getId())
.body(user);
}
@GetMapping
public ResponseEntity<UserPageResponse> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
log.info("Get users - page: {}, size: {}", page, size);
Page<User> userPage = userService.findAll(PageRequest.of(page, size));
UserPageResponse response = UserPageResponse.builder()
.page(userPage.getNumber())
.size(userPage.getSize())
.totalPages(userPage.getTotalPages())
.totalElements((int) userPage.getTotalElements())
.content(userPage.getContent())
.build();
return ResponseEntity.ok(response);
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable String id,
@Valid @RequestBody UpdateUserRequest request) {
log.info("Update user: {}", id);
return userService.updateUser(id, request)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// Request/Response DTOs
public record CreateUserRequest(
String email,
String firstName,
String lastName,
String password
) {}
public record UpdateUserRequest(
String firstName,
String lastName
) {}
@lombok.Builder
public static class UserPageResponse {
private final int page;
private final int size;
private final int totalPages;
private final int totalElements;
private final List<User> content;
// Getters
public int getPage() { return page; }
public int getSize() { return size; }
public int getTotalPages() { return totalPages; }
public int getTotalElements() { return totalElements; }
public List<User> getContent() { return content; }
}
}

Step 6: Pact Broker Configuration

pact-broker/docker-compose.yml:

version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_USER: pactbroker
POSTGRES_PASSWORD: pactbroker
POSTGRES_DB: pactbroker
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pactbroker"]
interval: 10s
timeout: 5s
retries: 5
pact-broker:
image: pactfoundation/pact-broker:2.107.0
ports:
- "9292:9292"
environment:
PACT_BROKER_DATABASE_URL: "postgres://pactbroker:pactbroker@postgres:5432/pactbroker"
PACT_BROKER_DATABASE_ADAPTER: "postgres"
PACT_BROKER_PORT: "9292"
PACT_BROKER_LOG_LEVEL: "INFO"
PACT_BROKER_BASE_URL: "http://localhost:9292"
PACT_BROKER_WEBHOOK_SCHEME_WHITELIST: "http https"
PACT_BROKER_ENABLE_GETTING_STARTED_HTML: "true"
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped

Step 7: Build and Deployment Scripts

build-scripts/publish-pacts.sh:
```bash

!/bin/bash

set -e

Configuration

PACT_BROKER_HOST="${PACT_BROKER_HOST:-localhost}"
PACT_BROKER_PORT="${PACT_BROKER_PORT:-9292}"
PACT_BROKER_SCHEME="${PACT_BROKER_SCHEME:-http}"
PACT_BROKER_URL="${PACT_BROKER_SCHEME}://${PACT_BROKER_HOST}:${PACT_BROKER_PORT}"

Colors for output

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}

log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}

check_broker_health() {
log_info "Checking Pact Broker health…"

if curl -f -s "${PACT_BROKER_URL}/diagnostic/status/heartbeat" > /dev/null; then
log_info "Pact Broker is healthy"
return 0
else
log_error "Pact Broker is not accessible at ${PACT_BROKER_URL}"
return 1
fi

}

publish_consumer_pacts() {
local consumer_dir="$1"
local consumer_name="$2"
local version="$3"

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper