Mocker: Service Virtualization with Spring Boot for Mocking REST, SOAP, and TCP Services
Learn how to build a service virtualization platform using Spring Boot. Record and replay service interactions, mock unavailable dependencies, and enable parallel development with dynamic response templating.
Table of Contents
Introduction
Modern software development often involves integrating with numerous external services - payment gateways, third-party APIs, legacy systems, and partner services. But what happens when:
- The external service is not yet developed?
- The service is unavailable due to maintenance?
- You need to test edge cases that are hard to reproduce?
- Rate limits or costs prevent unlimited testing?
Service Virtualization addresses these challenges by creating simulated versions of dependent services. The Mocker project provides a comprehensive service virtualization platform using Spring Boot, supporting REST, SOAP, and TCP protocols with intelligent request-response matching.
Key Insight: Service virtualization enables parallel development, consistent testing environments, and eliminates dependencies on external service availability - essential for modern CI/CD pipelines.
Microservices Architecture
What is Service Virtualization?
Service Virtualization simulates the behavior of dependent services, enabling:
| Capability | Benefit |
|---|---|
| Record & Replay | Capture real interactions and replay them |
| Parallel Development | Frontend teams work while backend is in progress |
| Consistent Testing | Reproducible test scenarios every time |
| Edge Case Testing | Simulate errors, timeouts, and unusual responses |
| Cost Reduction | Avoid expensive third-party API calls during testing |
How Mocker Works
+------------------+ +------------------+ +------------------+
| Your | | | | Target |
| Application |---->| Mocker |---->| Service |
| | | | | (Optional) |
+------------------+ +------------------+ +------------------+
|
| Hash-based
| Lookup
v
+------------------+
| In-Memory |
| Mock Database |
+------------------+
Architecture Overview
Mocker implements a smart matching strategy:
- First Request: If no mock exists, forwards to target service and records response
- Subsequent Requests: Returns cached response based on hash matching
- Manual Mocks: Pre-configured responses for specific scenarios
Microservices Architecture
Hash-Based Matching
The matching key is computed from:
- URL Path
- HTTP Method
- Request Body (normalized)
// Simplified hash computation
String hashKey = computeHash(
request.getMethod(),
request.getPath(),
normalizeBody(request.getBody())
);
Project Setup
Prerequisites
- JDK 8 or higher
- Maven 3.5.0+
- Lombok IDE configuration
- jpa-eclipselink Spring Boot auto-configuration
Maven Dependencies
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Data persistence -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Groovy for dynamic templates -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.9</version>
</dependency>
<!-- Swagger Documentation -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
Project Structure
Mocker/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── gonnect/
│ │ └── mocker/
│ │ ├── MockerApplication.java
│ │ ├── controller/
│ │ │ ├── MockController.java
│ │ │ └── AdminController.java
│ │ ├── service/
│ │ │ ├── MockService.java
│ │ │ └── TemplateService.java
│ │ ├── repository/
│ │ │ └── MockRepository.java
│ │ ├── model/
│ │ │ └── MockDefinition.java
│ │ └── template/
│ │ ├── SimpleResponseTemplate.java
│ │ └── SmartResponseTemplate.java
│ ├── groovy/
│ │ └── templates/
│ │ └── DynamicResponseTemplate.groovy
│ └── resources/
│ └── application.yml
└── pom.xml
Core Implementation
Mock Definition Model
package com.gonnect.mocker.model;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
@Table(name = "mock_definitions")
public class MockDefinition {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String method;
@Column(nullable = false)
private String path;
@Column(nullable = false)
private String requestHash;
@Lob
private String requestBody;
@Lob
@Column(nullable = false)
private String responseBody;
private int responseStatus = 200;
private String contentType = "application/json";
private long delayMs = 0;
@Lob
private String templateClass;
private boolean active = true;
@Column(updatable = false)
private java.time.LocalDateTime createdAt;
private java.time.LocalDateTime lastAccessedAt;
private long accessCount = 0;
@PrePersist
protected void onCreate() {
createdAt = java.time.LocalDateTime.now();
}
}
Mock Repository
package com.gonnect.mocker.repository;
import com.gonnect.mocker.model.MockDefinition;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface MockRepository extends JpaRepository<MockDefinition, Long> {
Optional<MockDefinition> findByRequestHashAndActiveTrue(String requestHash);
List<MockDefinition> findByPathContaining(String path);
List<MockDefinition> findByActiveTrue();
@Query("SELECT m FROM MockDefinition m ORDER BY m.accessCount DESC")
List<MockDefinition> findMostAccessed();
}
Mock Service
package com.gonnect.mocker.service;
import com.gonnect.mocker.model.MockDefinition;
import com.gonnect.mocker.repository.MockRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class MockService {
private final MockRepository mockRepository;
private final TemplateService templateService;
private final RestTemplate restTemplate;
@Value("${mocker.target.url:}")
private String targetUrl;
@Value("${mocker.record.enabled:true}")
private boolean recordEnabled;
/**
* Process a mock request - return cached response or record new one
*/
public ResponseEntity<String> processRequest(
String method,
String path,
String body,
HttpHeaders headers) {
String requestHash = computeHash(method, path, body);
log.debug("Processing request: {} {} [hash={}]", method, path, requestHash);
// Check for existing mock
Optional<MockDefinition> existingMock =
mockRepository.findByRequestHashAndActiveTrue(requestHash);
if (existingMock.isPresent()) {
return serveMock(existingMock.get(), body);
}
// No mock found - forward to target and record if enabled
if (recordEnabled && !targetUrl.isEmpty()) {
return forwardAndRecord(method, path, body, headers, requestHash);
}
// Return 404 if no mock and no target configured
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body("{\"error\": \"No mock found for this request\"}");
}
private ResponseEntity<String> serveMock(MockDefinition mock, String requestBody) {
log.info("Serving mock: {} [accessCount={}]",
mock.getName(), mock.getAccessCount());
// Update access statistics
mock.setLastAccessedAt(LocalDateTime.now());
mock.setAccessCount(mock.getAccessCount() + 1);
mockRepository.save(mock);
// Apply response template if configured
String responseBody = mock.getResponseBody();
if (mock.getTemplateClass() != null && !mock.getTemplateClass().isEmpty()) {
responseBody = templateService.applyTemplate(
mock.getTemplateClass(),
requestBody,
responseBody);
}
// Apply delay if configured
if (mock.getDelayMs() > 0) {
try {
Thread.sleep(mock.getDelayMs());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return ResponseEntity
.status(mock.getResponseStatus())
.contentType(MediaType.parseMediaType(mock.getContentType()))
.body(responseBody);
}
private ResponseEntity<String> forwardAndRecord(
String method,
String path,
String body,
HttpHeaders headers,
String requestHash) {
try {
String fullUrl = targetUrl + path;
log.info("Forwarding to target: {} {}", method, fullUrl);
HttpEntity<String> entity = new HttpEntity<>(body, headers);
ResponseEntity<String> response = restTemplate.exchange(
fullUrl,
HttpMethod.valueOf(method),
entity,
String.class);
// Record the response
MockDefinition mock = new MockDefinition();
mock.setName("Auto-recorded: " + method + " " + path);
mock.setMethod(method);
mock.setPath(path);
mock.setRequestHash(requestHash);
mock.setRequestBody(body);
mock.setResponseBody(response.getBody());
mock.setResponseStatus(response.getStatusCodeValue());
mock.setContentType(
response.getHeaders().getContentType() != null
? response.getHeaders().getContentType().toString()
: "application/json");
mockRepository.save(mock);
log.info("Recorded mock: {}", mock.getName());
return response;
} catch (Exception e) {
log.error("Error forwarding request", e);
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
.body("{\"error\": \"" + e.getMessage() + "\"}");
}
}
private String computeHash(String method, String path, String body) {
try {
String input = method + "|" + path + "|" + normalizeBody(body);
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
} catch (Exception e) {
throw new RuntimeException("Hash computation failed", e);
}
}
private String normalizeBody(String body) {
if (body == null) return "";
// Remove whitespace for consistent hashing
return body.replaceAll("\\s+", "");
}
}
Mock Controller
package com.gonnect.mocker.controller;
import com.gonnect.mocker.service.MockService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/mock")
@RequiredArgsConstructor
public class MockController {
private final MockService mockService;
@RequestMapping(value = "/**", method = {
RequestMethod.GET, RequestMethod.POST,
RequestMethod.PUT, RequestMethod.DELETE,
RequestMethod.PATCH
})
public ResponseEntity<String> handleRequest(
HttpServletRequest request,
@RequestBody(required = false) String body,
@RequestHeader HttpHeaders headers) {
String path = request.getRequestURI().replace("/mock", "");
String method = request.getMethod();
return mockService.processRequest(method, path, body, headers);
}
}
Admin Controller
package com.gonnect.mocker.controller;
import com.gonnect.mocker.model.MockDefinition;
import com.gonnect.mocker.repository.MockRepository;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/admin/mocks")
@Api(tags = "Mock Administration")
@RequiredArgsConstructor
public class AdminController {
private final MockRepository mockRepository;
@GetMapping
@ApiOperation("List all mock definitions")
public List<MockDefinition> listMocks() {
return mockRepository.findAll();
}
@GetMapping("/active")
@ApiOperation("List active mock definitions")
public List<MockDefinition> listActiveMocks() {
return mockRepository.findByActiveTrue();
}
@GetMapping("/{id}")
@ApiOperation("Get mock definition by ID")
public ResponseEntity<MockDefinition> getMock(@PathVariable Long id) {
return mockRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
@ApiOperation("Create new mock definition")
public MockDefinition createMock(@RequestBody MockDefinition mock) {
return mockRepository.save(mock);
}
@PutMapping("/{id}")
@ApiOperation("Update mock definition")
public ResponseEntity<MockDefinition> updateMock(
@PathVariable Long id,
@RequestBody MockDefinition mock) {
return mockRepository.findById(id)
.map(existing -> {
mock.setId(id);
mock.setCreatedAt(existing.getCreatedAt());
return ResponseEntity.ok(mockRepository.save(mock));
})
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
@ApiOperation("Delete mock definition")
public ResponseEntity<Void> deleteMock(@PathVariable Long id) {
if (mockRepository.existsById(id)) {
mockRepository.deleteById(id);
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
@PostMapping("/{id}/toggle")
@ApiOperation("Toggle mock active status")
public ResponseEntity<MockDefinition> toggleMock(@PathVariable Long id) {
return mockRepository.findById(id)
.map(mock -> {
mock.setActive(!mock.isActive());
return ResponseEntity.ok(mockRepository.save(mock));
})
.orElse(ResponseEntity.notFound().build());
}
}
Dynamic Response Templates
Mocker supports Groovy-based response templates for dynamic response generation.
Template Interface
package com.gonnect.mocker.template;
public interface SimpleResponseTemplate {
String transform(String requestBody, String responseTemplate);
}
public interface SmartResponseTemplate {
String transform(Map<String, Object> request, String responseTemplate);
}
Groovy Template Example
// templates/DynamicResponseTemplate.groovy
package templates
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
class DynamicResponseTemplate implements SimpleResponseTemplate {
@Override
String transform(String requestBody, String responseTemplate) {
def jsonSlurper = new JsonSlurper()
def request = jsonSlurper.parseText(requestBody)
// Replace placeholders in response template
def response = responseTemplate
.replace('{{orderId}}', request.orderId ?: 'ORD-' + UUID.randomUUID())
.replace('{{timestamp}}', System.currentTimeMillis().toString())
.replace('{{status}}', calculateStatus(request))
return response
}
private String calculateStatus(def request) {
if (request.amount > 10000) {
return 'PENDING_APPROVAL'
}
return 'CONFIRMED'
}
}
Template Service
package com.gonnect.mocker.service;
import com.gonnect.mocker.template.SimpleResponseTemplate;
import groovy.lang.GroovyClassLoader;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service
public class TemplateService {
private final GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
private final ConcurrentHashMap<String, Class<?>> templateCache =
new ConcurrentHashMap<>();
public String applyTemplate(
String templateClass,
String requestBody,
String responseTemplate) {
try {
Class<?> clazz = templateCache.computeIfAbsent(
templateClass,
this::loadTemplateClass);
SimpleResponseTemplate template =
(SimpleResponseTemplate) clazz.getDeclaredConstructor()
.newInstance();
return template.transform(requestBody, responseTemplate);
} catch (Exception e) {
log.error("Template execution failed", e);
return responseTemplate;
}
}
private Class<?> loadTemplateClass(String className) {
try {
return groovyClassLoader.loadClass(className);
} catch (Exception e) {
throw new RuntimeException("Failed to load template: " + className, e);
}
}
}
Configuration
Application Configuration
# application.yml
server:
port: 8080
tomcat:
max-threads: 200
min-spare-threads: 10
spring:
application:
name: mocker
datasource:
url: jdbc:h2:mem:mockerdb
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
path: /h2-console
jpa:
hibernate:
ddl-auto: create-drop
show-sql: false
# Mocker Configuration
mocker:
target:
url: ${TARGET_SERVICE_URL:}
record:
enabled: true
tcp:
enabled: false
port: 9999
Building and Running
Build
# Clone repository
git clone https://github.com/mgorav/Mocker.git
cd Mocker
# Build
mvn clean install
# Run
java -jar target/Mocker-*.jar
Docker
# Build image
docker build -t mocker:latest .
# Run with target service
docker run -p 8080:8080 \
-e TARGET_SERVICE_URL=http://real-service.com \
mocker:latest
API Usage Examples
Create a Mock
curl -X POST http://localhost:8080/admin/mocks \
-H "Content-Type: application/json" \
-d '{
"name": "Get User",
"method": "GET",
"path": "/api/users/123",
"requestHash": "",
"responseBody": "{\"id\": \"123\", \"name\": \"John Doe\", \"email\": \"john@example.com\"}",
"responseStatus": 200,
"contentType": "application/json",
"active": true
}'
Use the Mock
curl http://localhost:8080/mock/api/users/123
# Response:
# {"id": "123", "name": "John Doe", "email": "john@example.com"}
Access Swagger UI
Navigate to http://localhost:8080/swagger-ui.html for interactive API documentation.
Conclusion
The Mocker service virtualization platform provides:
- Record & Replay: Automatically capture and replay service interactions
- Protocol Support: REST, SOAP, and TCP mocking capabilities
- Dynamic Templates: Groovy-based response transformation
- Simple Administration: REST API and Swagger UI for mock management
- Hash-Based Matching: Intelligent request deduplication
This enables:
- Parallel development without waiting for dependencies
- Consistent testing environments
- Edge case simulation
- Cost reduction for third-party API testing
Service virtualization is an essential tool for modern development teams building distributed systems.