Home

Service Communication

Li

Li Wei

December 27, 202511 min read

Title: Service Communication

Overview

Once microservices are split, they inevitably need to call each other. Currently our inter‑service calls all use OpenFeign. In this style, the caller sends a request and waits for the provider to finish processing and return a result before continuing. In other words, the caller is blocked during the call, which we refer to as synchronous calling or synchronous communication. In many scenarios, however, we may want to use asynchronous communication.

Interpretation:

  • Synchronous communication: A service initiates a call and waits for it to complete and return a result, blocking the current thread.
  • Asynchronous communication: The caller sends a request and does not wait for the result immediately, but continues with other work.

If our business requires an immediate response from the provider, we should choose synchronous communication (synchronous call). If we aim for higher efficiency and do not need an immediate response, we should choose asynchronous communication (asynchronous call).

For asynchronous calls, the mechanism is based on message notification and generally involves three roles:

  • Message sender: The entity that posts the message – the original caller.
  • Message broker: Manages, stores temporarily, and forwards messages; you can think of it as a WeChat server.
  • Message receiver: The entity that receives and processes the message – the original service provider.

In an asynchronous call, the sender no longer calls the receiver’s business interface directly. Instead, it posts a message to the broker. The receiver then subscribes to messages from the broker as needed. Whenever the sender posts a message, the receiver can fetch and process it.

Thus, the sender and receiver are completely decoupled.

Pros and cons of asynchronous calls:

  • Advantages
    • Lower coupling
    • Better performance
    • Stronger business scalability
    • Fault isolation, preventing cascading failures
  • Disadvantages
    • Cannot obtain the call result immediately; timeliness is poorer
    • Fully dependent on the broker’s reliability, security, and performance
    • Architecture is more complex; maintenance and debugging are harder

Technology selection

Comparison of several common MQs:

RabbitMQ ActiveMQ RocketMQ Kafka
Company/Community Rabbit Apache Alibaba Apache
Development language Erlang Java Java Scala & Java
Protocol support AMQP, XMPP, SMTP, STOMP OpenWire, STOMP, REST, XMPP, AMQP Custom protocol Custom protocol
Availability Medium High High High
Single‑node throughput Medium Poor High Very high
Message latency Microseconds Milliseconds Milliseconds < millisecond
Message reliability High Medium High Medium

When to choose

  • Prioritize availability: Kafka, RocketMQ, RabbitMQ
  • Prioritize reliability: RabbitMQ, RocketMQ
  • Prioritize throughput: RocketMQ, Kafka
  • Prioritize low latency: RabbitMQ, Kafka

RabbitMQ

(Download and open with XMind)

Basic Introduction

RabbitMQ is an open‑source message‑oriented middleware developed in Erlang.

Official website:

Roles in RabbitMQ:

  • publisher: Producer
  • consumer: Consumer
  • exchange: Exchange, responsible for routing messages
  • queue: Queue, stores messages
  • virtualHost: Virtual host, isolates exchanges, queues, and messages of different tenants

Basic architecture:

Installation & Configuration

Example using a Docker container:

  • Pull the image
  • Run the container
  • 15672: Port of RabbitMQ’s management console
  • 5672: Message publishing endpoint

After installation, visit http://主机地址:15672 to see the management console.

Note: Sending/receiving messages via the console is not demonstrated here; see the Black Horse (Heima) documentation for details.

Getting Started Demo

Publisher implementation – idea:

  • Establish a connection
  • Create a channel
  • Declare a queue
  • Publish a message
  • Close the channel and connection

Code implementation:

Consumer implementation – idea:

  • Establish a connection
  • Create a channel
  • Declare a queue
  • Subscribe to messages

Code implementation:

Spring AMQP

Basic introduction
In the demo you can see that using a message queue for send/receive results in a lot of boilerplate code—much like the early days of JDBC. Fortunately, JDBC later got a well‑designed API that made it simple, and RabbitMQ enjoys the same benefit because it follows the AMQP protocol, which is language‑agnostic. Any language that implements AMQP can interact with RabbitMQ.

