DEV Community

Cover image for 5 Best Java Frameworks for Event-Driven Architecture in 2023
Aarav Joshi
Aarav Joshi

Posted on

1

5 Best Java Frameworks for Event-Driven Architecture in 2023

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Event-driven architecture (EDA) has become a cornerstone of modern Java application development, particularly for systems that need to react quickly to changes and handle high throughput. I've spent years implementing EDAs across various domains and can confidently say that choosing the right framework makes a significant difference in your application's responsiveness and maintainability.

Event-Driven Architecture: The Foundation

Event-driven architecture revolves around a simple premise: components communicate through events rather than direct method calls. When something noteworthy happens, a component publishes an event. Other components subscribe to events they're interested in and react accordingly.

This approach creates several advantages. Components remain decoupled, making the system more modular and easier to evolve. The architecture naturally accommodates asynchronous processing, improving responsiveness. It also enables better scalability as components can be distributed across multiple servers or containers.

Events in Java typically contain both metadata (timestamp, event type, etc.) and payload data relevant to the event. The basic components of an EDA include:

  1. Event producers that detect and publish events
  2. Event channels that transport events
  3. Event consumers that receive and process events

Let's explore five powerful Java frameworks that excel at implementing this pattern.

Spring Events: Simplicity Within the Spring Ecosystem

Spring Events provides an elegant implementation of the observer pattern integrated seamlessly with the broader Spring Framework. What I appreciate most about Spring Events is its simplicity for basic use cases combined with the ability to scale up to more complex scenarios.

The framework supports both synchronous and asynchronous event processing. Here's a basic example:

// Define an event
public class OrderCreatedEvent {
    private final Order order;

    public OrderCreatedEvent(Order order) {
        this.order = order;
    }

    public Order getOrder() {
        return order;
    }
}

// Publishing an event
@Service
public class OrderService {
    private final ApplicationEventPublisher eventPublisher;

    public OrderService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void createOrder(Order order) {
        // Business logic
        orderRepository.save(order);

        // Publish event
        eventPublisher.publishEvent(new OrderCreatedEvent(order));
    }
}

// Consuming an event
@Component
public class InventoryService {
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        Order order = event.getOrder();
        // Update inventory
    }
}
Enter fullscreen mode Exit fullscreen mode

For asynchronous event handling, simply add the @Async annotation:

@Component
public class EmailService {
    @Async
    @EventListener
    public void sendOrderConfirmation(OrderCreatedEvent event) {
        // Send email confirmation
    }
}
Enter fullscreen mode Exit fullscreen mode

To enable asynchronous processing, add @EnableAsync to your configuration class.

One limitation is that Spring Events works best within a single JVM. For distributed scenarios, you'll need to combine it with a message broker or another framework.

Axon Framework: CQRS and Event Sourcing Made Accessible

Axon Framework takes a more opinionated approach, focusing on Command Query Responsibility Segregation (CQRS) and event sourcing patterns. I've found Axon particularly valuable for complex domains where audit trails and state reconstruction are important.

The core concepts in Axon include:

  1. Commands: Instructions to change state
  2. Events: Records of state changes
  3. Queries: Requests for information

Here's a simplified example of an Axon application:

// Command
public class CreateOrderCommand {
    @TargetAggregateIdentifier
    private final String orderId;
    private final List<OrderItem> items;

    // Constructor and getters
}

// Aggregate
@Aggregate
public class OrderAggregate {
    @AggregateIdentifier
    private String orderId;
    private OrderStatus status;

    @CommandHandler
    public OrderAggregate(CreateOrderCommand command) {
        apply(new OrderCreatedEvent(command.getOrderId(), command.getItems()));
    }

    @EventSourcingHandler
    public void on(OrderCreatedEvent event) {
        this.orderId = event.getOrderId();
        this.status = OrderStatus.CREATED;
    }

    // Other command handlers and event sourcing handlers
}

// Query model (projection)
@Component
public class OrderSummaryProjection {
    private final OrderSummaryRepository repository;

    @EventHandler
    public void on(OrderCreatedEvent event) {
        OrderSummary summary = new OrderSummary(event.getOrderId());
        summary.setItems(event.getItems());
        repository.save(summary);
    }

