A Ready‑to‑Use Spring Boot Template for Seamless Third‑Party Integration
The article explains why ad‑hoc third‑party integrations quickly become unmaintainable, identifies four common pain points, and presents a layered architecture using template method, strategy and factory patterns that abstracts common concerns such as retries, logging and signing, dramatically reducing duplicated code and maintenance effort.
Why ad‑hoc third‑party integration gets messy
Backend developers often need to connect to dozens of external services—SMS, payment, object storage, LLM APIs, enterprise WeChat, logistics, etc. Most teams create a separate service for each vendor, rewriting HTTP calls, exception handling, retries, signatures and logging each time. Over time the codebase becomes inconsistent, duplicated, and hard to refactor; even swapping the underlying HTTP client requires changes in many places.
Four typical pain points
Lack of unified abstraction : each integration has its own contract and implementation (RestTemplate, OkHttp, HttpURLConnection), making onboarding new developers costly.
Common capabilities duplicated : timeout, retry, logging, signature, and data masking are re‑implemented for every vendor, leading to omissions and bugs.
Business logic tightly coupled with technical code : HTTP calls, parameter assembly and signing are mixed with business code, so changing the HTTP client or adding circuit‑breaker logic requires sweeping changes.
Inconsistent error handling : some integrations return null, others throw exceptions or embed error codes in responses, making troubleshooting difficult.
Core design: layered decoupling with reusable template
The solution uses a four‑layer architecture that follows the Open‑Closed Principle:
Business call layer : services invoke a unified client interface without knowing the vendor.
Vendor implementation layer : one class per third‑party handles only parameter assembly, result parsing and signature adaptation.
Abstract template layer : encapsulates the common flow—pre‑check, request building, logging, HTTP call, response parsing, timing, and unified exception handling.
Configuration & base layer : centralizes URLs, keys, timeouts and other settings, supporting dynamic refresh.
Design principles
Program to interfaces so business code depends only on the top‑level contract.
Template method pattern fixes the call flow in an abstract base class; subclasses implement the variable parts.
Strategy pattern isolates vendor‑specific logic, and a factory routes calls by client code.
Single‑responsibility: generic technical concerns stay in the base layer, business parameters stay in the vendor layer.
Key code artifacts
Unified request/response base classes :
/** 第三方请求基类 */
@Data
public abstract class ThirdPartyRequest {
/** 请求唯一ID,自动生成,用于链路追踪 */
private String requestId = UUID.randomUUID().toString().replace("-", "");
/** 业务超时时间,单位毫秒,不填使用默认值 */
private Long timeout;
}
/** 第三方响应基类 */
@Data
public class ThirdPartyResponse<T> {
private boolean success;
private String errorCode;
private String errorMsg;
private T data;
private String requestId;
private long costTime;
public static <T> ThirdPartyResponse<T> success(T data) { /* ... */ }
public static <T> ThirdPartyResponse<T> fail(String code, String msg) { /* ... */ }
}Top‑level client interface :
public interface ThirdPartyClient<Q extends ThirdPartyRequest, R> {
String getClientCode();
ThirdPartyResponse<R> execute(Q request);
}Abstract template class (core of the framework):
@Slf4j
public abstract class AbstractThirdPartyClient<Q extends ThirdPartyRequest, R>
implements ThirdPartyClient<Q, R> {
@Autowired protected ThirdPartyProperties properties;
@Autowired private RestTemplate restTemplate;
@Override
public final ThirdPartyResponse<R> execute(Q request) {
long start = System.currentTimeMillis();
String requestId = request.getRequestId();
String clientCode = getClientCode();
try {
preCheck(request);
ClientConfig config = properties.getClientConfig(clientCode);
String url = buildRequestUrl(request, config);
HttpHeaders headers = buildHeaders(request, config);
Object body = buildRequestBody(request, config);
log.info("[Third‑party request] client={}, requestId={}, url={}, params={}",
clientCode, requestId, url, desensitize(body));
ResponseEntity<String> resp = doHttpCall(url, headers, body, config);
ThirdPartyResponse<R> result = parseResponse(resp.getBody(), config);
result.setRequestId(requestId);
long cost = System.currentTimeMillis() - start;
result.setCostTime(cost);
log.info("[Third‑party response] client={}, requestId={}, success={}, cost={}ms, resp={}",
clientCode, requestId, result.isSuccess(), cost, desensitize(result.getData()));
return result;
} catch (BusinessException e) {
long cost = System.currentTimeMillis() - start;
log.warn("[Third‑party business error] client={}, requestId={}, code={}, msg={}, cost={}ms",
clientCode, requestId, e.getCode(), e.getMessage(), cost);
return ThirdPartyResponse.fail(e.getCode(), e.getMessage());
} catch (Exception e) {
long cost = System.currentTimeMillis() - start;
log.error("[Third‑party system error] client={}, requestId={}, cost={}ms", clientCode, requestId, cost, e);
return ThirdPartyResponse.fail("SYSTEM_ERROR", "Third‑party call exception: " + e.getMessage());
}
}
// ----- abstract methods to be implemented by concrete vendors -----
protected abstract void preCheck(Q request);
protected abstract String buildRequestUrl(Q request, ClientConfig config);
protected abstract HttpHeaders buildHeaders(Q request, ClientConfig config);
protected abstract Object buildRequestBody(Q request, ClientConfig config);
protected abstract ThirdPartyResponse<R> parseResponse(String responseBody, ClientConfig config);
protected Object desensitize(Object data) { return data; }
// ----- private helper for HTTP call -----
private ResponseEntity<String> doHttpCall(String url, HttpHeaders headers, Object body, ClientConfig config) {
HttpEntity<Object> entity = new HttpEntity<>(body, headers);
return restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
}
}Concrete SMS provider example (only the vendor‑specific parts are shown):
@Component
public class SmsProviderAClient extends AbstractThirdPartyClient<SmsSendRequest, SmsSendResult> {
private static final String CLIENT_CODE = "sms-provider-a";
@Override public String getClientCode() { return CLIENT_CODE; }
@Override protected void preCheck(SmsSendRequest req) {
if (StrUtil.isBlank(req.getPhone())) throw new BusinessException("PARAM_ERROR", "Phone cannot be empty");
if (StrUtil.isBlank(req.getContent())) throw new BusinessException("PARAM_ERROR", "Content cannot be empty");
}
@Override protected String buildRequestUrl(SmsSendRequest req, ClientConfig cfg) {
return cfg.getBaseUrl() + "/sms/send";
}
@Override protected HttpHeaders buildHeaders(SmsSendRequest req, ClientConfig cfg) {
HttpHeaders h = new HttpHeaders();
h.setContentType(MediaType.APPLICATION_JSON);
String appId = cfg.getParams().get("app-id");
String timestamp = String.valueOf(System.currentTimeMillis());
String sign = calculateSign(appId, timestamp, cfg.getParams().get("app-secret"));
h.add("X-App-Id", appId);
h.add("X-Timestamp", timestamp);
h.add("X-Sign", sign);
return h;
}
@Override protected Object buildRequestBody(SmsSendRequest req, ClientConfig cfg) {
Map<String, Object> body = new HashMap<>();
body.put("phone", req.getPhone());
body.put("content", req.getContent());
body.put("request_id", req.getRequestId());
return body;
}
@Override protected ThirdPartyResponse<SmsSendResult> parseResponse(String respBody, ClientConfig cfg) {
JSONObject json = JSON.parseObject(respBody);
int code = json.getIntValue("code");
if (code == 0) {
SmsSendResult r = new SmsSendResult();
r.setSmsId(json.getString("sms_id"));
r.setFee(json.getBigDecimal("fee"));
return ThirdPartyResponse.success(r);
}
return ThirdPartyResponse.fail(String.valueOf(code), json.getString("msg"));
}
@Override protected Object desensitize(Object data) {
if (data instanceof SmsSendRequest req) {
SmsSendRequest copy = new SmsSendRequest();
copy.setPhone(DesensitizeUtil.mobile(req.getPhone()));
copy.setContent(req.getContent());
return copy;
}
return data;
}
private String calculateSign(String appId, String timestamp, String secret) {
String raw = appId + timestamp + secret;
return SecureUtil.md5(raw).toUpperCase();
}
}Client factory that auto‑wires all implementations and returns the correct one by code:
@Component
public class ThirdPartyClientFactory implements InitializingBean {
@Autowired private List<ThirdPartyClient<?, ?>> allClients;
private final Map<String, ThirdPartyClient<?, ?>> clientMap = new HashMap<>();
@Override public void afterPropertiesSet() {
for (ThirdPartyClient<?, ?> c : allClients) clientMap.put(c.getClientCode(), c);
}
@SuppressWarnings("unchecked")
public <Q extends ThirdPartyRequest, R> ThirdPartyClient<Q, R> getClient(String code) {
ThirdPartyClient<?, ?> c = clientMap.get(code);
if (c == null) throw new IllegalArgumentException("No client for code: " + code);
return (ThirdPartyClient<Q, R>) c;
}
}Business‑layer usage (no HTTP or signing code appears):
@Service
public class SmsService {
@Autowired private ThirdPartyClientFactory clientFactory;
public void sendSms(String phone, String content) {
ThirdPartyClient<SmsSendRequest, SmsSendResult> client =
clientFactory.getClient("sms-provider-a");
SmsSendRequest req = new SmsSendRequest();
req.setPhone(phone);
req.setContent(content);
ThirdPartyResponse<SmsSendResult> resp = client.execute(req);
if (!resp.isSuccess()) {
throw new BusinessException(resp.getErrorCode(), resp.getErrorMsg());
}
}
}Extending the framework
To add a new third‑party integration you only need three steps: (1) add its configuration (URL, keys, timeouts) to application.yml; (2) create a subclass of AbstractThirdPartyClient implementing the five abstract methods; (3) invoke it via the factory using the configured client code. No core code changes are required, satisfying the open‑closed principle.
Additional cross‑cutting capabilities
Retry & circuit‑breaker : integrate Spring Retry or Resilience4j in the doHttpCall layer to automatically retry transient failures and open a circuit on high error rates.
Unified monitoring : embed Micrometer metrics (call count, success rate, latency, error‑code distribution) in the template method; expose them to Prometheus for alerting.
Unified exception hierarchy : business exceptions (vendor‑returned error codes), system exceptions (network timeouts, service unavailable), and signature exceptions each map to distinct retry and alerting policies.
Common signing utilities : MD5, RSA, HMAC helpers are provided so each vendor client only calls a utility method.
Data desensitization : a central utility masks phone numbers, IDs, keys and amounts in logs, preventing accidental leakage.
Best‑practice checklist
Business code depends only on the top‑level interface, never on concrete client classes.
All sensitive configuration lives in a configuration center; changes do not require redeployment.
Separate connection timeout and read timeout; keep connection timeout short (1‑2 s) and adjust read timeout per use case.
Provide fallback implementations for critical paths (payment, authentication) to avoid cascading failures.
Propagate a unique requestId through every layer; logs, metrics and third‑party responses all carry this ID for end‑to‑end traceability.
Keep client implementations pure: only parameter conversion and protocol adaptation; all business logic stays in service layer.
By applying well‑known design patterns in the right place—template method for the fixed flow, strategy for vendor differences, and factory for creation—you obtain a clean, extensible architecture that cuts maintenance cost dramatically while keeping the codebase consistent and observable.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
