Resumo
Este artigo analisa estratégias de comunicação entre serviços Spring Boot, discutindo critérios para a escolha entre chamadas síncronas e mecanismos orientados a eventos. Abordam-se ainda formas de integrar serviços de inteligência artificial (IA) sem acoplá-los ao restante da aplicação. São examinados Spring Cloud OpenFeign, Service Discovery, Spring Cloud LoadBalancer, webhooks, mensageria e mecanismos de resiliência, com exemplos derivados de uma aplicação SaaS em desenvolvimento.
1. Introdução
À medida que uma aplicação cresce e suas responsabilidades são separadas, torna-se necessário definir como os serviços se comunicarão. A criação de projetos Spring Boot independentes é relativamente simples. A dificuldade surge quando um serviço depende de dados ou regras pertencentes a outro, por exemplo ao consultar informações de usuários, executar uma regra de negócio ou acessar uma API externa. Surge também a necessidade de processar tarefas demoradas sem manter uma requisição HTTP aberta durante todo o processamento.
Nesse contexto, a comunicação entre serviços deixa de ser um detalhe de implementação e passa a ser uma decisão arquitetural. Este trabalho discute o tema com ênfase em Spring Cloud OpenFeign, Service Discovery, Spring Cloud LoadBalancer, chamadas HTTP síncronas, webhooks, eventos e processamento assíncrono. Apresenta-se também sua aplicação em um SaaS real, no qual uma camada de IA é isolada do backend principal.
2. O desafio da localização de serviços
Uma arquitetura monolítica pode ser representada de forma simples. Com a separação de responsabilidades, a API principal passa a depender de múltiplos serviços, como autenticação, IA, pagamentos e notificações. Surge então a questão central: como a API principal localiza e invoca esses serviços?
A alternativa mais simples é registrar endereços fixos no código:
http://localhost:8082
http://10.0.4.27:8082
Essa abordagem funciona até que ocorra alguma mudança de infraestrutura, como a alteração da instância, a execução de múltiplas réplicas, a migração de ambiente ou a adoção de Docker ou Kubernetes. Nesses casos, o endereço físico embutido no código torna-se uma fonte de acoplamento e de manutenção onerosa. O Service Discovery foi concebido para solucionar esse problema.
3. Service Discovery
No Service Discovery, o consumidor deixa de perguntar "onde está o serviço?" e passa a indicar "qual serviço é necessário?". No Spring Cloud, isso se expressa da seguinte forma:
@FeignClient(name = "ai-service")
O código torna-se independente do endereço concreto da instância (localhost:8082, 10.0.2.15:8082 ou ai-service:8082), que é resolvido pela infraestrutura de descoberta. Um exemplo clássico no ecossistema Spring é o Netflix Eureka, que mantém um catálogo dos serviços registrados:
ai-service
├── 10.0.1.20:8082
└── 10.0.1.21:8082
payment-service
└── 10.0.1.30:8083
A aplicação conhece apenas o nome lógico do serviço, que passa a representar sua identidade.
4. Spring Cloud OpenFeign
O Spring Cloud OpenFeign permite representar chamadas HTTP como interfaces Java. Sem esse recurso, é necessário gerenciar manualmente URL, cabeçalhos, serialização, desserialização, métodos HTTP e tratamento de respostas:
restClient
.post()
.uri("http://ai-service/api/process")
.body(request)
.retrieve()
.body(Response.class);
Com o OpenFeign, o contrato é declarado de forma explícita:
@FeignClient(name = "ai-service")
public interface AiServiceClient {
@PostMapping("/api/v1/ai/process")
AiResponse process(@RequestBody AiRequest request);
}
private final AiServiceClient aiServiceClient;
public void execute(AiRequest request) {
AiResponse response = aiServiceClient.process(request);
}
A chamada HTTP permanece, mas passa a ser expressa como um contrato Java.
Destaca-se a diferença entre os atributos name e url. Ao declarar @FeignClient(name = "ai-service", url = "http://localhost:8082"), determina-se que o endereço especificado seja chamado diretamente. Ao declarar apenas name, solicita-se ao mecanismo de descoberta que localize uma instância disponível do serviço. Embora a diferença sintática seja pequena, o impacto sobre o acoplamento é substancial.
5. Comunicação síncrona
O OpenFeign é particularmente adequado à comunicação síncrona, na qual a etapa seguinte depende da resposta anterior:
UserDTO user = userClient.findById(userId);
if (!user.isActive()) {
throw new BusinessException("Usuário inativo");
}
continueProcessing();
Nesse cenário, a conversão da chamada em evento não traria benefício evidente.
A comunicação síncrona torna-se problemática quando a operação envolve múltiplas etapas demoradas. Considere-se um endpoint POST /process-audio que deve:
(1) enviar o arquivo a um modelo de IA;
(2) aguardar a transcrição;
(3) interpretar o texto;
(4) consultar outro serviço;
(5) executar uma operação;
(6) gerar áudio;
(7) retornar o resultado.
A cadeia resultante é composta por chamadas síncronas encadeadas. Se cada serviço levar alguns segundos, a requisição original permanece aberta durante todo o processo. Torna-se, então, necessário avaliar se a operação realmente exige comportamento síncrono.
6. Comunicação assíncrona: webhooks e brokers
Comunicação síncrona (REST, OpenFeign, RestClient, WebClient) é adequada quando o resultado é necessário de imediato. Na comunicação assíncrona, o serviço emissor publica um evento em um message broker (RabbitMQ, Kafka, AWS SQS, Google Pub/Sub, entre outros) e prossegue sem aguardar o processamento pelo serviço receptor.
Webhooks constituem outra forma de comunicação baseada em HTTP, com lógica distinta. Em vez de o cliente consultar repetidamente o estado da tarefa (polling), o serviço responsável notifica o cliente ao término do processamento . Para uma tarefa de 20 segundos, o polling exige sucessivas chamadas GET /jobs/123. O webhook, por sua vez, envia o resultado por meio de POST /webhooks/jobs/123 quando este estiver disponível. Essa abordagem é vantajosa quando o processamento é demorado, quando o consumidor não deve permanecer bloqueado, quando há uma fronteira clara entre os serviços e quando o produtor é capaz de notificar o consumidor.
É necessário, porém, distinguir webhooks de message brokers. O webhook permanece uma comunicação HTTP direta entre dois serviços. O broker introduz uma camada intermediária que oferece persistência de mensagens, múltiplos consumidores, retry, reprocessamento, desacoplamento temporal e controle de entrega. Assim, o webhook pode ser uma solução simples para determinadas integrações, ao passo que o broker é mais indicado para arquiteturas orientadas a eventos.
7. Arquitetura híbrida
Os modelos síncrono e assíncrono não são excludentes e podem coexistir. A escolha deve considerar a natureza de cada operação:
- resposta necessária imediatamente: OpenFeign;
- continuidade possível sem a resposta: evento/fila;
- notificação de conclusão por sistema externo: webhook.
8. Integração de serviços de inteligência artificial
Uma aplicação moderna pode contar com um serviço especializado em IA. Nesse desenho, a API principal permanece responsável pelas regras de negócio, enquanto o serviço de IA interpreta linguagem, processa áudio, gera texto e respostas e integra provedores de modelos. Essa separação cria uma fronteira bem definida. Por exemplo, a instrução "Crie uma despesa de R$ 40 no iFood" é interpretada pelo serviço de IA, convertida em um CreateExpenseCommand e encaminhada à API principal, que aplica a regra de negócio e persiste o dado no PostgreSQL. O serviço de IA não acessa o banco de dados nem conhece a forma de persistência: limita-se a interpretar a intenção.
Entre os provedores possíveis, o Google Gemini mostra-se adequado a esse tipo de aplicação, sobretudo pela facilidade de integração via API e pela capacidade de processar diferentes tipos de entrada . O aspecto essencial, contudo, não é a escolha do provedor, mas evitar que o código de negócio dependa diretamente dele. A aplicação deve expressar a necessidade de "interpretar esta entrada", e não a de "chamar a API X neste ponto da regra de negócio".
O Spring AI contribui para esse objetivo ao oferecer abstrações, como o ChatClient, que separam a configuração do modelo/provedor da lógica da aplicação:
String response = chatClient
.prompt()
.user("Analise esta mensagem...")
.call()
.content();
Essa abordagem organiza a integração e torna menos invasiva a troca ou a experimentação de modelos, pois o código da aplicação não precisa lidar com os detalhes de HTTP, autenticação e formato de payload de cada provedor.
A IA também pode operar em fluxos assíncronos. No processamento de áudio, em vez de o usuário aguardar a resposta completa, a API registra um job em uma fila e retorna 202 Accepted com um jobId. O serviço de IA processa a tarefa, consulta o Gemini e notifica a API por webhook ou evento, e a API, por fim, notifica o usuário.
9. Limites do OpenFeign e mecanismos de resiliência
O OpenFeign resolve adequadamente a necessidade de invocar outro serviço HTTP, mas não é adequado a processamentos de longa duração. Tampouco resolve automaticamente idempotência, consistência distribuída, retry seguro, circuit breaker, filas, eventos, observabilidade ou transações distribuídas. Trata-se de uma ferramenta de comunicação, e não de uma arquitetura completa.
9.1 Retry e idempotência. Considere-se um POST /payments invocado por um cliente Feign. Se a requisição alcançar o servidor e o pagamento for criado, mas a resposta se perder, o cliente pode interpretar o evento como falha. Um retry automático geraria, nesse caso, um segundo pagamento. Chamadas que alteram estado devem, portanto, ser analisadas com cautela antes de receberem retry automático. Uma solução recorrente é o uso de chaves de idempotência (Idempotency-Key: 8f3c...), que permitem ao servidor reconhecer operações já processadas.
9.2 Timeout. O tempo máximo de espera deve ser definido para cada dependência. Uma API interna pode operar com connect timeout de 2 s e read timeout de 5 s, enquanto uma chamada a um serviço de IA pode requerer 5 s e 60 s, respectivamente. Valores excessivos retêm recursos, e valores insuficientes convertem operações normais em erros. O timeout é, portanto, parte do contrato da dependência, e não apenas um parâmetro de configuração.
9.3 Circuit Breaker. Se o Gemini estiver indisponível e a API persistir em novas tentativas, sucessivos timeouts e retries podem amplificar o problema. O circuit breaker altera esse comportamento por meio de três estados: CLOSED, no qual as chamadas ocorrem normalmente; OPEN, alcançado após muitas falhas e no qual as requisições são rejeitadas rapidamente; e HALF-OPEN, no qual uma chamada de teste é realizada e, se bem-sucedida, restaura o estado CLOSED. Isso evita sobrecarregar uma dependência já em falha.
10. Visão integrada
A Figura apresenta a arquitetura completa, e a Tabela 1 sintetiza as estratégias discutidas.
Tabela 1. Estratégias de comunicação segundo a situação.
| Situação | Estratégia |
|---|---|
| API precisa da resposta imediatamente | OpenFeign |
| Serviço precisa localizar outra instância | Service Discovery |
| Operação demorada | Assíncrono |
| Sistema externo precisa notificar a conclusão | Webhook |
| Vários consumidores precisam receber o evento | Message Broker |
| Integração com LLM | Spring AI |
| Dependência instável | Timeout + Circuit Breaker |
| Operação passível de repetição | Idempotência |
11. Critérios de decisão
Com base na experiência de desenvolvimento de uma aplicação SaaS, a escolha da estratégia de comunicação pode ser orientada por quatro perguntas:
- A resposta é necessária de imediato? Em caso afirmativo, utiliza-se HTTP/OpenFeign.
- É possível prosseguir sem essa resposta? Em caso afirmativo, utiliza-se evento/fila.
- Quem saberá quando o processamento terminar? Se for outro sistema, utiliza-se webhook.
- A operação pode ser executada mais de uma vez? Em caso afirmativo, deve-se garantir idempotência.
A última pergunta é particularmente relevante em sistemas distribuídos, nos quais falhas parciais passam a integrar o comportamento normal do sistema.
12. Implicações da transição de um monólito
A principal mudança na transição de um monólito não é a existência de múltiplos projetos Spring Boot, mas a aceitação de falhas intermediárias. Em um monólito, o fluxo é linear (Controller → Service → Repository → Database). Em uma arquitetura distribuída, o fluxo atravessa múltiplas fronteiras de rede (Controller → Service A → HTTP → Service B → HTTP → Service C), e cada etapa pode falhar de forma independente. Os serviços A e B podem estar operacionais e, ainda assim, a rede entre eles pode estar indisponível. Esse tipo de problema raramente é evidenciado quando os microsserviços são estudados apenas por meio de diagramas.
13. Conclusão
A adoção de microsserviços não se resume à divisão de uma aplicação em vários projetos. A existência de múltiplos processos exige atenção a comunicação, descoberta de serviços, timeout, falhas, idempotência e consistência. O Spring Cloud oferece ferramentas relevantes nesse sentido: o OpenFeign simplifica as chamadas HTTP entre serviços, o Service Discovery elimina o acoplamento a endereços físicos, o LoadBalancer viabiliza o uso de múltiplas instâncias, webhooks e eventos retiram operações demoradas do caminho síncrono, e o Spring AI facilita a integração com modelos de IA, incluindo o Gemini. Os mecanismos de resiliência, por sua vez, permitem lidar com o fato inevitável de que, em um sistema distribuído, falhas ocorrerão.
Conclui-se que a escolha entre comunicação síncrona, assíncrona, OpenFeign, webhooks ou mensageria deve ser orientada pela dependência entre as etapas do negócio, e não pela sofisticação aparente da tecnologia. Se a etapa seguinte depende da resposta, a chamada síncrona é adequada. Se pode aguardar, um evento tende a ser preferível. Se um sistema externo notifica a conclusão, o webhook pode simplificar a arquitetura.
Por fim, os microsserviços não eliminam o acoplamento: eles o transformam de acoplamento de código em acoplamento de comunicação, o que exige que essa comunicação seja projetada de forma deliberada.
Top comments (0)