    // Other event handlers
}
Enter fullscreen mode Exit fullscreen mode

Axon provides infrastructure components like the Command Bus, Event Bus, and Query Bus that handle message routing. The framework also includes an Event Store for persisting events and reconstructing aggregate state.

A major advantage is Axon's ability to scale from monoliths to microservices. You can start with everything in one application and gradually distribute components as needed.

Vert.x EventBus: High-Performance Reactive Messaging

Vert.x EventBus offers a lightweight, high-performance approach to event-driven programming. I've deployed Vert.x in scenarios requiring high throughput and low latency with excellent results.

The EventBus supports both point-to-point and publish-subscribe messaging patterns and works seamlessly across the network, making it ideal for microservices.

Here's a basic example:

// Create a Vert.x instance
Vertx vertx = Vertx.vertx();

// Publishing messages
vertx.deployVerticle(new AbstractVerticle() {
    @Override
    public void start() {
        // Publish an event every second
        vertx.setPeriodic(1000, id -> {
            JsonObject event = new JsonObject()
                .put("timestamp", System.currentTimeMillis())
                .put("message", "Hello from producer");

            vertx.eventBus().publish("notifications", event);
        });
    }
});

// Consuming messages
vertx.deployVerticle(new AbstractVerticle() {
    @Override
    public void start() {
        vertx.eventBus().consumer("notifications", message -> {
            JsonObject event = (JsonObject) message.body();
            System.out.println("Received: " + event.getString("message"));
        });
    }
});
Enter fullscreen mode Exit fullscreen mode

For distributed scenarios, configure the cluster manager:

VertxOptions options = new VertxOptions()
    .setClusterManager(new HazelcastClusterManager());

Vertx.clusteredVertx(options, res -> {
    if (res.succeeded()) {
        Vertx vertx = res.result();
        EventBus eventBus = vertx.eventBus();
        // Use the event bus
    }
});
Enter fullscreen mode Exit fullscreen mode

What makes Vert.x EventBus particularly powerful is its non-blocking nature. It handles large numbers of concurrent connections efficiently using an event loop model similar to Node.js.

The framework supports various message types including primitive values, JSON, binary data, and custom Java objects (when using the same JVM). For distributed scenarios, you'll need to register codecs for custom types.

Apache Camel: Integration-Focused Event Processing

Apache Camel excels at connecting disparate systems through its extensive component library. I've often turned to Camel when dealing with complex integration scenarios that involve multiple protocols and data formats.

The core abstractions in Camel include:

  1. Routes: Define the flow of messages
  2. Endpoints: Represent the source and destination of messages
  3. Components: Provide connectivity to external systems

Here's a simple Camel route that listens for file changes and processes them:

public class FileProcessingRoute extends RouteBuilder {
    @Override
    public void configure() throws Exception {
        from("file:input?noop=true")
            .log("Processing file ${header.CamelFileName}")
            .filter(xpath("/order[@type='urgent']"))
            .to("jms:queue:urgentOrders")
            .end()
            .to("file:processed");
    }
}
Enter fullscreen mode Exit fullscreen mode

Camel's Domain Specific Language (DSL) makes it intuitive to define complex routing logic. The following example shows how to consume events from Kafka, transform them, and send them to a REST endpoint:

from("kafka:orders?brokers=localhost:9092&groupId=orderProcessor")
    .unmarshal().json(JsonLibrary.Jackson, Order.class)
    .filter(simple("${body.amount} > 1000"))
    .process(exchange -> {
        Order order = exchange.getIn().getBody(Order.class);
        exchange.getIn().setBody(new HighValueOrder(order));
    })
    .marshal().json(JsonLibrary.Jackson)
    .to("rest:post:orders/high-value?host=orderservice");
Enter fullscreen mode Exit fullscreen mode

Camel supports various enterprise integration patterns (EIPs) like content-based routing, splitter, aggregator, and more. It also provides error handling, transactions, and monitoring capabilities.

