Once we have decomposed the application into autonomous services, an inevitable question arises: how do they communicate with each other and how does the outside world find them? Communication is the nervous system of a distributed architecture, and choosing the wrong style (synchronous versus asynchronous) or infrastructure (gateway, service discovery) can turn a set of elegant services into a fragile distributed monolith.

In this lesson we will compare synchronous communication (REST, gRPC) with asynchronous communication (messaging, events), understand the role of the API Gateway as an entry point, and see how service discovery allows services to locate each other in a dynamic environment.

Contents

  1. Synchronous versus asynchronous
  2. Synchronous communication: REST and gRPC
  3. Asynchronous communication: messaging and events
  4. API Gateway
  5. Service Discovery
  6. Choosing the right style
  7. Common mistakes and tips
  8. Exercises
  9. Conclusion

  1. Synchronous versus asynchronous

The first major decision is whether the caller waits for the response (synchronous) or continues without waiting (asynchronous).

Aspect Synchronous (REST/gRPC) Asynchronous (messages/events)
Model Request/response Publish/subscribe or queue
Temporal coupling High: both must be alive Low: decoupled in time
Simplicity Higher (direct flow) Lower (more moving parts)
Availability If B goes down, A is affected A keeps going even if B is down
Consistency Immediate Eventual
Typical cases Queries, immediate validations Notifications, integration, events

The key idea of temporal coupling: in a synchronous call, the target service must be available at that instant. In an asynchronous one, the message waits in a queue until the destination can process it.

  1. Synchronous communication: REST and gRPC

REST over HTTP is the de facto standard: simple, universal, based on resources and HTTP verbs.

// REST client with explicit timeout (never trust the network)
public Customer getCustomer(String id) {
    return webClient.get()
            .uri("/customers/{id}", id)
            .retrieve()
            .bodyToMono(Customer.class)
            .timeout(Duration.ofSeconds(2)) // cuts off if it takes too long
            .block();
}

The timeout is essential: without it, a slow service would block the caller indefinitely (remember the fallacy "latency is zero").

gRPC uses HTTP/2 and binary serialization (Protocol Buffers), which makes it faster and more compact, ideal for high-performance internal communication.

// Contract definition in Protocol Buffers (customers.proto)
syntax = "proto3";

service CustomerService {
  rpc GetCustomer (CustomerRequest) returns (CustomerResponse);
}

message CustomerRequest { string id = 1; }
message CustomerResponse { string id = 1; string name = 2; }

From this contract, gRPC automatically generates the client and server code. A quick comparison:

Criterion REST/JSON gRPC/Protobuf
Format Text (JSON) Binary
Performance Good Excellent
Readability High Low (binary)
Browsers Native support Limited
Streaming Limited Native (bidirectional)
Typical use Public APIs Internal communication

  1. Asynchronous communication: messaging and events

In asynchronous communication, a service publishes a message on a broker (RabbitMQ, Apache Kafka, etc.) and others consume it when they can. We distinguish:

  • Commands: a request for something to happen ("SendReceipt"). They usually have a single recipient.
  • Events: a notification of something that already happened ("PolicyCreated"). They can have many subscribers.
{
  "type": "PolicyCreated",
  "version": 1,
  "occurredAt": "2026-06-30T10:15:00Z",
  "data": {
    "policyId": "POL-00123",
    "line": "home"
  }
}

This event describes a fait accompli. The billing service can subscribe to generate the first receipt, and the notifications service can subscribe to send a welcome email, without the policies service knowing anything about them. This drastically reduces coupling.

// Event consumer: reacts to PolicyCreated
@KafkaListener(topics = "policies")
public void onPolicyCreated(PolicyCreatedEvent event) {
    // Idempotent processing: if we already processed it, we exit
    if (alreadyProcessed(event.getId())) return;
    billing.generateFirstReceipt(event.getPolicyId());
    markProcessed(event.getId());
}

Idempotency is critical because brokers usually guarantee "at least once" delivery, which means a message may arrive duplicated.

  1. API Gateway

The API Gateway is the single entry point for external clients. Instead of the frontend knowing the address of each microservice, it talks only to the gateway, which routes and aggregates.

graph LR
    APP[Mobile App] --> GW[API Gateway]
    WEB[Web] --> GW
    GW --> S1[Policies Service]
    GW --> S2[Customers Service]
    GW --> S3[Claims Service]

Typical responsibilities of the gateway:

