Spring Cloud Contract in Java: Complete Consumer-Driven Contracts Guide

Spring Cloud Contract enables Consumer-Driven Contracts (CDC) testing for microservices. This guide covers comprehensive implementation for both consumer and provider sides.


Setup and Dependencies

1. Provider Dependencies (Maven)
<!-- Provider POM -->
<project>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
</parent>
<properties>
<spring-cloud.version>2022.0.3</spring-cloud.version>
<spring-cloud-contract.version>4.0.3</spring-cloud-contract.version>
<maven-surefire-plugin.version>3.0.0</maven-surefire-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot Starters -->
<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>
<!-- Spring Cloud Contract -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<scope>test</scope>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- Spring Cloud Contract Maven Plugin -->
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<testFramework>JUNIT5</testFramework>
<baseClassForTests>com.company.provider.BaseContractTest</baseClassForTests>
<packageWithBaseClasses>com.company.provider</packageWithBaseClasses>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. Consumer Dependencies (Maven)
<!-- Consumer POM -->
<project>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
</parent>
<properties>
<spring-cloud.version>2022.0.3</spring-cloud.version>
<spring-cloud-contract.version>4.0.3</spring-cloud-contract.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Spring Cloud Contract Stub Runner -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
<!-- WebClient for HTTP calls -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

Provider Implementation