What sets Camel apart is its vast component library with over 300 components for different technologies and protocols. This makes it a natural choice for event-driven systems that need to integrate with existing infrastructure.

MicroProfile Reactive Messaging: Standard Approach for Microservices

MicroProfile Reactive Messaging provides a standardized approach to building event-driven microservices. I've found it particularly useful in cloud-native environments where portability between different runtime implementations is important.

The core concept is the channel, which connects message producers and consumers. Here's a basic example:

@ApplicationScoped
public class OrderProcessor {
    @Inject
    @Channel("orders-out")
    Emitter<Order> orderEmitter;

    @Incoming("orders-in")
    @Outgoing("processed-orders")
    public Order processOrder(Order order) {
        // Process the order
        order.setStatus(OrderStatus.PROCESSED);
        return order;
    }

    public void createOrder(Order order) {
        // Create a new order and emit it
        orderEmitter.send(order);
    }

    @Incoming("processed-orders")
    public CompletionStage<Void> handleProcessedOrder(Order order) {
        // Do something with the processed order
        System.out.println("Order processed: " + order.getId());
        return CompletableFuture.completedFuture(null);
    }
}
Enter fullscreen mode Exit fullscreen mode

Configuration is typically done in the microprofile-config.properties file:

# Kafka connector for the orders-in channel
mp.messaging.incoming.orders-in.connector=smallrye-kafka
mp.messaging.incoming.orders-in.topic=orders
mp.messaging.incoming.orders-in.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
mp.messaging.incoming.orders-in.value.deserializer=io.vertx.kafka.client.serialization.JsonObjectDeserializer

# Kafka connector for the processed-orders channel
mp.messaging.outgoing.processed-orders.connector=smallrye-kafka
mp.messaging.outgoing.processed-orders.topic=processed-orders
mp.messaging.outgoing.processed-orders.key.serializer=org.apache.kafka.common.serialization.StringSerializer
mp.messaging.outgoing.processed-orders.value.serializer=io.vertx.kafka.client.serialization.JsonObjectSerializer
Enter fullscreen mode Exit fullscreen mode

MicroProfile Reactive Messaging integrates well with other MicroProfile specifications like Config, Metrics, and Health. This makes it suitable for building observable microservices.

The specification supports various messaging patterns including:

  1. Point-to-point using the @Incoming and @Outgoing annotations
  2. Publish-subscribe using broadcast channels
  3. Request-reply with the @Channel and Emitter APIs

Implementations like SmallRye Reactive Messaging provide connectors for popular messaging systems such as Kafka, AMQP, MQTT, and JMS.

Implementing a Real-World Event-Driven Application

To demonstrate how these frameworks can be applied in practice, let's consider an e-commerce order processing system. The system needs to handle orders, update inventory, process payments, and notify customers.

Here's how I'd implement it using Spring Events for simplicity:

// Domain events
public class OrderCreatedEvent {
    private final Order order;
    // Constructor and getters
}

public class PaymentProcessedEvent {
    private final Payment payment;
    // Constructor and getters
}

public class OrderShippedEvent {
    private final Order order;
    private final Shipment shipment;
    // Constructor and getters
}

// Order service
@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher eventPublisher;

    // Constructor with dependency injection

    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setItems(request.getItems());
        order.setCustomerId(request.getCustomerId());
        order.setStatus(OrderStatus.CREATED);

        Order savedOrder = orderRepository.save(order);
        eventPublisher.publishEvent(new OrderCreatedEvent(savedOrder));

        return savedOrder;
    }

    @Transactional
    @EventListener
    public void onPaymentProcessed(PaymentProcessedEvent event) {
        Payment payment = event.getPayment();
        Order order = orderRepository.findById(payment.getOrderId())
            .orElseThrow(() -> new OrderNotFoundException(payment.getOrderId()));

        if (payment.getStatus() == PaymentStatus.COMPLETED) {
            order.setStatus(OrderStatus.PAID);
            orderRepository.save(order);
        }
    }
}

