Spring Modulith enables developers to build well-structured modular monoliths with built-in event capabilities. This allows teams to leverage Event-Driven Architecture patterns without immediately committing to a distributed system.
It provides multiple event externalizers out-of-the-box:
-
Kafka:
spring-modulith-events-kafka
-
AMQP:
spring-modulith-events-amqp
-
JMS:
spring-modulith-events-jms
-
AWS SQS:
spring-modulith-events-aws-sqs
-
AWS SNS:
spring-modulith-events-aws-sns
-
Spring Messaging:
spring-modulith-events-messaging
While these built-in externalizers cover common use cases, there are scenarios where you need more flexibility and control.
I'm happy to introduce a new library featuring a new Spring Modulith event externalizer for Spring Cloud Stream.
Why a New Library?
This library addresses several key needs by:
- Leverage Spring Cloud Stream to support multiple message brokers at once, even inside the same application.
- Providing enhanced control over message headers and metadata.
- Supporting flexible payload serialization with both JSON and Avro.
Getting Started
Using this library is straightforward. Here's what you need to do:
- Add the Spring-Modulith Events Externalizer dependency to your project
- Include your preferred Spring Cloud Stream binder (Kafka, RabbitMQ, etc.)
- Configure Spring Cloud Stream bindings in application.yml
- Enable externalization with
@EnableSpringCloudStreamEventExternalization
- Use
ApplicationEventPublisher
as normal, to publish POJOs, Avro orMessage<?>
events
1. Add Core Dependency
<dependency>
<groupId>io.zenwave360.sdk</groupId>
<artifactId>spring-modulith-events-scs</artifactId>
<version>1.0.0-RC1</version>
</dependency>
2. Add Spring Cloud Stream Message Broker Binder
Choose your preferred message broker. For Kafka:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>
3. Configure Bindings
Configure your output bindings in application.yml
. We are going to configure two output bindings for different payload types:
spring:
cloud:
stream:
bindings:
# JSON events binding
customers-json-out-0:
destination: customers-json-topic
# Avro events binding
customers-avro-out-0:
destination: customers-avro-topic
content-type: application/*+avro
# Kafka-specific configuration
kafka:
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
A key advantage of using Spring Cloud Stream is the ability to configure multiple message brokers simultaneously:
- Use different brokers (Kafka, RabbitMQ, etc.) in the same application
- Route different events to different brokers through configuration
Basic Configuration
Enable Spring Cloud Stream externalization by adding the @EnableSpringCloudStreamEventExternalization
annotation:
@Configuration
@EnableSpringCloudStreamEventExternalization
public class EventsConfiguration { }
Sending Events
POJO Events
Use ApplicationEventPublisher
as you normally would in Spring Modulith:
@Externalized("customers-json-out-0::#{#this.getId()}") // binding name and routing key
public class CustomerEvent {
// Your POJO implementation
}
@Service
@Transactional
public class CustomerEventsProducer {
private final ApplicationEventPublisher publisher;
public CustomerEventsProducer(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void publishCustomerEvent(CustomerEvent event) {
publisher.publishEvent(event); // <-- Sending the event
}
}
Avro Events
- Add the Avro dependency:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-avro</artifactId>
</dependency>
- Define your Avro event:
@Externalized("customers-avro-out-0::#{#this.getId()}") // binding name and routing key
public class CustomerEvent extends SpecificRecordBase implements SpecificRecord {
// Your Avro implementation
}
- Publish events the same way as POJOs:
@Service
@Transactional
public class CustomerEventsProducer {
private final ApplicationEventPublisher publisher;
public void publishAvroEvent(CustomerEvent event) {
publisher.publishEvent(event); // <-- Sending the event
}
}
Routing Key Header
The SpringCloudStreamEventExternalizer
automatically maps routing keys to the appropriate message header based on your message broker:
Message Broker | Header Name |
---|---|
Kafka | kafka_messageKey |
RabbitMQ | rabbit_routingKey |
Kinesis | partitionKey |
Google PubSub | pubsub_orderingKey |
Azure Event Hubs | partitionKey |
Solace | solace_messageKey |
Apache Pulsar | pulsar_key |
Event Serialization for Spring Modulith Publication Log
Spring Modulith's Transactional Event Publication Log requires events to be serialized for database storage. This presents two challenges:
- Type Information: The default
JacksonEventSerializer
loses generic type information forMessage<?>
payloads - Format Support: If you need to support Avro payloads,
JacksonEventSerializer
does not play well with Avro GenericRecord/SpecificRecord
This library addresses these challenges by:
- Adding a
_class
field to preserve complete type information forMessage<?>
payloads - Supporting both POJO (JSON) and Avro serialization formats, through
AvroMapper
- Enabling full deserialization back to original types
Avro serialization requires the com.fasterxml.jackson.dataformat.avro.AvroMapper
class to be present in the classpath. In order to use Avro serialization, you need to add com.fasterxml.jackson.dataformat:jackson-dataformat-avro
dependency to your project, as stated above
Sending Spring Message Events
For advanced control over message headers, you can send Message<?>
objects by including the spring.cloud.stream.sendto.destination
routing header. This header should point to your intended Spring Cloud Stream output binding.
@Service
public class CustomerEventsProducer {
private final ApplicationEventPublisher applicationEventPublisher;
public CustomerEventsProducer(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Transactional
public void sendCustomerEvent(CustomerEvent event) {
Message<CustomerEvent> message = MessageBuilder
.withPayload(event) // supports both POJO and Avro payloads
.setHeader(
SpringCloudStreamEventExternalizer.SPRING_CLOUD_STREAM_SENDTO_DESTINATION_HEADER,
"customers-json-out-0" // target binding name
)
.build();
applicationEventPublisher.publishEvent(message);
}
}
This header is automatically set when using ZenWave SDK AsyncAPI Generator.
Event Externalization and API Management with AsyncAPI
While Spring Modulith's @Externalized
annotation provides a quick and convenient way to publish events, teams building event-driven systems often need additional capabilities:
- API Documentation: No built-in support for formal API documentation
- Schema Management: No friction to prevent breaking changes in event schemas that could impact consumers
- API Governance: No standardized way to enforce API design standards: naming conventions, versioning, headers/metadata...
API-First Approach with AsyncAPI
For teams following API-First practices, AsyncAPI offers a better approach to describe your Event-Driven Architecture:
- Formal API documentation
- Schema validation (Avro, JSON Schema)
- API governance and versioning
- Contract-first development
Code Generation with ZenWave SDK
The ZenWave SDK AsyncAPI Generator can generate full SDKs from AsyncAPI definitions, including:
- Event Models/DTOs with full type safety
- Strongly-typed header objects with runtime population support
- Spring Cloud Stream event producers/consumers with transactional support via Spring Modulith
- Zero boilerplate code
NOTE: Already using
@Externalized
? We're developing a tool to reverse engineer your events into AsyncAPI specifications.
Example Implementation
See it in action with this complete example:
- Playground Project: Full implementation with AsyncAPI + Avro
- Implementation Guide: Step-by-step tutorial
Benefits Over Built-in Externalization
- Broker Flexibility: Connect to any message broker supported by Spring Cloud Stream
- Enhanced Header Control: Simple configuration of message headers
- Multiple Serialization Formats: Built-in support for JSON and Avro
- AsyncAPI Integration: Seamless integration with Spring Modulith Events and Spring Cloud Stream through ZenWave SDK AsyncAPI Generator
Get Involved
Visit GitHub repository
We welcome contributions and feedback from the community! 🚀
Originally published at: https://www.zenwave360.io/posts/Spring-Modulith-Events-Spring-Cloud-Stream-Externalizer/
Top comments (1)
As a beginner, this article was super helpful! I had no idea how to externalize events with Spring Cloud Stream, but the clear explanations and examples made it click. I’m definitely going to try this out in my own projects—thanks for sharing!
Regards,
Roberto Travado
Flower delivery Spain