senior
Что такое асинхронные API и как их проектировать?
Асинхронные API — подходы к взаимодействию клиента и сервера, при которых обработка запроса не блокирует клиента и результат доставляется позже (через callback, polling или push-уведомление).
Когда нужны
- Долгие операции — генерация отчёта, обработка видео, массовый импорт.
- Real-time обновления — чат, биржевые котировки, уведомления.
- Событийные уведомления — платёж завершён, заказ отправлен.
- Интеграции — webhook-и от внешних систем (Stripe, GitHub).
Сравнение подходов
| Характеристика | Polling | Webhooks | SSE | WebSocket |
|---|---|---|---|---|
| Направление | Клиент -> Сервер | Сервер -> Клиент | Сервер -> Клиент | Двунаправленный |
| Протокол | HTTP | HTTP | HTTP | WS (поверх HTTP) |
| Задержка | Высокая (интервал) | Низкая | Низкая | Очень низкая |
| Нагрузка на сервер | Высокая | Низкая | Средняя | Средняя |
| Масштабирование | Простое | Простое | Сложнее | Сложнее |
| Подходит для | Долгие операции | Интеграции, события | Уведомления, ленты | Чат, real-time игры |
Паттерн 1: Polling (202 Accepted + Location)
Реализация
// Запуск долгой операции
@PostMapping("/api/reports")
public ResponseEntity<TaskStatus> createReport(
@Valid @RequestBody ReportRequest request) {
String taskId = reportService.startGeneration(request);
URI statusUri = URI.create("/api/reports/tasks/" + taskId);
TaskStatus status = new TaskStatus(taskId, "PENDING", null);
return ResponseEntity.accepted()
.location(statusUri)
.header("Retry-After", "5")
.body(status);
}
// Проверка статуса
@GetMapping("/api/reports/tasks/{taskId}")
public ResponseEntity<TaskStatus> getTaskStatus(@PathVariable String taskId) {
TaskStatus status = reportService.getStatus(taskId);
if ("COMPLETED".equals(status.status())) {
return ResponseEntity.status(HttpStatus.SEE_OTHER)
.location(URI.create(status.resultUrl()))
.body(status);
}
if ("FAILED".equals(status.status())) {
return ResponseEntity.ok(status);
}
return ResponseEntity.ok()
.header("Retry-After", "5")
.body(status);
}
public record TaskStatus(
String taskId,
String status, // PENDING, PROCESSING, COMPLETED, FAILED
String resultUrl,
Integer progress,
String errorMessage
) {
public TaskStatus(String taskId, String status, String resultUrl) {
this(taskId, status, resultUrl, null, null);
}
}
Паттерн 2: Webhooks (Callback URL)
Реализация
@PostMapping("/api/webhooks")
public ResponseEntity<WebhookRegistration> registerWebhook(
@Valid @RequestBody WebhookRequest request) {
WebhookRegistration registration = webhookService.register(request);
return ResponseEntity.status(HttpStatus.CREATED).body(registration);
}
public record WebhookRequest(
@NotBlank String url, // URL для callback
@NotEmpty Set<String> events, // ["order.completed", "payment.failed"]
String secret // секрет для подписи
) {}
@Service
@RequiredArgsConstructor
@Slf4j
public class WebhookSender {
private final RestClient restClient;
private final WebhookRepository webhookRepository;
@Async
@TransactionalEventListener
public void onOrderCompleted(OrderCompletedEvent event) {
List<WebhookRegistration> hooks = webhookRepository
.findByEvent("order.completed");
for (WebhookRegistration hook : hooks) {
sendWebhook(hook, event);
}
}
private void sendWebhook(WebhookRegistration hook, Object payload) {
String body = objectMapper.writeValueAsString(payload);
String signature = HmacUtils.hmacSha256Hex(hook.getSecret(), body);
try {
restClient.post()
.uri(hook.getUrl())
.header("X-Webhook-Signature", "sha256=" + signature)
.header("X-Webhook-Event", "order.completed")
.body(body)
.retrieve()
.toBodilessEntity();
} catch (Exception e) {
log.error("Webhook delivery failed: {}", hook.getUrl(), e);
webhookRetryService.scheduleRetry(hook, payload);
}
}
}
Паттерн 3: SSE (Server-Sent Events)
Сервер отправляет поток событий клиенту через постоянное HTTP-соединение. Однонаправленный.
Реализация
@RestController
@RequestMapping("/api/events")
public class SseController {
private final SseEmitterService sseService;
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamEvents(@RequestParam Long userId) {
SseEmitter emitter = new SseEmitter(0L);
sseService.register(userId, emitter);
emitter.onCompletion(() -> sseService.unregister(userId));
emitter.onTimeout(() -> sseService.unregister(userId));
return emitter;
}
}
@Service
public class SseEmitterService {
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
public void sendEvent(Long userId, String eventType, Object data) {
SseEmitter emitter = emitters.get(userId);
if (emitter != null) {
try {
emitter.send(SseEmitter.event()
.name(eventType)
.data(data, MediaType.APPLICATION_JSON)
.id(UUID.randomUUID().toString())
.reconnectTime(3000));
} catch (IOException e) {
emitters.remove(userId);
}
}
}
}
Паттерн 4: WebSocket (двунаправленный)
Реализация
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
@Controller
@RequiredArgsConstructor
public class ChatController {
private final SimpMessagingTemplate messagingTemplate;
@MessageMapping("/chat.send")
public void sendMessage(@Payload ChatMessage message,
SimpMessageHeaderAccessor headerAccessor) {
String userId = headerAccessor.getUser().getName();
message.setSenderId(userId);
message.setTimestamp(Instant.now());
messagingTemplate.convertAndSend(
"/topic/chat." + message.getRoomId(), message);
}
}
Частые ошибки
- Использование WebSocket там, где достаточно SSE.
- Отсутствие retry-механизма для webhook.
- Polling без
Retry-Afterзаголовка. - Хранение состояния SSE/WebSocket в памяти без учёта масштабирования.
Как используется в 2026
- Spring WebFlux + SSE — реактивный подход для потоковых данных.
- AsyncAPI 3.0 — стандарт описания асинхронных API.
- Virtual threads (Java 21+) упрощают обработку большого числа SSE/WebSocket-соединений.
- Standard Webhooks (standardwebhooks.com) — единый формат подписи и retry.
На собеседовании: нужно знать все четыре паттерна (Polling, Webhooks, SSE, WebSocket) и когда использовать какой. Polling — самый простой для долгих операций, Webhooks — для интеграций, SSE — для уведомлений, WebSocket — только для полнодуплексной связи. Частая ошибка — предлагать WebSocket для всех async-задач.