Skip to content

salilvnair/api-processor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

api-processor

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.

Maven

<dependency>
  <groupId>com.github.salilvnair</groupId>
  <artifactId>api-processor</artifactId>
  <version>1.0.4</version>
</dependency>

Core Concepts

1) REST flow

RestWebServiceFacade.initiate(handler, restWsMap, objects...) drives the call lifecycle:

  1. handler.prepareRequest(...)
  2. handler.delegate().invoke(...)
  3. optional retry wrapper (if delegate enables retry)
  4. handler.processResponse(...)

Contracts

  • RestWebServiceRequest and RestWebServiceResponse are marker interfaces.
  • You define your own concrete request/response classes.
  • restWsMap is a shared mutable map for passing data across steps.
  • objects... is optional contextual input (your DTO/context class).

2) Handler responsibilities

RestWebServiceHandler:

  • chooses delegate (delegate())
  • builds request (prepareRequest(...))
  • transforms response (processResponse(...))
  • optional metadata:
    • webServiceName() for logs
    • printLogs() default true
    • emptyPayLoad() default false

3) Delegate responsibilities

RestWebServiceDelegate:

  • performs actual HTTP invocation in invoke(...)
  • optional retry controls:
    • retry()
    • maxRetries()
    • delay() + delayTimeUnit()
    • whiteListedExceptions() (string match on exception message)

4) Retry behavior

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

Why This Pattern Works

  • 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

REST Quick Start (Minimal)

Step 1: Define context

public class LoanApiContext {
    private String path;
    private String baseUrl;
    private String apiKey;
    private Map<String, Object> queryParams;
    private LoanWsResponse mappedResponse;

    // getters/setters
}

Step 2: Define request/response wrappers

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
}

Step 3: Implement delegate (actual HTTP call)

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();
  }
}

Step 4: Implement handler (request prep + response mapping)

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";
  }
}

Step 5: Call facade from service

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();
  }
}

SOAP Usage (Overview)

SoapWebServiceFacade follows the same structure:

  • Handler prepares SOAP request wrapper
  • Delegate invokes WebServiceTemplate
  • Handler processes response
  • Retry available via delegate options

Key interfaces:

  • SoapWebServiceHandler
  • SoapWebServiceDelegate
  • SoapWebServiceRequest
  • SoapWebServiceResponse

For SOAP delegates, helper methods exist in SoapWebServiceDelegate for:

  • marshalSendAndReceive(...)
  • header extraction
  • easy WebServiceTemplate creation via context path + endpoint

Best Practices for Consumers

  1. Keep delegates pure transport-layer.
  2. Keep handlers focused on request/response mapping.
  3. Pass business context through typed context object via objects....
  4. Use restWsMap for integration outputs only (mappedResponse, status, raw payload).
  5. Enable retry only for idempotent/safe calls.
  6. Keep whitelist specific to transient errors (timeouts, 5xx, connection issues).

Common Pitfalls

  1. handler == null or delegate == null
  • Facade throws explicit exception.
  1. Retry not triggering
  • retry() must return true in delegate.
  • For whitelist mode, exception text must contain configured strings.
  1. Empty payload call
  • Return true from emptyPayLoad() if API doesn’t need request building.
  1. Missing mapped response
  • Ensure processResponse(...) writes output to restWsMap.

Summary

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.).

About

Standardize the way of calling SOAP and Rest API calls in Java

Resources

License

Stars

Watchers

Forks

Contributors

Languages