Comunicação entre serviços: REST, gRPC ou message queue?
1. Visão geral
Quando você separa um sistema monolítico em vários serviços pequenos, a primeira pergunta é sempre: como esses serviços se comunicam? Este artigo compara as três opções mais populares — REST, gRPC e message queue (Kafka/RabbitMQ) — com exemplos de código específicos. Ao final, você saberá quando escolher cada uma delas e evitará alguns erros clássicos ao projetar a comunicação entre serviços.
2. O problema proposto
Suponha que você tenha um sistema de e-commerce com 3 serviços: OrderService, PaymentService, InventoryService. Quando OrderService recebe um pedido, ele precisa:
- Chamar
PaymentServicepara cobrar (precisa do resultado imediatamente para informar o usuário). - Chamar
InventoryServicepara debitar do estoque (pode ser processado de forma assíncrona).
Essas duas necessidades diferentes levam a dois tipos de comunicação diferentes: síncrona (REST, gRPC) e assíncrona (message queue).
3. REST — simples, popular, fácil de debugar
REST via HTTP/JSON é a escolha padrão porque praticamente todos sabem usar, e as ferramentas de debug (Postman, curl) estão disponíveis em todo lugar.
3.1. Exemplo de chamada REST com RestClient
public record PaymentRequest(String orderId, BigDecimal amount) {}
public record PaymentResponse(String transactionId, String status) {}
@Service
public class PaymentClient {
private final RestClient restClient;
public PaymentClient(RestClient.Builder builder) {
this.restClient = builder.baseUrl("http://payment-service:8081").build();
}
public PaymentResponse charge(PaymentRequest request) {
return restClient.post()
.uri("/api/payments")
.body(request)
.retrieve()
.body(PaymentResponse.class);
}
}
RestClient (Spring 6.1+) envia JSON para payment-service e recebe de volta PaymentResponse já desserializado. Se payment-service retornar 4xx/5xx, retrieve() lançará RestClientException — você deve capturar esse erro na classe chamadora.
3.2. Vantagens e desvantagens
- Vantagens: fácil de aprender, fácil de testar com curl/Postman, suportado em todas as linguagens, permite aproveitar HTTP caching, load balancer, API gateway.
- Desvantagens: payload JSON é mais pesado que binário, não há contrato rigoroso (fácil divergir fields entre cliente/servidor), latência maior que gRPC por causa de parsing de texto e overhead HTTP/1.1.
Observação: REST é adequado quando o número de requisições não é muito grande e você precisa da capacidade de debugar rapidamente a olho nu. Para sistemas internos que fazem dezenas de milhares de requisições/segundo, REST JSON pode ser um gargalo.
4. gRPC — alto desempenho, contrato rigoroso
gRPC usa Protocol Buffers (binário) e HTTP/2, adequado para comunicação interna entre serviços que precisam de velocidade e contrato claro.
4.1. Declaração do proto
syntax = "proto3";
package payment;
option java_package = "dev.nonla.payment.grpc";
option java_multiple_files = true;
service PaymentGrpcService {
rpc Charge (ChargeRequest) returns (ChargeResponse);
}
message ChargeRequest {
string order_id = 1;
double amount = 2;
}
message ChargeResponse {
string transaction_id = 1;
string status = 2;
}
Este arquivo .proto gera um stub Java para servidor e cliente — este é o “contrato” rigoroso que REST não tem por padrão.
4.2. Dependência Maven
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>
A biblioteca grpc-spring-boot-starter ajuda a integrar o servidor/cliente gRPC no Spring Boot sem precisar gerenciar manualmente o Server.
4.3. Implementação do servidor
@GrpcService
public class PaymentGrpcServer extends PaymentGrpcServiceGrpc.PaymentGrpcServiceImplBase {
@Override
public void charge(ChargeRequest request, StreamObserver<ChargeResponse> responseObserver) {
ChargeResponse response = ChargeResponse.newBuilder()
.setTransactionId(UUID.randomUUID().toString())
.setStatus("SUCCESS")
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
@GrpcService registra automaticamente o servidor no runtime gRPC. onNext envia a resposta, onCompleted fecha o stream — é obrigatório chamar, se esquecer o cliente ficará esperando para sempre.
4.4. Cliente chamando gRPC
@Service
public class PaymentGrpcClient {
@GrpcClient("payment-service")
private PaymentGrpcServiceGrpc.PaymentGrpcServiceBlockingStub stub;
public ChargeResponse charge(String orderId, double amount) {
ChargeRequest request = ChargeRequest.newBuilder()
.setOrderId(orderId)
.setAmount(amount)
.build();
return stub.charge(request);
}
}
BlockingStub faz uma chamada síncrona, retornando o resultado diretamente — código mais conciso que REST porque não precisa fazer desserialização JSON manualmente.
4.5. Vantagens e desvantagens
- Vantagens: binário pequeno, 2–5 vezes mais rápido que JSON; multiplexing HTTP/2; contrato
.protoforça cliente/servidor a sincronizar schema; suporta streaming bidirecional. - Desvantagens: difícil debugar manualmente (não pode ler o payload diretamente), requer ferramentas específicas (grpcurl, BloomRPC), não é adequado chamar diretamente de um navegador (precisa de grpc-web), o time precisa aprender proto.
Observação: não use gRPC para uma API pública voltada ao frontend web sem infraestrutura de grpc-web. gRPC é mais adequado para comunicação interna entre serviços.
5. Message queue — assíncrona, boa tolerância a falhas
Quando OrderService não precisa esperar InventoryService responder imediatamente, message queue (Kafka, RabbitMQ) é a escolha razoável: envia uma mensagem e continua processando, o consumer processa quando estiver pronto.
5.1. Dependência Maven
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
5.2. Producer — OrderService publicando evento
public record StockReservedEvent(String orderId, String sku, int quantity) {}
@Service
public class InventoryEventPublisher {
private final KafkaTemplate<String, StockReservedEvent> kafkaTemplate;
public InventoryEventPublisher(KafkaTemplate<String, StockReservedEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void publishStockReserved(StockReservedEvent event) {
kafkaTemplate.send("stock-reserved-topic", event.orderId(), event);
}
}
kafkaTemplate.send envia o evento para o tópico stock-reserved-topic, usando orderId como chave para garantir que todos os eventos do mesmo pedido fiquem na mesma partição (mantendo a ordem de processamento).
5.3. Consumer — InventoryService escutando
@Component
public class StockReservedListener {
@KafkaListener(topics = "stock-reserved-topic", groupId = "inventory-service")
public void onStockReserved(StockReservedEvent event) {
log.info("Reservando {} unidades de {} para o pedido {}",
event.quantity(), event.sku(), event.orderId());
// debitar do estoque, processar de forma idempotente por orderId
}
}
@KafkaListener automaticamente sonda por novas mensagens do tópico. Se InventoryService ficar temporariamente inativo, Kafka mantém a mensagem — nenhum dado é perdido, o consumer continua processando quando se recupera.
5.4. Vantagens e desvantagens
- Vantagens: desacoplamento forte (o produtor não precisa saber se o consumidor está vivo ou não), boa tolerância a falhas (as mensagens são retidas), fácil de escalar por partição, suporta arquitetura orientada a eventos.
- Desvantagens: sem resposta imediata (precisa projetar callback/polling se precisar do resultado), debug do fluxo de dados é mais complexo (precisa rastrear vários tópicos), requer infraestrutura de broker separada (Kafka/RabbitMQ) para operar e monitorar.
Observação: não use message queue quando precisar de resposta imediata para o usuário (por exemplo, confirmação de pagamento bem-sucedido/falhado) — a latência assíncrona piorará a experiência do usuário.
6. Comparação rápida das três abordagens
| Critério | REST | gRPC | Message Queue |
|---|---|---|---|
| Tipo de comunicação | Síncrona | Síncrona | Assíncrona |
| Formato | JSON (texto) | Protobuf (binário) | Geralmente JSON/Avro |
| Velocidade | Média | Alta | Não se aplica diretamente (throughput alto) |
| Contrato | Flexível (OpenAPI opcional) | Rigoroso (.proto) |
Flexível (schema registry se houver) |
| Debug | Fácil (curl, Postman) | Mais difícil | Mais difícil (precisa rastrear vários consumers) |
| Tolerância a falhas quando o serviço está offline | Falha imediatamente | Falha imediatamente | Mensagem aguarda, não é perdida |
| Adequado para | API pública, CRUD simples | Comunicação interna, alto desempenho | Event-driven, processamento em background |
7. Escolher qual comunicação para qual situação
Voltando ao exemplo OrderService:
@Service
public class OrderProcessingService {
private final PaymentGrpcClient paymentClient; // precisa do resultado imediatamente, interno, alto desempenho
private final InventoryEventPublisher inventoryPublisher; // não precisa esperar, processamento em background
public OrderProcessingService(PaymentGrpcClient paymentClient,
InventoryEventPublisher inventoryPublisher) {
this.paymentClient = paymentClient;
this.inventoryPublisher = inventoryPublisher;
}
public OrderResult processOrder(Order order) {
ChargeResponse charge = paymentClient.charge(order.id(), order.amount());
if (!"SUCCESS".equals(charge.getStatus())) {
return OrderResult.failed(order.id());
}
inventoryPublisher.publishStockReserved(
new StockReservedEvent(order.id(), order.sku(), order.quantity()));
return OrderResult.accepted(order.id(), charge.getTransactionId());
}
}
processOrder chama gRPC para saber imediatamente se o pagamento foi bem-sucedido ou não (obrigatoriamente síncrono), depois apenas publica o evento para debitar do estoque — porque o usuário não precisa esperar por essa etapa, e se InventoryService ficar temporariamente fora, o Kafka ainda mantém a mensagem segura.
Regras gerais para escolher:
- Precisa de resultado imediatamente, comunicação interna, frequência alta → gRPC.
- Precisa de resultado imediatamente, comunicação com terceiros ou time não familiarizado com gRPC → REST.
- Não precisa de resultado imediatamente, precisa de tolerância a falhas e desacoplamento → message queue.
8. Escrever testes para a lógica de escolha do fluxo de processamento
@ExtendWith(MockitoExtension.class)
class OrderProcessingServiceTest {
@Mock PaymentGrpcClient paymentClient;
@Mock InventoryEventPublisher inventoryPublisher;
@InjectMocks OrderProcessingService orderProcessingService;
@Test
void shouldPublishStockEvent_whenPaymentSucceeds() {
Order order = new Order("order-1", "SKU-01", 2, new BigDecimal("100.00"));
ChargeResponse success = ChargeResponse.newBuilder()
.setTransactionId("tx-1")
.setStatus("SUCCESS")
.build();
given(paymentClient.charge(order.id(), order.amount())).willReturn(success);
OrderResult result = orderProcessingService.processOrder(order);
assertThat(result.status()).isEqualTo("ACCEPTED");
verify(inventoryPublisher).publishStockReserved(
new StockReservedEvent("order-1", "SKU-01", 2));
}
@Test
void shouldNotPublishStockEvent_whenPaymentFails() {
Order order = new Order("order-2", "SKU-02", 1, new BigDecimal("50.00"));
ChargeResponse failed = ChargeResponse.newBuilder().setStatus("FAILED").build();
given(paymentClient.charge(order.id(), order.amount())).willReturn(failed);
OrderResult result = orderProcessingService.processOrder(order);
assertThat(result.status()).isEqualTo("FAILED");
verifyNoInteractions(inventoryPublisher);
}
}
O primeiro teste verifica se, quando o pagamento for bem-sucedido, InventoryEventPublisher.publishStockReserved é chamado com os parâmetros corretos. O segundo teste verifica se, quando o pagamento falha, o sistema não publica o evento de débito — verifyNoInteractions garante que não haja efeitos colaterais indesejados.
9. Erros comuns ao combinar as três
- Abuso de REST síncrono para tudo: leva a um “monólito distribuído” — serviço A chama B, B chama C, um serviço lento causa timeout em toda a cadeia.
- Usar gRPC mas não versionar o proto: mudar fields no
.protosem manter compatibilidade reversa (deletar field antigo em vez de deprecar) faz clients antigos quebrar. - Publicar mensagens mas não processar de forma idempotente: Kafka pode reenviar mensagens quando o consumer tenta novamente — se
InventoryServicedebita do estoque duas vezes para o mesmoorderIdé um bug. - Misturar síncrono/assíncrono sem clareza: para facilitar a manutenção do código, nomes de método/classe devem deixar claro o tipo de comunicação, por exemplo
publishXxxEvent(assíncrono) diferente defetchXxx/chargeXxx(síncrono).
10. Conclusão
Não há uma abordagem “melhor” para todos os casos — REST é adequado quando você precisa de simplicidade e debug fácil, gRPC é adequado quando você precisa de velocidade e contrato rigoroso para comunicação interna, message queue é adequado quando você precisa de desacoplamento e tolerância a falhas para fluxos que não precisam de resposta imediata. Na prática, um sistema maduro de microservices geralmente usa os três simultaneamente, cada abordagem para exatamente o tipo de comunicação para o qual é mais adequada — como o exemplo OrderService acima demonstrou.