api-processor is a lightweight Java library to standardize external API integrations using a Facade -> Handler -> Delegate pattern.
It supports:
- REST orchestration via
RestWebServiceFacade - SOAP orchestration via
SoapWebServiceFacade - Built-in retry support (optional)
- Consistent request/response preparation and post-processing hooks
If you have many downstream APIs and want a clean, reusable integration structure, this library gives you that contract.
<dependency>
<groupId>com.github.salilvnair</groupId>
<artifactId>api-processor</artifactId>
<version>1.0.4</version>
</dependency>RestWebServiceFacade.initiate(handler, restWsMap, objects...) drives the call lifecycle:
handler.prepareRequest(...)handler.delegate().invoke(...)- optional retry wrapper (if delegate enables retry)
handler.processResponse(...)
RestWebServiceRequestandRestWebServiceResponseare marker interfaces.- You define your own concrete request/response classes.
restWsMapis a shared mutable map for passing data across steps.objects...is optional contextual input (your DTO/context class).
RestWebServiceHandler:
- chooses delegate (
delegate()) - builds request (
prepareRequest(...)) - transforms response (
processResponse(...)) - optional metadata:
webServiceName()for logsprintLogs()defaulttrueemptyPayLoad()defaultfalse
RestWebServiceDelegate:
- performs actual HTTP invocation in
invoke(...) - optional retry controls:
retry()maxRetries()delay()+delayTimeUnit()whiteListedExceptions()(string match on exception message)
When enabled, facade wraps delegate invocation with RetryExecutor:
- retries up to
maxRetries - waits for configured delay between retries
- if whitelist is set, retries only matching exception messages
- Keeps API transport logic inside delegate
- Keeps input/output mapping inside handler
- Keeps business flow outside infra classes
- Makes each API integration testable and replaceable
public class LoanApiContext {
private String path;
private String baseUrl;
private String apiKey;
private Map<String, Object> queryParams;
private LoanWsResponse mappedResponse;
// getters/setters
}import com.github.salilvnair.api.processor.rest.model.RestWebServiceRequest;
import com.github.salilvnair.api.processor.rest.model.RestWebServiceResponse;
public class LoanWsRequest implements RestWebServiceRequest {
private SomeClassInfo someClassInfo;
// getters/setters
}
public class LoanWsResponse implements RestWebServiceResponse {
private SomeClassInfo someClassInfo;
// getters/setters
}import com.github.salilvnair.api.processor.rest.handler.RestWebServiceDelegate;
import com.github.salilvnair.api.processor.rest.model.RestWebServiceRequest;
import com.github.salilvnair.api.processor.rest.model.RestWebServiceResponse;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Component
public class LoanApiWsDelegate implements RestWebServiceDelegate {
@Override
public RestWebServiceResponse invoke(RestWebServiceRequest request, Map<String, Object> restWsMap, Object... objects) {
LoanWsRequest wsRequest = (LoanWsRequest) request;
LoanApiContext context = (LoanApiContext) objects[0];
HttpHeaders headers = new HttpHeaders();
headers.setAccept(java.util.List.of(MediaType.APPLICATION_JSON));
headers.set("X-API-KEY", context.getApiKey());
HttpEntity<?> entity = new HttpEntity<>(wsRequest, headers);
String url = context.getBaseUrl() + context.getPath() + buildQuery(context.getQueryParams());
ResponseEntity<LoanWsResponse> responseEntity = new RestTemplate().exchange(url, HttpMethod.GET, entity, Map.class);
return responseEntity.getBody();
}
@Override
public boolean retry() {
return true;
}
@Override
public int maxRetries() {
return 2;
}
@Override
public int delay() {
return 300;
}
@Override
public TimeUnit delayTimeUnit() {
return TimeUnit.MILLISECONDS;
}
@Override
public java.util.List<String> whiteListedExceptions() {
return java.util.List.of("timed out", "Connection refused", "503");
}
private String buildQuery(Map<String, Object> queryParams) {
if (queryParams == null || queryParams.isEmpty()) {
return "";
}
StringBuilder out = new StringBuilder();
for (Map.Entry<String, Object> e : queryParams.entrySet()) {
if (e.getKey() == null || e.getKey().isBlank() || e.getValue() == null) {
continue;
}
out.append(out.isEmpty() ? "?" : "&")
.append(URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8))
.append("=")
.append(URLEncoder.encode(String.valueOf(e.getValue()), StandardCharsets.UTF_8));
}
return out.toString();
}
}import com.github.salilvnair.api.processor.rest.handler.RestWebServiceDelegate;
import com.github.salilvnair.api.processor.rest.handler.RestWebServiceHandler;
import com.github.salilvnair.api.processor.rest.model.RestWebServiceRequest;
import com.github.salilvnair.api.processor.rest.model.RestWebServiceResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class LoanApiWsHandler implements RestWebServiceHandler {
private final LoanApiWsDelegate delegate;
@Override
public RestWebServiceDelegate delegate() {
return delegate;
}
@Override
public RestWebServiceRequest prepareRequest(Map<String, Object> restWsMap, Object... objects) {
LoanApiContext context = (LoanApiContext) objects[0];
LoanWsRequest request = new LoanWsRequest();
request.setSomeClassInfo(prepareSomeClassInfo(context));
return request;
}
private SomeClassInfo prepareSomeClassInfo(LoanApiContext context) {
// build request wrapper from context
}
@Override
public void processResponse(RestWebServiceRequest request, RestWebServiceResponse response, Map<String, Object> restWsMap, Object... objects) {
LoanApiContext context = (LoanApiContext) objects[0];
LoanWsResponse wsResponse = (LoanWsResponse) response;
context.setMappedResponse(wsResponse);
restWsMap.put("mappedResponse", wsResponse);
}
@Override
public String webServiceName() {
return "LoanApiHttp";
}
}import com.github.salilvnair.api.processor.rest.facade.RestWebServiceFacade;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.LinkedHashMap;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class LoanApiService {
private final RestWebServiceFacade facade = new RestWebServiceFacade();
private final LoanApiWsHandler loanApiWsHandler;
public LoanWsResponse call( String path, Map<String, Object> queryParams) {
LoanApiContext context = new LoanApiContext();
context.setBaseUrl("https://some-loan-api.com");
context.setApiKey("some-api-key");
context.setPath(path);
context.setQueryParams(queryParams);
context.setResponseFieldMap(responseFieldMap);
Map<String, Object> restWsMap = new LinkedHashMap<>();
facade.initiate(loanApiHandler, restWsMap, context);
return context.getMappedResponse();
}
}SoapWebServiceFacade follows the same structure:
- Handler prepares SOAP request wrapper
- Delegate invokes
WebServiceTemplate - Handler processes response
- Retry available via delegate options
Key interfaces:
SoapWebServiceHandlerSoapWebServiceDelegateSoapWebServiceRequestSoapWebServiceResponse
For SOAP delegates, helper methods exist in SoapWebServiceDelegate for:
marshalSendAndReceive(...)- header extraction
- easy
WebServiceTemplatecreation via context path + endpoint
- Keep delegates pure transport-layer.
- Keep handlers focused on request/response mapping.
- Pass business context through typed context object via
objects.... - Use
restWsMapfor integration outputs only (mappedResponse, status, raw payload). - Enable retry only for idempotent/safe calls.
- Keep whitelist specific to transient errors (timeouts, 5xx, connection issues).
handler == nullordelegate == null
- Facade throws explicit exception.
- Retry not triggering
retry()must returntruein delegate.- For whitelist mode, exception text must contain configured strings.
- Empty payload call
- Return
truefromemptyPayLoad()if API doesn’t need request building.
- Missing mapped response
- Ensure
processResponse(...)writes output torestWsMap.
api-processor gives you a small, explicit integration framework:
- predictable lifecycle
- clear separation of concerns
- reusable transport delegates
- easy orchestration from service layer
For most consumer apps, start with one generic handler + one delegate + one context class, then scale by endpoint group (loan, customer, payment, etc.).