How to Connect Spring Boot to Third‑Party APIs: RestTemplate, Feign, WebClient, and Sync Strategies

This guide walks through integrating Spring Boot with external services using RestTemplate, Feign, and WebClient, then compares full, incremental, and real‑time data‑synchronization approaches, providing step‑by‑step code examples, configuration details, and practical trade‑off analysis.

java1234
java1234
java1234
How to Connect Spring Boot to Third‑Party APIs: RestTemplate, Feign, WebClient, and Sync Strategies

HTTP client options

Spring Boot provides three HTTP client solutions for third‑party integration: RestTemplate (synchronous), Feign (declarative, Spring Cloud), and WebClient (reactive, non‑blocking).

RestTemplate (synchronous)

Steps:

Add the spring-boot-starter-web dependency.

Configure a RestTemplate bean with connection and read timeouts.

Inject the bean in a service and invoke the third‑party API (GET and POST examples).

Define request and response DTO classes that match the API payload.

@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 userId) {
        String url = "https://api.example.com/users/{id}";
        return restTemplate.getForObject(url, UserDTO.class, userId);
    }

    public UserDTO createUser(UserRequest request) {
        String url = "https://api.example.com/users";
        return restTemplate.postForObject(url, request, UserDTO.class);
    }
}

public class UserRequest {
    private String username;
    private String email;
    // getters & setters
}

public class UserDTO {
    private Long id;
    private String username;
    private String email;
    // getters & setters
}

Feign (declarative)

Steps:

Add the OpenFeign dependency ( spring-cloud-starter-openfeign).

Enable Feign clients with @EnableFeignClients on the Spring Boot application class.

Define a @FeignClient interface that maps HTTP methods to Java methods.

Inject the Feign client in a service and call it directly.

@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 userId);

    @PostMapping("/users")
    UserDTO createUser(@RequestBody UserRequest request);
}

@Service
public class UserService {
    @Resource
    private UserFeignClient userFeignClient;

    public UserDTO getUser(Long userId) {
        return userFeignClient.getUserById(userId);
    }

    public UserDTO addUser(UserRequest request) {
        return userFeignClient.createUser(request);
    }
}

WebClient (reactive, non‑blocking)

Steps:

Add the WebFlux starter ( spring-boot-starter-webflux).

Configure a WebClient bean with a base URL and default headers.

Use the bean in a service; the methods return Mono<UserDTO> (single result) or Flux<...> (multiple results).

Expose the reactive endpoints from a controller, returning the same reactive type.

@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 userId) {
        return webClient.get()
                .uri("/users/{id}", userId)
                .retrieve()
                .bodyToMono(UserDTO.class);
    }

    public Mono<UserDTO> createUser(UserRequest request) {
        return webClient.post()
                .uri("/users")
                .bodyValue(request)
                .retrieve()
                .bodyToMono(UserDTO.class);
    }
}

@RestController
@RequestMapping("/api/users")
public class UserController {
    @Resource
    private ReactiveThirdPartyService service;

    @GetMapping("/{id}")
    public Mono<UserDTO> getUser(@PathVariable Long id) {
        return service.getUserById(id);
    }

    @PostMapping
    public Mono<UserDTO> addUser(@RequestBody UserRequest request) {
        return service.createUser(request);
    }
}

Data synchronization strategies

Full sync (scheduled batch)

Suitable for small data volumes with low real‑time requirements. A @Scheduled(cron = "0 0 2 * * *") job runs at 02:00 daily, deletes existing local data, fetches all departments and users via RestTemplate, and batch‑saves them inside a transaction. The job logs start time, duration, and any errors.

@Slf4j
@Component
@RequiredArgsConstructor
public class FullSyncScheduler {
    private final RestTemplate restTemplate = new RestTemplate();
    private final DepartmentService departmentService;
    private final UserService userService;
    @Value("${third-party.api-base-url}")
    private String apiBaseUrl;

    @Scheduled(cron = "0 0 2 * * *")
    public void performFullSync() {
        log.info("--- Starting full sync ---");
        Instant start = Instant.now();
        try {
            departmentService.remove(new QueryWrapper<>());
            userService.remove(new QueryWrapper<>());
            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) {
            departmentService.saveBatch(Arrays.asList(remote));
        }
    }

