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.

Java Companion
Java Companion
Java Companion
How to Integrate Spring Boot with Third‑Party APIs: HTTP Clients, Sync Strategies, and Code Samples

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.

Spring BootfeignMessage Queueresttemplatewebclientdata-syncIncremental Syncwebhook
Java Companion
Written by

Java Companion

A highly professional Java public account

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.