Function Description
Routing Directs each request to the correct service.
Authentication Validates tokens before passing to the backend.
Rate limiting Protects against abuse (rate limiting).
Aggregation Combines responses from several services into one.
TLS Terminates encryption at a single point.
# Route configuration in an API Gateway (Spring Cloud Gateway style)
spring:
  cloud:
    gateway:
      routes:
        - id: policies
          uri: lb://policies-service   # lb = load balancing via service discovery
          predicates:
            - Path=/api/policies/**
        - id: customers
          uri: lb://customers-service
          predicates:
            - Path=/api/customers/**

Each route associates a URL pattern (predicates) with a target service (uri). The lb:// prefix indicates that the specific address is resolved through service discovery, not hardcoded by hand.

Beware: the gateway must not contain business logic. If you turn it into a central brain, you are back to the monolith.

  1. Service Discovery

In the cloud, service instances appear and disappear continuously (autoscaling, deployments, failures). Their IP addresses are not fixed. Service discovery solves the problem: a central registry where services register themselves and look each other up by name.

sequenceDiagram
    participant S as Policies Service
    participant R as Registry (Discovery)
    participant C as Client Service
    S->>R: I register: policies -> 10.0.0.5:8080
    C->>R: Where is "policies"?
    R->>C: 10.0.0.5:8080
    C->>S: HTTP request

There are two models:

Model Description Example
Client-side The client queries the registry and picks an instance. Eureka + Ribbon
Server-side A load balancer resolves for you. Kubernetes Services

In Kubernetes, discovery is built in: each service has a stable DNS name and the cluster balances automatically.

# In Kubernetes, "policies-service" is a DNS name resolvable from other pods
apiVersion: v1
kind: Service
metadata:
  name: policies-service
spec:
  selector:
    app: policies
  ports:
    - port: 8080

With this definition, any pod can call http://policies-service:8080 and Kubernetes routes to a healthy instance, without anyone managing IPs.

  1. Choosing the right style

A practical guide for deciding:

  • Do you need the response now to continue (validate a data item, show a screen)? → Synchronous.
  • Are you notifying something that already happened, to several interested parties? → Asynchronous (events).
  • Do you want the target service to be able to be down without affecting you? → Asynchronous.
  • Is the operation a simple, low-latency query? → Synchronous (internal gRPC or REST).

The general principle: prefer asynchronous to reduce coupling, and reserve synchronous for queries that really need an immediate response.

Common Mistakes and Tips

  • Chained synchronous calls: A calls B, which calls C, which calls D. Latency and the probability of failure accumulate. Consider events.
  • Forgetting timeouts: every synchronous call must have a timeout. Without it, a slow service drags you down.
  • Putting business logic in the gateway: the gateway routes and protects, it doesn't decide. The logic lives in the services.
  • Assuming exactly-once delivery: brokers deliver "at least once". Make consumers idempotent.
  • Hardcoding IPs by hand: use service discovery; addresses change constantly.

Exercises

  1. Compare synchronous and asynchronous communication in a table in terms of temporal coupling, availability, and consistency.
  2. Explain why an event consumer must be idempotent and show in Java pseudocode how to achieve it.
  3. You have a screen that needs to show, in a single call, the customer's data and their policies, which live in different services. Which piece of the architecture would you use and how?

Solutions

  1. Synchronous: high temporal coupling (both must be alive), availability dependent on the destination, immediate consistency. Asynchronous: low temporal coupling (the message waits in a queue), independent availability, eventual consistency.

  2. It must be idempotent because brokers deliver "at least once", so a message may arrive duplicated. Solution:

public void onEvent(Event e) {
    if (alreadyProcessed(e.getId())) return; // discard duplicates
    process(e);
    markProcessed(e.getId());
}
  1. The API Gateway with an aggregation pattern (or a BFF, Backend For Frontend): the gateway receives the request, calls the customers service and the policies service in parallel, and combines both responses into a single JSON that it returns to the screen. This way the frontend makes a single call.

Conclusion

We have learned that communication between services moves between two poles: synchronous (REST, gRPC), simple but coupled in time, and asynchronous (events, messages), decoupled but eventually consistent. The API Gateway provides a single entry point that routes, protects, and aggregates, while service discovery allows services to find each other in a dynamic environment.

All this network communication shares one risk: dependencies fail. The next lesson, Resilience Patterns: Circuit Breaker, Retry, and Bulkhead, will teach us to design services that survive partial failures instead of propagating them.

Application Architecture Course

Module 1: Fundamentals of Application Architecture

Module 2: Design Principles and Tactics

Module 3: Architectural Styles and Patterns

Module 4: Distributed Architectures and Microservices

Module 5: Event-Driven Architectures and Messaging

Module 6: Domain-Driven Design (DDD)

Module 7: Data and Persistence

Module 8: Cloud Architecture and Deployment

Module 9: Quality, Security and Observability

Module 10: Evolution, Governance and Case Studies

© Copyright 2026. All rights reserved