Spring Builders

Cover image for Externalize Spring-Modulith Events with Spring Cloud Stream
Ivan Garcia Sainz-Aja
Ivan Garcia Sainz-Aja

Posted on • Updated on

Externalize Spring-Modulith Events with Spring Cloud Stream

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:

  1. Leverage Spring Cloud Stream to support multiple message brokers at once, even inside the same application.
  2. Providing enhanced control over message headers and metadata.
  3. Supporting flexible payload serialization with both JSON and Avro.

GitHub Repository

Getting Started

Using this library is straightforward. Here's what you need to do:

  1. Add the Spring-Modulith Events Externalizer dependency to your project
  2. Include your preferred Spring Cloud Stream binder (Kafka, RabbitMQ, etc.)
  3. Configure Spring Cloud Stream bindings in application.yml
  4. Enable externalization with @EnableSpringCloudStreamEventExternalization
  5. Use ApplicationEventPublisher as normal, to publish POJOs, Avro or Message<?> events

1. Add Core Dependency

<dependency>
    <groupId>io.zenwave360.sdk</groupId>
    <artifactId>spring-modulith-events-scs</artifactId>
    <version>1.0.0-RC1</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

Avro Events

  1. Add the Avro dependency:
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-avro</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode
  1. 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
}
Enter fullscreen mode Exit fullscreen mode
  1. 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Type Information: The default JacksonEventSerializer loses generic type information for Message<?> payloads
  2. 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 for Message<?> 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Transactional OutBox With AsyncAPI SpringCloud Stream And Spring Modulith

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 (0)