1. Provider Domain Models
package com.company.provider.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "users")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
@Column(unique = true, nullable = false)
private String email;
@NotBlank(message = "First name is required")
@Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")
private String firstName;
@NotBlank(message = "Last name is required")
@Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters")
private String lastName;
@Enumerated(EnumType.STRING)
private UserStatus status = UserStatus.ACTIVE;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<UserPreference> preferences;
// Constructors
public User() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public User(String email, String firstName, String lastName) {
this();
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long 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 UserStatus getStatus() { return status; }
public void setStatus(UserStatus status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public List<UserPreference> getPreferences() { return preferences; }
public void setPreferences(List<UserPreference> preferences) { this.preferences = preferences; }
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
public enum UserStatus {
ACTIVE, INACTIVE, SUSPENDED
}
}
@Entity
@Table(name = "user_preferences")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserPreference {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Preference key is required")
private String key;
@NotBlank(message = "Preference value is required")
private String value;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
// Constructors, Getters, and Setters
public UserPreference() {}
public UserPreference(String key, String value) {
this.key = key;
this.value = value;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
public User getUser() { return user; }
public void setUser(User user) { this.user = user; }
}
2. Provider DTOs
package com.company.provider.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.validation.constraints.*;
import java.time.LocalDateTime;
import java.util.List;
public class UserDto {
public static class CreateUserRequest {
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "First name is required")
@Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")
private String firstName;
@NotBlank(message = "Last name is required")
@Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters")
private String lastName;
private List<PreferenceRequest> preferences;
// Getters and Setters
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 List<PreferenceRequest> getPreferences() { return preferences; }
public void setPreferences(List<PreferenceRequest> preferences) { this.preferences = preferences; }
}
public static class UpdateUserRequest {
@Size(min = 2, max = 50, message = "First name must be between 2 and 50 characters")
private String firstName;
@Size(min = 2, max = 50, message = "Last name must be between 2 and 50 characters")
private String lastName;
// Getters and Setters
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 static class UserResponse {
private Long id;
private String email;
private String firstName;
private String lastName;
private String status;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime updatedAt;
private List<PreferenceResponse> preferences;
// Constructors
public UserResponse() {}
public UserResponse(Long id, String email, String firstName, String lastName, 
String status, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
this.status = status;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long 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 LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public List<PreferenceResponse> getPreferences() { return preferences; }
public void setPreferences(List<PreferenceResponse> preferences) { this.preferences = preferences; }
}
public static class PreferenceRequest {
@NotBlank(message = "Preference key is required")
private String key;
@NotBlank(message = "Preference value is required")
private String value;
// Getters and Setters
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}
public static class PreferenceResponse {
private Long id;
private String key;
private String value;
// Constructors
public PreferenceResponse() {}
public PreferenceResponse(Long id, String key, String value) {
this.id = id;
this.key = key;
this.value = value;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}
public static class ErrorResponse {
private String timestamp;
private int status;
private String error;
private String message;
private String path;
// Constructors
public ErrorResponse() {
this.timestamp = LocalDateTime.now().toString();
}
public ErrorResponse(int status, String error, String message, String path) {
this();
this.status = status;
this.error = error;
this.message = message;
this.path = path;
}
// Getters and Setters
public String getTimestamp() { return timestamp; }
public void setTimestamp(String timestamp) { this.timestamp = timestamp; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
}
}
3. Provider Controller
package com.company.provider.controller;
import com.company.provider.dto.UserDto;
import com.company.provider.model.User;
import com.company.provider.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<UserDto.UserResponse> createUser(
@Valid @RequestBody UserDto.CreateUserRequest request) {
UserDto.UserResponse user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
@GetMapping("/{id}")
public ResponseEntity<UserDto.UserResponse> getUser(@PathVariable Long id) {
UserDto.UserResponse user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@GetMapping
public ResponseEntity<Page<UserDto.UserResponse>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt") String sort,
@RequestParam(defaultValue = "desc") String direction) {
Sort.Direction sortDirection = "desc".equalsIgnoreCase(direction) 
? Sort.Direction.DESC : Sort.Direction.ASC;
Pageable pageable = PageRequest.of(page, size, Sort.by(sortDirection, sort));
Page<UserDto.UserResponse> users = userService.getUsers(pageable);
return ResponseEntity.ok(users);
}
@GetMapping("/search")
public ResponseEntity<List<UserDto.UserResponse>> searchUsers(
@RequestParam String email) {
List<UserDto.UserResponse> users = userService.searchUsersByEmail(email);
return ResponseEntity.ok(users);
}
@PutMapping("/{id}")
public ResponseEntity<UserDto.UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserDto.UpdateUserRequest request) {
UserDto.UserResponse user = userService.updateUser(id, request);
return ResponseEntity.ok(user);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
@PatchMapping("/{id}/deactivate")
public ResponseEntity<UserDto.UserResponse> deactivateUser(@PathVariable Long id) {
UserDto.UserResponse user = userService.deactivateUser(id);
return ResponseEntity.ok(user);
}
@PatchMapping("/{id}/activate")
public ResponseEntity<UserDto.UserResponse> activateUser(@PathVariable Long id) {
UserDto.UserResponse user = userService.activateUser(id);
return ResponseEntity.ok(user);
}
@GetMapping("/{id}/preferences")
public ResponseEntity<List<UserDto.PreferenceResponse>> getUserPreferences(@PathVariable Long id) {
List<UserDto.PreferenceResponse> preferences = userService.getUserPreferences(id);
return ResponseEntity.ok(preferences);
}
@PostMapping("/{id}/preferences")
public ResponseEntity<UserDto.PreferenceResponse> addUserPreference(
@PathVariable Long id,
@Valid @RequestBody UserDto.PreferenceRequest request) {
UserDto.PreferenceResponse preference = userService.addUserPreference(id, request);
return ResponseEntity.status(HttpStatus.CREATED).body(preference);
}
}
4. Provider Service
package com.company.provider.service;
import com.company.provider.dto.UserDto;
import com.company.provider.model.User;
import com.company.provider.model.UserPreference;
import com.company.provider.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public UserDto.UserResponse createUser(UserDto.CreateUserRequest request) {
// Check if user already exists
if (userRepository.existsByEmail(request.getEmail())) {
throw new UserAlreadyExistsException("User with email " + request.getEmail() + " already exists");
}
// Create user
User user = new User(request.getEmail(), request.getFirstName(), request.getLastName());
// Add preferences if provided
if (request.getPreferences() != null) {
List<UserPreference> preferences = request.getPreferences().stream()
.map(pref -> new UserPreference(pref.getKey(), pref.getValue()))
.collect(Collectors.toList());
preferences.forEach(pref -> pref.setUser(user));
user.setPreferences(preferences);
}
User savedUser = userRepository.save(user);
return mapToUserResponse(savedUser);
}
public UserDto.UserResponse getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
return mapToUserResponse(user);
}
public Page<UserDto.UserResponse> getUsers(Pageable pageable) {
return userRepository.findAll(pageable)
.map(this::mapToUserResponse);
}
public List<UserDto.UserResponse> searchUsersByEmail(String email) {
return userRepository.findByEmailContainingIgnoreCase(email).stream()
.map(this::mapToUserResponse)
.collect(Collectors.toList());
}
public UserDto.UserResponse updateUser(Long id, UserDto.UpdateUserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
if (request.getFirstName() != null) {
user.setFirstName(request.getFirstName());
}
if (request.getLastName() != null) {
user.setLastName(request.getLastName());
}
User updatedUser = userRepository.save(user);
return mapToUserResponse(updatedUser);
}
public void deleteUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
userRepository.delete(user);
}
public UserDto.UserResponse deactivateUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
user.setStatus(User.UserStatus.INACTIVE);
User updatedUser = userRepository.save(user);
return mapToUserResponse(updatedUser);
}
public UserDto.UserResponse activateUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
user.setStatus(User.UserStatus.ACTIVE);
User updatedUser = userRepository.save(user);
return mapToUserResponse(updatedUser);
}
public List<UserDto.PreferenceResponse> getUserPreferences(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId));
return user.getPreferences().stream()
.map(this::mapToPreferenceResponse)
.collect(Collectors.toList());
}
public UserDto.PreferenceResponse addUserPreference(Long userId, UserDto.PreferenceRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId));
UserPreference preference = new UserPreference(request.getKey(), request.getValue());
preference.setUser(user);
user.getPreferences().add(preference);
UserPreference savedPreference = userRepository.save(user).getPreferences()
.stream()
.filter(p -> p.getKey().equals(request.getKey()))
.findFirst()
.orElseThrow(() -> new RuntimeException("Failed to save preference"));
return mapToPreferenceResponse(savedPreference);
}
private UserDto.UserResponse mapToUserResponse(User user) {
UserDto.UserResponse response = new UserDto.UserResponse(
user.getId(),
user.getEmail(),
user.getFirstName(),
user.getLastName(),
user.getStatus().name(),
user.getCreatedAt(),
user.getUpdatedAt()
);
if (user.getPreferences() != null) {
List<UserDto.PreferenceResponse> preferences = user.getPreferences().stream()
.map(this::mapToPreferenceResponse)
.collect(Collectors.toList());
response.setPreferences(preferences);
}
return response;
}
private UserDto.PreferenceResponse mapToPreferenceResponse(UserPreference preference) {
return new UserDto.PreferenceResponse(
preference.getId(),
preference.getKey(),
preference.getValue()
);
}
// Custom Exceptions
public static class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
public static class UserAlreadyExistsException extends RuntimeException {
public UserAlreadyExistsException(String message) {
super(message);
}
}
}