AMQP (Advanced Message Queuing Protocol) is an open standard for asynchronous message transfer. It defines how producers, consumers, and brokers communicate and the format of messages. AMQP provides reliable delivery, routing, queues, exchanges, and other features that enable flexible, dependable communication between disparate systems.

Key AMQP concepts:

  • Producer: Sends messages to a queue or exchange.
  • Consumer: Receives messages from a queue or exchange.
  • Message broker: Middleware that receives, stores, routes, and forwards messages; typically includes queues and exchanges.
  • Queue: Buffer that stores messages from producers and delivers them to consumers according to defined rules.
  • Exchange: Receives messages from producers and routes them to one or more queues based on routing logic.
  • Routing key: Keyword supplied by the producer that the exchange uses to decide where to route the message.
  • Binding: Association between an exchange and a queue that determines how messages are routed.
  • Virtual host: Logical container on an AMQP server that isolates different applications or users.

Spring AMQP wraps the AMQP protocol for Spring applications, providing a convenient template built on top of RabbitMQ and auto‑configuration via Spring Boot.

Spring AMQP official site: https://spring.io/projects/spring-amqp
Documentation: https://docs.spring.io/spring-amqp/docs/2.4.14/reference/html

Spring AMQP offers three main features:

  • Automatic declaration of queues, exchanges, and bindings
  • Annotation‑based listener model @RabbitListener for asynchronous message reception (requires @Component on the class)
  • A RabbitTemplate utility for sending messages
Simple Queue Model – BasicQueues

In this model, messages are sent directly to a queue, bypassing any exchange (see diagram). The publisher posts directly to the queue, and the consumer listens and processes messages from that queue. This pattern is mainly for testing and rarely used in production.

Message sending

  • Add the Maven dependency to the publisher service
  • Add configuration in application.yml of the publisher
  • Write a test class SpringAMQPTest and use RabbitTemplate to send messages

Message receiving

  • Add the Maven dependency to the consumer service
  • Add configuration in application.yml of the consumer
  • Create a SpringRabbitListener class in the consumer’s listener package to receive messages (code omitted)
  • Start the consumer service, then run the publisher test to send MQ messages; the console will show both send and receive logs
Work Queues Model – “the capable do more”

Work queues (also called task queues) let multiple consumers bind to a single queue and share the load.

When processing is time‑consuming, the production rate can far exceed the consumption rate, causing a backlog. Using the work‑queue model, multiple consumers process messages concurrently, dramatically increasing throughput.

Using the work model

  • Multiple consumers bind to one queue; each message is processed by only one consumer
  • Set **prefetch** to control how many messages each consumer pre‑fetches

Message sending

Add a test method in the SpringAmqpTest class of the publisher service.

Message receiving

To simulate multiple consumers on the same queue, add two new methods to SpringRabbitListener in the consumer service. Both consumers are configured with Thead.sleep to simulate processing time:

  • Consumer 1 sleeps 20 ms → ~50 messages/sec
  • Consumer 2 sleeps 200 ms → ~5 messages/sec

Thus, messages are distributed evenly regardless of each consumer’s capacity, leading to one consumer being idle while the other is overloaded, and overall processing time exceeds one second.

“The capable do more”

Spring allows a simple configuration to address this. Add the following to the consumer’s application.yml:

(configuration omitted)

After restarting, the faster consumer processes more messages, while the slower one handles only six. Total execution time drops to around one second, demonstrating efficient utilization of each consumer’s capability and preventing backlog.

Publish/Subscribe Model – Basic Introduction

The previous two examples did not involve an exchange; producers sent directly to a queue. Introducing an exchange changes the flow dramatically (see diagram).

In the publish/subscribe model, an exchange appears, and the process is slightly different:

  • Publisher: Sends messages to an exchange instead of directly to a queue.
  • Exchange: Receives messages from the publisher and decides what to do—deliver to a specific queue, all queues, or discard—based on its type.
  • Queue: Still stores and buffers messages, but must be bound to an exchange.
  • Consumer: Subscribes to a queue as before.

Important: An exchange only forwards messages; it does not store them. If no queue is bound to the exchange, or no queue matches the routing rules, the message is lost!