// Inventory service
@Service
public class InventoryService {
    private final InventoryRepository inventoryRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Async
    @EventListener
    public void handleOrderCreated(OrderCreatedEvent event) {
        Order order = event.getOrder();

        // Check inventory and reserve items
        boolean allItemsAvailable = order.getItems().stream()
            .allMatch(item -> checkAndReserveInventory(item));

        if (allItemsAvailable) {
            order.setStatus(OrderStatus.INVENTORY_RESERVED);
            // Trigger payment processing or next step
        } else {
            order.setStatus(OrderStatus.INVENTORY_FAILED);
            // Handle inventory failure
        }
    }

    private boolean checkAndReserveInventory(OrderItem item) {
        // Implementation details
        return true;
    }
}

// Payment service
@Service
public class PaymentService {
    private final PaymentGateway paymentGateway;
    private final ApplicationEventPublisher eventPublisher;

    @Async
    @EventListener
    public void processPayment(OrderCreatedEvent event) {
        Order order = event.getOrder();

        // Only process payment if inventory is reserved
        if (order.getStatus() != OrderStatus.INVENTORY_RESERVED) {
            return;
        }

        Payment payment = new Payment();
        payment.setOrderId(order.getId());
        payment.setAmount(calculateTotalAmount(order));

        try {
            PaymentResult result = paymentGateway.processPayment(payment);
            payment.setStatus(result.isSuccessful() ? 
                PaymentStatus.COMPLETED : PaymentStatus.FAILED);
        } catch (Exception e) {
            payment.setStatus(PaymentStatus.FAILED);
            payment.setErrorMessage(e.getMessage());
        }

        eventPublisher.publishEvent(new PaymentProcessedEvent(payment));
    }
}

// Notification service
@Service
public class NotificationService {
    private final EmailSender emailSender;

    @Async
    @EventListener
    public void notifyOrderCreation(OrderCreatedEvent event) {
        Order order = event.getOrder();
        Customer customer = customerRepository.findById(order.getCustomerId()).orElseThrow();

        emailSender.sendEmail(
            customer.getEmail(),
            "Order Confirmation",
            "Your order " + order.getId() + " has been received."
        );
    }

    @Async
    @EventListener
    public void notifyShipment(OrderShippedEvent event) {
        Order order = event.getOrder();
        Shipment shipment = event.getShipment();
        Customer customer = customerRepository.findById(order.getCustomerId()).orElseThrow();

        emailSender.sendEmail(
            customer.getEmail(),
            "Order Shipped",
            "Your order " + order.getId() + " has been shipped. " +
            "Tracking number: " + shipment.getTrackingNumber()
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

This implementation demonstrates several benefits of event-driven architecture:

  1. Services remain decoupled, communicating only through events
  2. Asynchronous processing improves throughput and responsiveness
  3. New functionality (like analytics) can be added by subscribing to existing events
  4. The system can be scaled by distributing components across multiple servers

For production systems, I would recommend adding error handling, idempotency checks, and monitoring. Also, for truly distributed scenarios, consider using Axon Framework or combining Spring Events with a message broker like RabbitMQ or Kafka.

Conclusion

Event-driven architecture offers a powerful approach to building responsive Java applications. By decoupling components through events, we create systems that are more maintainable, scalable, and resilient.

The five frameworks we've explored each have their strengths:

  • Spring Events provides simplicity and Spring integration
  • Axon Framework excels at CQRS and event sourcing
  • Vert.x EventBus delivers high-performance messaging
  • Apache Camel offers extensive integration capabilities
  • MicroProfile Reactive Messaging standardizes event-driven microservices

My personal recommendation is to start with Spring Events for simple applications within a single JVM. For distributed systems or more complex domains, consider Axon Framework or a combination of Spring with a message broker. If performance is critical, Vert.x is hard to beat. For integration-heavy scenarios, Apache Camel offers unparalleled connectivity.

Remember that successful event-driven systems require careful design of events, thoughtful error handling, and monitoring. Events should be designed to be meaningful and self-contained, containing all the information consumers need to process them.

The time I've spent implementing these patterns across various domains has convinced me that event-driven architecture is not just a technical choice but a strategic advantage for building systems that can evolve with changing business needs.


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

If you found this post helpful, please leave a ❤️ or a friendly comment below!

Okay