Contract Definitions

1. Contract Files Structure
src/test/resources/contracts/
├── users/
│   ├── shouldCreateUser.groovy
│   ├── shouldReturnUser.groovy
│   ├── shouldReturnUserNotFound.groovy
│   ├── shouldReturnAllUsers.groovy
│   ├── shouldUpdateUser.groovy
│   ├── shouldDeleteUser.groovy
│   └── shouldDeactivateUser.groovy
└── preferences/
├── shouldGetUserPreferences.groovy
└── shouldAddUserPreference.groovy
2. User Contracts
// shouldCreateUser.groovy
package contracts.users
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Should create a new user"
name "create_user"
label "create_user_success"
request {
method POST()
url "/api/v1/users"
headers {
contentType(applicationJson())
}
body([
email: "[email protected]",
firstName: "John",
lastName: "Doe",
preferences: [
[
key: "theme",
value: "dark"
],
[
key: "language",
value: "en"
]
]
])
}
response {
status CREATED()
headers {
contentType(applicationJson())
}
body([
id: 1,
email: "[email protected]",
firstName: "John",
lastName: "Doe",
status: "ACTIVE",
createdAt: $(consumer("2023-01-01T10:00:00"), producer(regex(iso8601WithOffset()))),
updatedAt: $(consumer("2023-01-01T10:00:00"), producer(regex(iso8601WithOffset()))),
preferences: [
[
id: 1,
key: "theme",
value: "dark"
],
[
id: 2,
key: "language",
value: "en"
]
]
])
}
}
// shouldReturnUser.groovy
package contracts.users
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Should return user by ID"
name "get_user"
label "get_user_success"
request {
method GET()
url $(consumer("/api/v1/users/1"), producer(regex('/api/v1/users/[0-9]+')))
}
response {
status OK()
headers {
contentType(applicationJson())
}
body([
id: 1,
email: "[email protected]",
firstName: "John",
lastName: "Doe",
status: "ACTIVE",
createdAt: $(consumer("2023-01-01T10:00:00"), producer(regex(iso8601WithOffset()))),
updatedAt: $(consumer("2023-01-01T10:00:00"), producer(regex(iso8601WithOffset())))
])
}
}
// shouldReturnUserNotFound.groovy
package contracts.users
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Should return 404 when user not found"
name "get_user_not_found"
label "get_user_not_found"
request {
method GET()
url $(consumer("/api/v1/users/999"), producer(regex('/api/v1/users/[0-9]+')))
}
response {
status NOT_FOUND()
headers {
contentType(applicationJson())
}
body([
timestamp: $(consumer("2023-01-01T10:00:00"), producer(regex(iso8601WithOffset()))),
status: 404,
error: "Not Found",
message: "User not found with id: 999",
path: $(consumer("/api/v1/users/999"), producer(regex('/api/v1/users/[0-9]+')))
])
}
}
// shouldReturnAllUsers.groovy
package contracts.users
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Should return paginated users"
name "get_users"
label "get_users_success"
request {
method GET()
url "/api/v1/users"
queryParameters {
parameter "page": "0"
parameter "size": "2"
parameter "sort": "createdAt"
parameter "direction": "desc"
}
}
response {
status OK()
headers {
contentType(applicationJson())
}
body([
content: [
[
id: 1,
email: "[email protected]",
firstName: "John",
lastName: "Doe",
status: "ACTIVE",
createdAt: $(consumer("2023-01-01T10:00:00"), producer(regex(iso8601WithOffset()))),
updatedAt: $(consumer("2023-01-01T10:00:00"), producer(regex(iso8601WithOffset())))
],
[
id: 2,
email: "[email protected]",
firstName: "Jane",
lastName: "Smith",
status: "ACTIVE",
createdAt: $(consumer("2023-01-01T09:00:00"), producer(regex(iso8601WithOffset()))),
updatedAt: $(consumer("2023-01-01T09:00:00"), producer(regex(iso8601WithOffset())))
]
],
totalElements: 2,
totalPages: 1,
size: 2,
number: 0,
first: true,
last: true,
numberOfElements: 2,
empty: false
])
bodyMatchers {
jsonPath('$.content', byType {
minOccurrence(1)
})
}
}
}
// shouldUpdateUser.groovy
package contracts.users
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Should update user"
name "update_user"
label "update_user_success"
request {
method PUT()
url $(consumer("/api/v1/users/1"), producer(regex('/api/v1/users/[0-9]+')))
headers {
contentType(applicationJson())
}
body([
firstName: "Johnny",
lastName: "Doe Jr."
])
}
response {
status OK()
headers {
contentType(applicationJson())
}
body([
id: 1,
email: "[email protected]",
firstName: "Johnny",
lastName: "Doe Jr.",
status: "ACTIVE",
createdAt: $(consumer("2023-01-01T10:00:00"), producer(regex(iso8601WithOffset()))),
updatedAt: $(consumer("2023-01-01T11:00:00"), producer(regex(iso8601WithOffset())))
])
}
}
3. Preference Contracts
// shouldGetUserPreferences.groovy
package contracts.preferences
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Should return user preferences"
name "get_user_preferences"
label "get_user_preferences_success"
request {
method GET()
url $(consumer("/api/v1/users/1/preferences"), producer(regex('/api/v1/users/[0-9]+/preferences')))
}
response {
status OK()
headers {
contentType(applicationJson())
}
body([
[
id: 1,
key: "theme",
value: "dark"
],
[
id: 2,
key: "language",
value: "en"
]
])
}
}
// shouldAddUserPreference.groovy
package contracts.preferences
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "Should add user preference"
name "add_user_preference"
label "add_user_preference_success"
request {
method POST()
url $(consumer("/api/v1/users/1/preferences"), producer(regex('/api/v1/users/[0-9]+/preferences')))
headers {
contentType(applicationJson())
}
body([
key: "notifications",
value: "enabled"
])
}
response {
status CREATED()
headers {
contentType(applicationJson())
}
body([
id: 3,
key: "notifications",
value: "enabled"
])
}
}

