How to Integrate Spring Boot with Third‑Party APIs: HTTP Clients, Sync Strategies, and Code Samples
This article explains how to connect Spring Boot to external services by choosing the appropriate HTTP client (RestTemplate, Feign, WebClient), configuring beans, implementing service methods, and applying various data‑synchronization techniques such as full sync, UPSERT, incremental sync, webhook callbacks, and message‑queue based replication.
Choosing an HTTP Client
Spring Boot offers three main HTTP client options depending on the scenario:
RestTemplate – synchronous, suitable for simple calls.
Feign – declarative, integrates with Spring Cloud for micro‑service environments.
WebClient – reactive, non‑blocking, ideal for high‑concurrency workloads.
RestTemplate Example
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(5000); // 5 s connection timeout
factory.setReadTimeout(5000); // 5 s read timeout
return new RestTemplate(factory);
}
}
@Service
public class ThirdPartyService {
@Resource
private RestTemplate restTemplate;
public UserDTO getUserById(Long id) {
String url = "https://api.example.com/users/{id}";
return restTemplate.getForObject(url, UserDTO.class, id);
}
public UserDTO createUser(UserRequest req) {
String url = "https://api.example.com/users";
return restTemplate.postForObject(url, req, UserDTO.class);
}
}Feign Example
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>3.1.3</version>
</dependency>
@SpringBootApplication
@EnableFeignClients
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@FeignClient(name = "user-api", url = "https://api.example.com")
public interface UserFeignClient {
@GetMapping("/users/{id}")
UserDTO getUserById(@PathVariable("id") Long id);
@PostMapping("/users")
UserDTO createUser(@RequestBody UserRequest req);
}
@Service
public class ThirdPartyService {
@Resource
private UserFeignClient client;
public UserDTO getUserById(Long id) { return client.getUserById(id); }
public UserDTO createUser(UserRequest req) { return client.createUser(req); }
}WebClient Example
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Content-Type", "application/json")
.build();
}
}
@Service
public class ReactiveThirdPartyService {
@Resource
private WebClient webClient;
public Mono<UserDTO> getUserById(Long id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(UserDTO.class);
}
public Mono<UserDTO> createUser(UserRequest req) {
return webClient.post()
.uri("/users")
.bodyValue(req)
.retrieve()
.bodyToMono(UserDTO.class);
}
}Data Synchronization Strategies
The article classifies synchronization into three major categories: full/UPSERT sync, incremental sync, and real‑time sync.
1. Full Sync (Scheduled Batch)
Typical for small data sets where real‑time consistency is not required. A @Scheduled job runs at 02:00 daily, deletes existing rows, fetches the entire dataset from the third‑party API, and inserts it in a single transaction.
@Scheduled(cron = "0 0 2 * * *")
public void performFullSync() {
log.info("--- Starting full sync ---");
Instant start = Instant.now();
try {
syncDepartments();
syncUsers();
log.info("--- Full sync completed in {} ms ---", Duration.between(start, Instant.now()).toMillis());
} catch (Exception e) {
log.error("Full sync failed", e);
}
}
private void syncDepartments() {
String url = apiBaseUrl + "/api/departments";
Department[] remote = restTemplate.getForObject(url, Department[].class);
if (remote == null || remote.length == 0) { log.warn("No department data"); return; }
List<Department> list = Arrays.asList(remote);
departmentService.saveBatch(list);
log.info("Departments synced: {}", remote.length);
}
private void syncUsers() {
String url = apiBaseUrl + "/api/users";
User[] remote = restTemplate.getForObject(url, User[].class);
if (remote == null || remote.length == 0) { log.warn("No user data"); return; }
List<User> list = Arrays.asList(remote);
userService.saveBatch(list);
log.info("Users synced: {}", remote.length);
}2. UPSERT + Delete‑Not‑In (SaveOrUpdateBatch)
For most production scenarios, this method guarantees data consistency while avoiding full deletions. It performs a batch UPSERT and then removes rows that no longer exist in the source.
public void syncDepartments() {
Department[] remote = restTemplate.getForObject(url, Department[].class);
List<Department> list = Arrays.asList(remote);
departmentService.saveOrUpdateBatch(list);
List<String> remoteIds = list.stream().map(Department::getExternalId).collect(Collectors.toList());
departmentService.removeByExternalIdNotIn(remoteIds);
log.info("Departments upserted: {}", remote.length);
}3. Incremental Sync (Timestamp‑Based)
A sync_checkpoint table stores the last successful sync timestamp. Each run queries the third‑party API with since=last_sync_time, processes the delta, and updates the checkpoint.
@Scheduled(cron = "0 */10 * * * *")
public void performIncrementalSync() {
LocalDateTime last = getLastSyncTime();
String url = apiBaseUrl + "/api/changes?since=" + dtf.format(last);
ChangeEventWrapper changes = restTemplate.getForObject(url, ChangeEventWrapper.class);
if (changes == null || (changes.getDepartments().isEmpty() && changes.getUsers().isEmpty())) {
updateCheckpoint();
return;
}
applyChanges(changes);
updateCheckpoint();
log.info("Incremental sync completed");
}4. Real‑Time Sync via Webhook
Third‑party systems push events to a public endpoint. The controller validates the signature, parses the JSON payload, and hands the event to an asynchronous service.
@RestController
@RequestMapping("/api/webhook")
public class WebhookController {
@Autowired private SyncService syncService;
@Autowired private WebhookSignatureService sigService;
@PostMapping("/dingtalk")
public ResponseEntity<String> handle(@RequestBody String body,
@RequestHeader("X-Signature") String sig,
@RequestHeader("X-Timestamp") String ts) {
if (!sigService.validateSignature(body, ts, sig)) {
return ResponseEntity.badRequest().body("Invalid signature");
}
DingTalkWebhookEvent ev = JsonUtils.parseObject(body, DingTalkWebhookEvent.class);
syncService.asyncProcessEvent(ev);
return ResponseEntity.ok("{\"errcode\":0,\"errmsg\":\"success\"}");
}
}5. Real‑Time Sync via Message Queue
Producers publish change events to a durable queue (e.g., RabbitMQ). Consumers listen, deserialize the event, and apply the update. Ack and DLQ mechanisms ensure reliability.
@Service
public class EventProducer {
@Autowired private RabbitTemplate rabbitTemplate;
public void sendUserUpdate(User user) {
UserUpdateEvent ev = new UserUpdateEvent(user.getId(), user.getName(), LocalDateTime.now());
rabbitTemplate.convertAndSend("user.sync.exchange", "user.update", ev);
}
}
@Service
public class EventConsumer {
@Autowired private UserRepository repo;
@RabbitListener(queues = "user.update.queue")
public void handle(UserUpdateEvent ev) {
User u = repo.findByExternalId(ev.getUserId())
.orElseThrow(() -> new RuntimeException("User not found"));
u.setName(ev.getUserName());
repo.save(u);
}
}Each approach balances latency, complexity, and reliability. For small, infrequent integrations, RestTemplate or Feign with scheduled full sync may suffice. High‑throughput or low‑latency requirements favor WebClient combined with incremental or webhook‑driven sync, optionally backed by a message queue for durability.
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.
