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.
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:00ZIncremental 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=12345Real‑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.
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.
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
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.