Provider Test Base Class

package com.company.provider;
import com.company.provider.model.User;
import com.company.provider.model.UserPreference;
import com.company.provider.repository.UserRepository;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.verifier.messaging.boot.AutoConfigureMessageVerifier;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.context.WebApplicationContext;
import java.time.LocalDateTime;
import java.util.Arrays;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMessageVerifier
@DirtiesContext
@ActiveProfiles("test")
public abstract class BaseContractTest {
@Autowired
private WebApplicationContext context;
@Autowired
private UserRepository userRepository;
@BeforeEach
public void setup() {
RestAssuredMockMvc.webAppContextSetup(context);
setupTestData();
}
private void setupTestData() {
userRepository.deleteAll();
// Create test users
User user1 = new User("[email protected]", "John", "Doe");
user1.setId(1L);
user1.setCreatedAt(LocalDateTime.of(2023, 1, 1, 10, 0, 0));
user1.setUpdatedAt(LocalDateTime.of(2023, 1, 1, 10, 0, 0));
UserPreference pref1 = new UserPreference("theme", "dark");
pref1.setId(1L);
pref1.setUser(user1);
UserPreference pref2 = new UserPreference("language", "en");
pref2.setId(2L);
pref2.setUser(user1);
user1.setPreferences(Arrays.asList(pref1, pref2));
userRepository.save(user1);
User user2 = new User("[email protected]", "Jane", "Smith");
user2.setId(2L);
user2.setCreatedAt(LocalDateTime.of(2023, 1, 1, 9, 0, 0));
user2.setUpdatedAt(LocalDateTime.of(2023, 1, 1, 9, 0, 0));
userRepository.save(user2);
}
}