    private void syncUsers() {
        String url = apiBaseUrl + "/api/users";
        User[] remote = restTemplate.getForObject(url, User[].class);
        if (remote != null && remote.length > 0) {
            userService.saveBatch(Arrays.asList(remote));
        }
    }
}

For larger datasets the article recommends an UPSERT + delete‑not‑in pattern ( saveOrUpdateBatch followed by removeByExternalIdNotIn) to avoid data gaps.

Incremental sync (timestamp based)

Maintain a sync_checkpoint table that records the last successful last_sync_time. Each run reads this timestamp, calls the third‑party API with ?since=last_sync_time, processes the returned rows, and finally updates last_sync_time to the current time.

CREATE TABLE sync_checkpoint (
    id INT PRIMARY KEY AUTO_INCREMENT,
    task_name VARCHAR(50) NOT NULL COMMENT 'Task name',
    last_sync_time DATETIME NOT NULL COMMENT 'Last sync timestamp',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

Example request URLs:

GET /api/departments?since=2024-05-20T10:00:00Z
GET /api/users?since=2024-05-20T10:00:00Z

Incremental sync (change‑id based)

If the external system provides a monotonically increasing change_id, add a last_change_id column to sync_checkpoint. Each run queries ?last_change_id=..., processes the changes, and updates the stored maximum ID.

ALTER TABLE sync_checkpoint ADD COLUMN last_change_id BIGINT DEFAULT 0 COMMENT 'Maximum processed change ID';

Example request:

GET /api/changes?last_change_id=12345

Real‑time sync (Webhook)

Expose a public POST endpoint, verify the signature supplied in request headers, parse the JSON payload into a domain event object, and hand the event to an asynchronous service to avoid blocking the caller.

@RestController
@RequestMapping("/api/webhook")
@Slf4j
public class WebhookController {
    @Autowired
    private SyncService syncService;
    @Autowired
    private WebhookSignatureService signatureService;

    @PostMapping("/dingtalk")
    public ResponseEntity<?> handle(@RequestBody String body,
                                    @RequestHeader("X-Signature") String signature,
                                    @RequestHeader("X-Timestamp") String timestamp) {
        if (!signatureService.validateSignature(body, timestamp, signature)) {
            log.warn("Invalid webhook signature");
            return ResponseEntity.badRequest().body("Invalid signature");
        }
        DingTalkWebhookEvent event = JsonUtils.parseObject(body, DingTalkWebhookEvent.class);
        log.info("Received DingTalk event: {}", event.getEventType());
        syncService.asyncProcessEvent(event);
        return ResponseEntity.ok("{\"errcode\":0,\"errmsg\":\"success\"}");
    }
}

Key considerations: signature verification, idempotent handling via event_id, and asynchronous processing (thread pool or message queue).

Real‑time sync (Message Queue)

Publish change events to a durable MQ (RabbitMQ example) and consume them with manual ACK, persistence, and dead‑letter handling to guarantee reliable delivery.

@Service
public class EventProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    public void sendUserUpdateEvent(User user) {
        UserUpdateEvent event = new UserUpdateEvent(user.getId(), user.getName(), LocalDateTime.now());
        rabbitTemplate.convertAndSend("user.sync.exchange", "user.update", event);
        log.info("Sent user update event: {}", user.getId());
    }
}

@Service
public class EventConsumer {
    @Autowired
    private UserRepository userRepository;

    @RabbitListener(queues = "user.update.queue")
    public void handle(UserUpdateEvent event) {
        log.info("Processing user update: {}", event.getUserId());
        User user = userRepository.findByExternalId(event.getUserId())
                .orElseThrow(() -> new RuntimeException("User not found"));
        user.setName(event.getUserName());
        userRepository.save(user);
    }
}

Ensure message persistence, manual ACK, and a dead‑letter queue for failures.

Choosing a solution

Use RestTemplate for simple synchronous calls, Feign when a declarative client and load‑balancing are needed in a micro‑service architecture, and WebClient for high‑concurrency, non‑blocking scenarios. Select a synchronization strategy based on data volume, latency tolerance, and consistency requirements: full batch for low‑frequency small datasets, timestamp or change‑id incremental sync for moderate change rates, and webhook or MQ for near‑real‑time propagation.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaIntegrationSpring Bootfeignresttemplatewebclientdata-sync
java1234
Written by

java1234

Former senior programmer at a Fortune Global 500 company, dedicated to sharing Java expertise. Visit Feng's site: Java Knowledge Sharing, www.java1234.com

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.