Skip to content
Vince.
All case studies

Middleware · Camel · Spring Boot

Integration gateway

One REST interface over a SOAP backend and a REST one

A middleware layer that gives callers one interface over two very different provisioning backends, and answers the questions a gateway exists for: what to retry, what to give up on, and what a caller gets back when the far end is down.

  • Java 21
  • Apache Camel
  • Spring Boot
  • WireMock

Repository: integration-gateway

In short

  • Retry policy that distinguishes transient from permanent — 5xx and 429 back off, 4xx fails immediately
  • Idempotent by request id, and a duplicate gets an answer rather than being silently dropped
  • SOAP envelopes escaped against XML injection; responses parsed with DTDs disabled against XXE
  • 26 tests against a WireMock backend that can be told to fail twice and then recover
  • A stub profile runs it end to end with no Docker and no external services

Anyone can forward a request

The value of a gateway is not in passing messages along. It is in having a considered answer for what happens when a backend is slow, down, or replying with nonsense — and in making that answer the same for every caller, so twelve client teams do not each invent their own retry loop.

Here that means a canonical request and result shape that callers speak regardless of which backend serves them. That broadband provisioning is a twenty-year-old SOAP service and mobile is a modern REST API is the gateway's problem, not theirs.

Retrying the right things

A 503 means try later, and a retry with backoff is exactly right. A 400 means the request is wrong, and four more attempts produce four more 400s while holding a connection open and delaying the caller's error. Retrying everything is the default mistake, and it converts a small backend wobble into a self-inflicted load spike at the worst possible moment.

A SOAP fault gets the same treatment as a 4xx. It is a considered answer that will be identical next time, so it maps to a failed result rather than three more attempts. The test asserts the backend was called exactly once.

Two retry layers is one too many

The test for 429 handling failed on its first run: eight requests reached the backend where four were expected. Apache HttpClient retries 429 and 503 by default, underneath Camel's error handler, so the two layers multiplied — and because the inner layer knows nothing about the outer backoff, the attempts also came twice as fast as intended.

This is the kind of defect that never surfaces in a functional test, because the request still succeeds. It surfaces in production, as a backend that is already struggling receiving double the retries anyone intended. Retry policy belongs in exactly one place; here that is the route, because only the route knows which failures are worth retrying and who is waiting for the answer.

Security in a translation layer

The SOAP envelope is assembled as text, which makes the bytes on the wire obvious and puts the burden of escaping squarely on this code. Without it, a subscriber id containing a closing tag lets a caller write arbitrary elements into the envelope — the XML equivalent of SQL injection. The test feeds exactly that and asserts the injected element does not appear.

In the other direction, the response parser disables DTDs entirely. A SOAP response is XML from another system, and a document declaring an external entity can make the parser read local files or open outbound connections — unauthenticated, and invisible in a response that otherwise looks normal. The test feeds the classic payload and asserts it is refused.

Seeing it run

Captured from an actual run, not an illustration. The repository has the script that produced it.

./demo/run-demo.sh
$ mvn spring-boot:run -Dspring-boot.run.profiles=stub$ ./demo/run-demo.sh 1. Broadband activation — routed to the SOAP backend  HTTP 200  {"status":"ACTIVATED","backendReference":"PRV-CIRCUIT-1"} 2. Mobile activation — routed to the REST backend  HTTP 200  {"status":"ACTIVATED","backendReference":"REST-MSISDN-1"} 3. Same request id twice — idempotency  HTTP 200  {"status":"ACTIVATED","backendReference":"PRV-CIRCUIT-2"}  HTTP 200  {"status":"DUPLICATE","message":"Request already processed"} 4. Backend fails twice then recovers — retry with backoff  HTTP 200  {"status":"ACTIVATED","backendReference":"PRV-FLAKY-2"} 5. Backend always answers 500 — retries exhaust  HTTP 502  {"status":"FAILED","message":"...statusCode: 500"} 6. Backend answers 400 — must NOT be retried  HTTP 502  {"status":"FAILED","message":"...statusCode: 400"} 7. SOAP fault — a considered refusal, not a transient error  HTTP 502  {"status":"FAILED","message":"Provisioning fault: Circuit already active"} How many times each resource actually reached a backend  {"FAIL-400":1,"FAIL-500":4,"FAULT":1,"FLAKY-2":3}
The four numbers at the end are the retry policy. FAIL-400 reached the backend once, because a client error will not improve on the fourth attempt.

Source

  • src/main/java/com/dvpalmes/gateway/route/ActivationRoutes.java
  • src/main/java/com/dvpalmes/gateway/transform/SoapResponseParser.java
  • src/test/java/com/dvpalmes/gateway/route/ActivationRoutesTest.java