Consumer Implementation

1. Consumer Client
package com.company.consumer.client;
import com.company.consumer.config.UserServiceClientConfig;
import com.company.consumer.dto.UserResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@FeignClient(
name = "user-service",
url = "${user.service.url:http://localhost:8080}",
configuration = UserServiceClientConfig.class
)
public interface UserServiceClient {
@PostMapping("/api/v1/users")
UserResponse createUser(@RequestBody CreateUserRequest request);
@GetMapping("/api/v1/users/{id}")
UserResponse getUser(@PathVariable("id") Long id);
@GetMapping("/api/v1/users")
Page<UserResponse> getUsers(
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "20") int size,
@RequestParam(value = "sort", defaultValue = "createdAt") String sort,
@RequestParam(value = "direction", defaultValue = "desc") String direction);
@GetMapping("/api/v1/users/search")
List<UserResponse> searchUsers(@RequestParam("email") String email);
@PutMapping("/api/v1/users/{id}")
UserResponse updateUser(@PathVariable("id") Long id, @RequestBody UpdateUserRequest request);
@DeleteMapping("/api/v1/users/{id}")
void deleteUser(@PathVariable("id") Long id);
@PatchMapping("/api/v1/users/{id}/deactivate")
UserResponse deactivateUser(@PathVariable("id") Long id);
@PatchMapping("/api/v1/users/{id}/activate")
UserResponse activateUser(@PathVariable("id") Long id);
@GetMapping("/api/v1/users/{id}/preferences")
List<PreferenceResponse> getUserPreferences(@PathVariable("id") Long id);
@PostMapping("/api/v1/users/{id}/preferences")
PreferenceResponse addUserPreference(@PathVariable("id") Long id, @RequestBody PreferenceRequest request);
// Request/Response DTOs
class CreateUserRequest {
private String email;
private String firstName;
private String lastName;
private List<PreferenceRequest> preferences;
// Constructors
public CreateUserRequest() {}
public CreateUserRequest(String email, String firstName, String lastName) {
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}
// Getters and Setters
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 List<PreferenceRequest> getPreferences() { return preferences; }
public void setPreferences(List<PreferenceRequest> preferences) { this.preferences = preferences; }
}
class UpdateUserRequest {
private String firstName;
private String lastName;
// Getters and Setters
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; }
}
class UserResponse {
private Long id;
private String email;
private String firstName;
private String lastName;
private String status;
private String createdAt;
private String updatedAt;
private List<PreferenceResponse> preferences;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long 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<PreferenceResponse> getPreferences() { return preferences; }
public void setPreferences(List<PreferenceResponse> preferences) { this.preferences = preferences; }
}
class PreferenceRequest {
private String key;
private String value;
// Constructors
public PreferenceRequest() {}
public PreferenceRequest(String key, String value) {
this.key = key;
this.value = value;
}
// Getters and Setters
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}
class PreferenceResponse {
private Long id;
private String key;
private String value;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
}
}
2. Consumer Client Configuration
package com.company.consumer.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import feign.Logger;
import feign.RequestInterceptor;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UserServiceClientConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public RequestInterceptor requestInterceptor() {
return template -> {
template.header("Content-Type", "application/json");
template.header("Accept", "application/json");
};
}
@Bean
public ErrorDecoder errorDecoder() {
return new UserServiceErrorDecoder();
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
3. Consumer Error Handling
package com.company.consumer.config;
import com.company.consumer.exception.UserServiceException;
import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.http.HttpStatus;
import java.io.IOException;
import java.io.InputStream;
public class UserServiceErrorDecoder implements ErrorDecoder {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Exception decode(String methodKey, Response response) {
try {
InputStream responseBody = response.body().asInputStream();
ErrorResponse errorResponse = objectMapper.readValue(responseBody, ErrorResponse.class);
return new UserServiceException(
errorResponse.getMessage(),
HttpStatus.valueOf(response.status())
);
} catch (IOException e) {
return new UserServiceException(
"Error communicating with user service",
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
static class ErrorResponse {
private String timestamp;
private int status;
private String error;
private String message;
private String path;
// Getters and Setters
public String getTimestamp() { return timestamp; }
public void setTimestamp(String timestamp) { this.timestamp = timestamp; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
}
}
package com.company.consumer.exception;
import org.springframework.http.HttpStatus;
public class UserServiceException extends RuntimeException {
private final HttpStatus status;
public UserServiceException(String message, HttpStatus status) {
super(message);
this.status = status;
}
public HttpStatus getStatus() {
return status;
}
}
4. Consumer Service
package com.company.consumer.service;
import com.company.consumer.client.UserServiceClient;
import com.company.consumer.exception.UserServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserServiceClient userServiceClient;
@Autowired
public UserService(UserServiceClient userServiceClient) {
this.userServiceClient = userServiceClient;
}
public UserServiceClient.UserResponse createUser(UserServiceClient.CreateUserRequest request) {
try {
return userServiceClient.createUser(request);
} catch (Exception e) {
throw new UserServiceException("Failed to create user: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
public UserServiceClient.UserResponse getUser(Long id) {
try {
return userServiceClient.getUser(id);
} catch (UserServiceException e) {
if (e.getStatus() == HttpStatus.NOT_FOUND) {
throw new UserServiceException("User not found with id: " + id, HttpStatus.NOT_FOUND);
}
throw e;
}
}
public Page<UserServiceClient.UserResponse> getUsers(int page, int size, String sort, String direction) {
try {
return userServiceClient.getUsers(page, size, sort, direction);
} catch (Exception e) {
throw new UserServiceException("Failed to get users: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
public List<UserServiceClient.UserResponse> searchUsers(String email) {
try {
return userServiceClient.searchUsers(email);
} catch (Exception e) {
throw new UserServiceException("Failed to search users: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
public UserServiceClient.UserResponse updateUser(Long id, UserServiceClient.UpdateUserRequest request) {
try {
return userServiceClient.updateUser(id, request);
} catch (UserServiceException e) {
if (e.getStatus() == HttpStatus.NOT_FOUND) {
throw new UserServiceException("User not found with id: " + id, HttpStatus.NOT_FOUND);
}
throw e;
}
}
public void deleteUser(Long id) {
try {
userServiceClient.deleteUser(id);
} catch (UserServiceException e) {
if (e.getStatus() == HttpStatus.NOT_FOUND) {
throw new UserServiceException("User not found with id: " + id, HttpStatus.NOT_FOUND);
}
throw e;
}
}
public UserServiceClient.UserResponse deactivateUser(Long id) {
try {
return userServiceClient.deactivateUser(id);
} catch (UserServiceException e) {
if (e.getStatus() == HttpStatus.NOT_FOUND) {
throw new UserServiceException("User not found with id: " + id, HttpStatus.NOT_FOUND);
}
throw e;
}
}
public UserServiceClient.UserResponse activateUser(Long id) {
try {
return userServiceClient.activateUser(id);
} catch (UserServiceException e) {
if (e.getStatus() == HttpStatus.NOT_FOUND) {
throw new UserServiceException("User not found with id: " + id, HttpStatus.NOT_FOUND);
}
throw e;
}
}
public List<UserServiceClient.PreferenceResponse> getUserPreferences(Long userId) {
try {
return userServiceClient.getUserPreferences(userId);
} catch (UserServiceException e) {
if (e.getStatus() == HttpStatus.NOT_FOUND) {
throw new UserServiceException("User not found with id: " + userId, HttpStatus.NOT_FOUND);
}
throw e;
}
}
public UserServiceClient.PreferenceResponse addUserPreference(Long userId, UserServiceClient.PreferenceRequest request) {
try {
return userServiceClient.addUserPreference(userId, request);
} catch (UserServiceException e) {
if (e.getStatus() == HttpStatus.NOT_FOUND) {
throw new UserServiceException("User not found with id: " + userId, HttpStatus.NOT_FOUND);
}
throw e;
}
}
}

Consumer Contract Tests

1. Consumer Base Test Class
package com.company.consumer.contract;
import com.company.consumer.client.UserServiceClient;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
@AutoConfigureStubRunner(
ids = "com.company.provider:user-service:+:stubs:8080",
stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
@ActiveProfiles("test")
public abstract class BaseContractTest {
@Autowired
protected UserServiceClient userServiceClient;
@BeforeEach
void setUp() {
// Common setup for all contract tests
}
}
2. Consumer Contract Tests
package com.company.consumer.contract;
import com.company.consumer.client.UserServiceClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceContractTest extends BaseContractTest {
@Autowired
private UserServiceClient userServiceClient;
@Test
void shouldCreateUser() {
// Given
UserServiceClient.CreateUserRequest request = new UserServiceClient.CreateUserRequest(
"[email protected]", "John", "Doe"
);
// When
UserServiceClient.UserResponse response = userServiceClient.createUser(request);
// Then
assertNotNull(response);
assertThat(response.getId()).isEqualTo(1L);
assertThat(response.getEmail()).isEqualTo("[email protected]");
assertThat(response.getFirstName()).isEqualTo("John");
assertThat(response.getLastName()).isEqualTo("Doe");
assertThat(response.getStatus()).isEqualTo("ACTIVE");
assertThat(response.getPreferences()).hasSize(2);
}
@Test
void shouldGetUser() {
// When
UserServiceClient.UserResponse response = userServiceClient.getUser(1L);
// Then
assertNotNull(response);
assertThat(response.getId()).isEqualTo(1L);
assertThat(response.getEmail()).isEqualTo("[email protected]");
assertThat(response.getFirstName()).isEqualTo("John");
assertThat(response.getLastName()).isEqualTo("Doe");
assertThat(response.getStatus()).isEqualTo("ACTIVE");
}
@Test
void shouldGetAllUsers() {
// When
Page<UserServiceClient.UserResponse> response = userServiceClient.getUsers(0, 2, "createdAt", "desc");
// Then
assertNotNull(response);
assertThat(response.getContent()).hasSize(2);
assertThat(response.getTotalElements()).isEqualTo(2);
assertThat(response.getTotalPages()).isEqualTo(1);
UserServiceClient.UserResponse firstUser = response.getContent().get(0);
assertThat(firstUser.getId()).isEqualTo(1L);
assertThat(firstUser.getEmail()).isEqualTo("[email protected]");
}
@Test
void shouldUpdateUser() {
// Given
UserServiceClient.UpdateUserRequest request = new UserServiceClient.UpdateUserRequest();
request.setFirstName("Johnny");
request.setLastName("Doe Jr.");
// When
UserServiceClient.UserResponse response = userServiceClient.updateUser(1L, request);
// Then
assertNotNull(response);
assertThat(response.getId()).isEqualTo(1L);
assertThat(response.getFirstName()).isEqualTo("Johnny");
assertThat(response.getLastName()).isEqualTo("Doe Jr.");
}
@Test
void shouldDeleteUser() {
// When & Then - Should not throw exception
assertDoesNotThrow(() -> userServiceClient.deleteUser(1L));
}
@Test
void shouldDeactivateUser() {
// When
UserServiceClient.UserResponse response = userServiceClient.deactivateUser(1L);
// Then
assertNotNull(response);
assertThat(response.getId()).isEqualTo(1L);
assertThat(response.getStatus()).isEqualTo("INACTIVE");
}
@Test
void shouldGetUserPreferences() {
// When
List<UserServiceClient.PreferenceResponse> preferences = userServiceClient.getUserPreferences(1L);
// Then
assertNotNull(preferences);
assertThat(preferences).hasSize(2);
UserServiceClient.PreferenceResponse firstPreference = preferences.get(0);
assertThat(firstPreference.getId()).isEqualTo(1L);
assertThat(firstPreference.getKey()).isEqualTo("theme");
assertThat(firstPreference.getValue()).isEqualTo("dark");
}
@Test
void shouldAddUserPreference() {
// Given
UserServiceClient.PreferenceRequest request = new UserServiceClient.PreferenceRequest("notifications", "enabled");
// When
UserServiceClient.PreferenceResponse response = userServiceClient.addUserPreference(1L, request);
// Then
assertNotNull(response);
assertThat(response.getId()).isEqualTo(3L);
assertThat(response.getKey()).isEqualTo("notifications");
assertThat(response.getValue()).isEqualTo("enabled");
}
}

Build and Deployment Configuration

1. Provider Maven Configuration
<build>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<testFramework>JUNIT5</testFramework>
<baseClassForTests>com.company.provider.BaseContractTest</baseClassForTests>
<packageWithBaseClasses>com.company.provider</packageWithBaseClasses>
<basePackageForTests>com.company.provider.contract</basePackageForTests>
</configuration>
<executions>
<execution>
<goals>
<goal>convert</goal>
<goal>generateTests</goal>
<goal>generateStubs</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<includes>
<include>**/*Test.java</include>
<include>**/*ContractTest.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
2. Stub Repository Configuration
# application.yml for Provider
spring:
application:
name: user-service
cloud:
contract:
verifier:
# Where the generated tests will be created
generated-test-sources-directory: src/test/java/contracts
# Where the contracts are stored
contractsDslDir: src/test/resources/contracts
# Base package for generated tests
basePackageForTests: com.company.provider.contract
# Stub repository configuration
repository:
cache:
download:
stubs-root: http://nexus.company.com/repository/maven-releases
stubrunner:
# Where to publish stubs
stubs-mode: remote
# Repository configuration for stubs
repository-root: http://nexus.company.com/repository/maven-releases
ids:
- com.company.provider:user-service:+:stubs:8080
# Test profile
---
spring:
profiles: test
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password: 
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
show-sql: true
h2:
console:
enabled: true
3. CI/CD Pipeline Script
// Jenkinsfile for Provider
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn clean compile'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
}
stage('Contract Tests') {
steps {
sh 'mvn spring-cloud-contract:generateTests'
sh 'mvn test -Dtest=**/*ContractTest'
}
}
stage('Generate Stubs') {
steps {
sh 'mvn spring-cloud-contract:generateStubs'
}
}
stage('Publish Stubs') {
steps {
sh 'mvn deploy -DskipTests'
}
}
stage('Integration Tests') {
steps {
sh 'mvn verify -Pintegration'
}
}
}
post {
always {
junit '**/target/surefire-reports/*.xml'
}
}
}

Summary

This Spring Cloud Contract implementation provides:

  1. Consumer-Driven Contracts: Consumers define expected API behavior
  2. Provider Verification: Automated testing against contract specifications
  3. Stub Generation: Realistic API stubs for consumer testing
  4. API Evolution: Safe API changes with contract compatibility

Key Benefits:

  • Reduced Integration Issues: Catch breaking changes early
  • Faster Development: Parallel development with confidence
  • Better Collaboration: Clear API expectations between teams
  • Automated Testing: Contract validation in CI/CD pipelines
  • Documentation: Contracts serve as living API documentation

This comprehensive setup ensures that microservices can evolve independently while maintaining compatibility through automated contract testing.

Leave a Reply

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


Macro Nepal Helper