A proof-of-concept demonstrating header-based traffic routing for external service virtualization using Envoy, WireMock, and Mountebank. This pattern enables running load tests in production-like environments without affecting real external services.
This project showcases a technique for isolating load test traffic from production traffic at the infrastructure level. When your application makes calls to external services, the traffic is routed based on the X-Traffic-Type header:
LOAD_TEST→ Routed to Mountebank (mock server for load testing)PRODUCTION(or no header) → Routed to WireMock (simulating the real external service)
┌─────────────────┐
│ Mountebank │
│ (Load Test) │
│ :4545 │
└────────▲────────┘
│
│ X-Traffic-Type: LOAD_TEST
│
┌──────────┐ ┌──────────┐ ┌────────┴────────┐
│ Client │─────▶│ App │─────▶│ Envoy │
│ │ │ :8080 │ │ (Proxy) │
└──────────┘ └──────────┘ │ :15001 │
└────────┬────────┘
│
│ X-Traffic-Type: PRODUCTION
│
┌────────▼────────┐
│ WireMock │
│ (Real Service) │
│ :8080 │
└─────────────────┘
| Component | Purpose | Port |
|---|---|---|
| App | Spring Boot application with PaymentClient | 8080 |
| Envoy | Service proxy for header-based routing | 15001 |
| WireMock | Simulates the real external payment service | 8081 (host) / 8080 (container) |
| Mountebank | Mock server for load test traffic | 4545 |
| Service | Endpoint | Description |
|---|---|---|
| Swagger UI | http://localhost:8080/swagger-ui.html | API documentation with built-in traffic header support |
| OpenAPI Spec | http://localhost:8080/v3/api-docs | OpenAPI 3.0 specification (JSON) |
| Mountebank Admin | http://localhost:2525 | Mountebank admin interface for managing imposters |
| Mountebank Imposters | http://localhost:2525/imposters | View/manage all configured imposters |
| WireMock Admin | http://localhost:8081/__admin | WireMock admin interface |
| WireMock Mappings | http://localhost:8081/__admin/mappings | View/manage WireMock stub mappings |
💡 Tip: The Swagger UI includes
X-Traffic-TypeandX-Test-Run-Idheader fields on all endpoints, making it easy to test traffic routing directly from the browser.
-
Traffic Context Filter: Incoming requests to the app are intercepted by
TrafficTypeFilterwhich extracts theX-Traffic-Typeheader and stores it in a thread-local context. -
Header Propagation: When
PaymentClientmakes outbound HTTP calls, it reads the traffic context and sets theX-Traffic-Typeheader accordingly:- If the incoming request had
X-Traffic-Type: LOAD_TEST→ outbound request getsLOAD_TEST - Otherwise → outbound request gets
PRODUCTION
- If the incoming request had
-
Envoy Routing: Envoy intercepts all outbound traffic (via DNS alias
payment.external) and routes based on the header:LOAD_TEST→ Mountebank cluster- Everything else → WireMock cluster
- Docker & Docker Compose
- Java 21 (for local development)
docker-compose up --buildcurl -X GET http://localhost:8080/paymentsExpected Response:
{
"status": "SUCCESS",
"transactionId": "real-txn-12345",
"message": "Payment processed via WireMock (Simulating REAL Server)"
}curl -X GET -H "X-Traffic-Type: LOAD_TEST" http://localhost:8080/paymentsExpected Response:
{
"transactionId": "mountebank-txn-67890",
"status": "SUCCESS",
"message": "Payment processed via Mountebank (LOAD_TEST)"
}Open http://localhost:8080/swagger-ui.html in your browser. Each endpoint includes X-Traffic-Type and X-Test-Run-Id header fields, allowing you to easily switch between routing targets without using curl.
├── docker-compose.yml # Container orchestration
├── Dockerfile # App container build
├── src/
│ └── main/
│ └── kotlin/
│ └── com/example/virtualization/
│ ├── client/
│ │ └── PaymentClient.kt # HTTP client with header propagation
│ ├── config/
│ │ └── OpenApiConfig.kt # Swagger UI with traffic header support
│ ├── controller/
│ │ └── PaymentController.kt # REST endpoint
│ └── traffic/
│ ├── TrafficContext.kt # Traffic type constants
│ ├── TrafficContextManager.kt
│ └── TrafficTypeFilter.kt # Request interceptor
└── services/
├── envoy/
│ ├── envoy.yaml # Envoy routing configuration
│ └── logs/ # Access logs
├── mountebank/
│ └── imposters/
│ └── payment-imposter.json # Load test mock responses
└── wiremock/
└── mappings/
└── payment-stub.json # Production mock responses
| Header | Values | Description |
|---|---|---|
X-Traffic-Type |
LOAD_TEST, PRODUCTION |
Determines traffic routing |
X-Test-Run-Id |
Any string | Optional identifier for test runs (for logging/tracing) |
Access logs are written to services/envoy/logs/access.log with the following format:
[timestamp] "METHOD PATH PROTOCOL" STATUS FLAGS BYTES_IN BYTES_OUT DURATION "TRAFFIC_TYPE" "UPSTREAM_CLUSTER"
Example:
[2026-02-03T10:30:45.123Z] "GET /external/payment HTTP/1.1" 200 - 0 156 12 "LOAD_TEST" "mountebank"
[2026-02-03T10:30:50.456Z] "GET /external/payment HTTP/1.1" 200 - 0 148 8 "PRODUCTION" "wiremock"
Run load tests against your application while ensuring test traffic doesn't hit real external services:
# Using k6, JMeter, or any load testing tool
# Just include the header in your requests:
curl -H "X-Traffic-Type: LOAD_TEST" http://your-app/paymentsConfigure different responses in Mountebank to test how your application handles various scenarios (errors, timeouts, edge cases) without affecting production users.
- Use WireMock to simulate real service responses during development
- Use Mountebank to test error handling and edge cases
- Add new cluster definitions in
envoy.yaml - Create corresponding stubs in WireMock and Mountebank
- Update routing rules as needed
Modify envoy.yaml to add additional header-based routing rules:
routes:
- match:
prefix: "/"
headers:
- name: X-Traffic-Type
exact_match: "CANARY"
route:
cluster: canary_service