Exchange types (four kinds):

  • Fanout: Broadcasts the message to all queues bound to the exchange. This is the type we first used in the console.
  • Direct: Routes based on RoutingKey(路由key) to queues that have subscribed.
  • Topic: Like Direct, but the routing key can contain wildcards.
  • Headers: Matches on message headers; used less frequently.

Fanout Exchange
“Fanout” literally means “spread out”; in MQ terminology it’s essentially a broadcast.

In broadcast mode, the message flow is:

(diagram omitted)

Characteristics

  • Can have multiple queues
  • Each queue must be bound to the exchange
  • Producers send only to the exchange
  • The exchange forwards the message to all bound queues
  • All consumers of those queues receive the message

Direct Exchange
In Fanout mode, every subscribed queue receives each message. Sometimes we need different messages to go to different queues; that’s where a Direct exchange is used.

Characteristics

  • Binding a queue to an exchange requires specifying a routing key RoutingKey
  • When publishing, the producer must also specify a routing key RoutingKey
  • The exchange forwards the message only to queues whose binding key RoutingKey exactly matches the message’s routing key RoutingKey

Topic Exchange
Compared with Direct, a Topic exchange also routes based on the routing key, but it allows wildcards in the binding key.

A binding key typically consists of one or more words separated by . (e.g., item.insert).

Wildcard rules:

  • #: matches one or more words
  • *: matches exactly one word

Examples:

  • item.# matches item.spu.insert or item.spu
  • item.* matches only item.spu

(diagram omitted)

Assume a producer sends messages with four possible routing keys:

  • china.news – Chinese news
  • china.weather – Chinese weather
  • japan.news – Japanese news
  • japan.weather – Japanese weather

Explanation:

  • topic.queue1 binds to china.#; any routing key starting with china. matches, including china.news and china.weather.
  • topic.queue2 binds to #.news; any routing key ending with .news matches, including china.news and japan.news.

Declaring Queues and Exchanges
When you declare queues and exchanges in Java code, the framework checks at startup and creates them automatically if they do not exist.

Basic API
Spring AMQP provides a Queue class for creating queues:

(code omitted)

Spring AMQP also offers a Exchange interface representing all exchange types:

(code omitted)

You can create queues and exchanges yourself, but Spring AMQP supplies ExchangeBuilder to simplify the process:

(code omitted)

When binding a queue to an exchange, use BindingBuilder to create a Binding object:

(code omitted)

@Bean // fanout example

Create a class in the consumer to declare the queue and exchange:

(code omitted)

Direct example

Because Direct mode may require binding multiple KEY, it can become verbose—each key needs its own binding:

(code omitted)

@RabbitListener declaration – using @Bean to declare queues and exchanges can be cumbersome; Spring also supports annotation‑based declarations.

Direct example (annotation)

(code omitted)

Topic example

(code omitted)

Easy piece!

Message Converter
Spring’s sending code receives the message body as an Object:

(code omitted)

During transmission, Spring serializes the object to a byte stream for the MQ and deserializes it back to a Java object on receipt. By default, Spring uses JDK serialization.

It is well‑known that JDK serialization has several drawbacks:

  • Performance – relatively slow, especially for complex objects or large payloads, because it traverses the entire object graph and creates many intermediate objects.
  • Platform dependence – serialized data can differ across Java versions or operating systems, causing compatibility issues.
  • Version compatibility – changes to class structure (adding/removing fields) may break deserialization or lead to data loss.
  • Security – vulnerable to remote code execution (RCE) attacks via crafted serialized streams.
  • Poor readability

Configuring a Message Converter

  • Add the required dependencies to both publisher and consumer services.
    • Note: If the project already includes spring-boot-starter-web, you do not need to add Jackson again.
  • Configure the converter by adding a Bean bean in the startup classes of publisher and consumer.
    • Adding a messageId in the converter helps with future idempotency checks.

Message Reliability

Overview
Every step from producer to consumer can cause message loss:

  • Loss when sending:
    • Producer fails to connect to MQ (producer retry mechanism)
    • Producer sends a message but the exchange is not found (producer confirm mechanism)

(content truncated)


Originally written by Li Wei (李唯_) and published in Chinese on 后端技术栈全书 (Full-Stack Backend Engineering). Translated and adapted for DriftSeas with permission.

Keep reading

More related articles from DriftSeas.