AsyncAPI Shopping Cart
Section titled “AsyncAPI Shopping Cart”Event-driven shopping cart application demonstrating AsyncAPI with Avro schemas and ZenWave SDK code generation.
This is a complete example of an event-driven application built with AsyncAPI and Avro schemas, showcasing how ZenWave SDK generates production-ready Kafka producers and consumers. You can find the Complete Source Code at GitHub.
What You’ll Learn
Section titled “What You’ll Learn”- Define multi-message AsyncAPI contracts using Avro schemas
- Generate complete Kafka producers and consumers using ZenWaveSDK
- Implement event-driven architecture with Spring Boot and Kafka
- Prevent API drift by sourcing versioned AsyncAPI definitions
- Test event-driven applications with in-memory implementations
We assume you have already read the Getting Started section and installed ZenWave SDK CLI and are somewhat familiar with the concepts of Event-Driven Architecture and AsyncAPI.
What we will be building: An Event-Driven Shopping Cart
Section titled “What we will be building: An Event-Driven Shopping Cart”We will be building a multi-module event-driven application that demonstrates how to use AsyncAPI with Avro schemas to implement a shopping cart system with Kafka messaging.
The application is composed of three modules which in real-world scenarios would be in different repositories with independent lifecycles:
apis- Contains the AsyncAPI+Avro definition files which can be packaged and distributed as a Maven JAR artifactshopping-cart- The provider application that exposes a REST API and produces events to a Kafka topicclient- A consumer application that consumes events from the Kafka topic and logs them
This example showcases how you can send multiple Avro subjects to the same Kafka topic when you need strict ordering of events of different types. The sequence “ItemAdded, ItemUpdated, ItemRemoved” is fundamentally different from “ItemRemoved, ItemUpdated, ItemAdded”, and this pattern ensures proper event ordering.
AsyncAPI Definition with Avro Schemas
Section titled “AsyncAPI Definition with Avro Schemas”The AsyncAPI definition contains one channel ShoppingCartChannel with 5 messages in Avro format, and 5 different operations, each sending one particular message to the channel:
AsyncAPI Definition for Shopping Cart Events
asyncapi: 3.0.0info: title: "AsyncAPI Shopping Cart Example" version: 0.0.1 tags: - name: "Default" - name: "ShoppingCart"
defaultContentType: application/json
channels: ShoppingCartChannel: address: "shopping-cart" messages: ShoppingCartItemUpdatedMessage: $ref: '#/components/messages/ShoppingCartItemUpdatedMessage' ShoppingCartCheckedOutMessage: $ref: '#/components/messages/ShoppingCartCheckedOutMessage' ShoppingCartItemAddedMessage: $ref: '#/components/messages/ShoppingCartItemAddedMessage' ShoppingCartCreatedMessage: $ref: '#/components/messages/ShoppingCartCreatedMessage' ShoppingCartItemRemovedMessage: $ref: '#/components/messages/ShoppingCartItemRemovedMessage'
operations: onShoppingCartCreated: action: send tags: - name: ShoppingCart channel: $ref: '#/channels/ShoppingCartChannel' messages: - $ref: '#/channels/ShoppingCartChannel/messages/ShoppingCartCreatedMessage' onShoppingCartItemAdded: action: send tags: - name: ShoppingCart channel: $ref: '#/channels/ShoppingCartChannel' messages: - $ref: '#/channels/ShoppingCartChannel/messages/ShoppingCartItemAddedMessage' onShoppingCartItemRemoved: action: send tags: - name: ShoppingCart channel: $ref: '#/channels/ShoppingCartChannel' messages: - $ref: '#/channels/ShoppingCartChannel/messages/ShoppingCartItemRemovedMessage' onShoppingCartItemUpdated: action: send tags: - name: ShoppingCart channel: $ref: '#/channels/ShoppingCartChannel' messages: - $ref: '#/channels/ShoppingCartChannel/messages/ShoppingCartItemUpdatedMessage' onShoppingCartCheckedOut: action: send tags: - name: ShoppingCart channel: $ref: '#/channels/ShoppingCartChannel' messages: - $ref: '#/channels/ShoppingCartChannel/messages/ShoppingCartCheckedOutMessage'
components: messages: ShoppingCartCreatedMessage: name: ShoppingCartCreatedMessage title: "" summary: "" traits: - $ref: '#/components/messageTraits/CommonHeaders' payload: schemaFormat: application/vnd.apache.avro+json;version=1.9.0 schema: $ref: "./avro/ShoppingCartCreated.avsc" ShoppingCartItemAddedMessage: name: ShoppingCartItemAddedMessage title: "" summary: "" traits: - $ref: '#/components/messageTraits/CommonHeaders' payload: schemaFormat: application/vnd.apache.avro+json;version=1.9.0 schema: $ref: "./avro/ShoppingCartItemAdded.avsc" ShoppingCartItemRemovedMessage: name: ShoppingCartItemRemovedMessage title: "" summary: "" traits: - $ref: '#/components/messageTraits/CommonHeaders' payload: schemaFormat: application/vnd.apache.avro+json;version=1.9.0 schema: $ref: "./avro/ShoppingCartItemRemoved.avsc" ShoppingCartItemUpdatedMessage: name: ShoppingCartItemUpdatedMessage title: "" summary: "" traits: - $ref: '#/components/messageTraits/CommonHeaders' payload: schemaFormat: application/vnd.apache.avro+json;version=1.9.0 schema: $ref: "./avro/ShoppingCartItemUpdated.avsc" ShoppingCartCheckedOutMessage: name: ShoppingCartCheckedOutMessage title: "" summary: "" traits: - $ref: '#/components/messageTraits/CommonHeaders' payload: schemaFormat: application/vnd.apache.avro+json;version=1.9.0 schema: $ref: "./avro/ShoppingCartCheckedOut.avsc"
messageTraits: CommonHeaders: headers: type: object properties: kafka_messageKey: type: "integer" format: "int64" description: This header value will be populated automatically at runtime x-runtime-expression: $message.payload#/customerId # CloudEvents Attributes ce-id: type: string description: Unique identifier for the event x-runtime-expression: $message.payload#{#this.customerId.toString()} ce-source: type: string description: URI identifying the context where event happened x-runtime-expression: $message.payload#{"ShoppingCart"} ce-specversion: type: string description: CloudEvents specification version x-runtime-expression: $message.payload#{"1.0"} ce-type: type: string description: Event type x-runtime-expression: $message.payload#{#this.getClass().getSimpleName()} ce-time: type: string description: Timestamp of when the event happened x-runtime-expression: $message.payload#{T(java.time.Instant).now().toString()}The AsyncAPI definition references Avro schema files (*.avsc) that define the structure of each event message:
ShoppingCartItemAdded.avsc - Event Schema
{ "type" : "record", "name" : "ShoppingCartItemAdded", "namespace" : "io.example.asyncapi.shoppingcart.events.avro", "fields" : [ { "name" : "customerId", "type" : "long" }, { "name" : "shoppingCart", "type" : "ShoppingCart" }, { "name" : "item", "type" : "Item" } ]}ShoppingCart.avsc - Supporting Schema
{ "type" : "record", "name" : "ShoppingCart", "namespace" : "io.example.asyncapi.shoppingcart.events.avro", "fields" : [ { "name" : "id", "type" : "long" }, { "name" : "customerId", "type" : "long" }, { "name" : "items", "type" : { "type" : "array", "items" : "Item", "java-class" : "java.util.List" } } ]}Building with ZenWave SDK and AsyncAPI
Section titled “Building with ZenWave SDK and AsyncAPI”The shopping cart application demonstrates how ZenWave SDK generates a complete event-driven architecture from AsyncAPI definitions. All generated code is recreated on each build, ensuring your implementation stays in sync with your API definitions.
Code Generation from AsyncAPI
Section titled “Code Generation from AsyncAPI”ZenWave SDK’s AsyncAPI Generator creates a complete SDK for producing events to Kafka:
- Avro DTOs for all messages defined in the AsyncAPI definition
- Producer Interface (
ShoppingCartEventsProducer) with one method per operation - Spring Implementation (
DefaultShoppingCartEventsProducer) as a Spring@Component - In-Memory Implementation for unit testing purposes
- Test Context (
EventsProducerInMemoryContext) for easier testing
All this code is generated as part of your Maven build and placed in target/generated-sources/zenwave-sdk. Because it’s not part of the project source code, it’s not versioned in Git. To modify the generated code, you change the AsyncAPI+Avro definition files first and then rebuild the project.
Maven Configuration for Code Generation
Section titled “Maven Configuration for Code Generation”The Maven configuration uses ZenWave SDK Maven Plugin to generate code from AsyncAPI definitions. The plugin can source AsyncAPI+Avro definitions from local files, classpath resources, or remote HTTP resources.
Maven Plugin Configuration in pom.xml
<plugin> <groupId>io.zenwave360.sdk</groupId> <artifactId>zenwave-sdk-maven-plugin</artifactId> <version>${zenwave.version}</version> <configuration> <inputSpec>${asyncapi.inputSpec}</inputSpec> <skip>false</skip> <addCompileSourceRoot>true</addCompileSourceRoot> <addTestCompileSourceRoot>true</addTestCompileSourceRoot> <!-- <authentication> --> <!-- <authentication><key>API_KEY</key><value>XXX</value></authentication> --> <!-- </authentication> --> </configuration> <executions> <execution> <id>generate-asyncapi</id> <phase>generate-sources</phase> <goals> <goal>generate</goal> </goals> <configuration> <generatorName>AsyncAPIGenerator</generatorName> <configOptions> <role>provider</role> <templates>${zenwave.asyncapiGenerator.templates}</templates> <!-- <modelPackage>${asyncApiModelPackage}</modelPackage> required for json-schema, here it'll use the avro package --> <producerApiPackage>${asyncApiProducerApiPackage}</producerApiPackage> <consumerApiPackage>${asyncApiConsumerApiPackage}</consumerApiPackage> <avroCompilerProperties.imports>${asyncapi.avro.imports}</avroCompilerProperties.imports> </configOptions> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>io.zenwave360.sdk.plugins</groupId> <artifactId>asyncapi-generator</artifactId> <version>${zenwave.version}</version> </dependency> <dependency> <groupId>org.apache.avro</groupId> <artifactId>avro-compiler</artifactId> <version>${avro.version}</version> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> </exclusion> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.example.asyncapi.shoppingcart</groupId> <artifactId>shopping-cart-apis</artifactId> <version>1.0.0</version> </dependency> </dependencies> </plugin>The key configuration properties are:
Maven Properties for AsyncAPI Code Generation in pom.xml
<properties> <basePackage>io.example.asyncapi.shoppingcart</basePackage> <openApiApiPackage>${basePackage}.web</openApiApiPackage> <openApiModelPackage>${basePackage}.web.model</openApiModelPackage>
<asyncapiPrefix>classpath:io/example/asyncapi/shoppingcart/apis</asyncapiPrefix> <asyncapi.inputSpec>${asyncapiPrefix}/asyncapi.yml</asyncapi.inputSpec> <asyncapi.avro.imports> ${asyncapiPrefix}/avro/ </asyncapi.avro.imports> <zenwave.asyncapiGenerator.templates>SpringKafka</zenwave.asyncapiGenerator.templates>
<asyncApiProducerApiPackage>${basePackage}.events</asyncApiProducerApiPackage> <asyncApiConsumerApiPackage>${basePackage}.commands</asyncApiConsumerApiPackage>
<spotless-maven-plugin.version>2.44.3</spotless-maven-plugin.version> <palantir-java-format.version>2.55.0</palantir-java-format.version> </properties>Or for remote HTTP resources:
Maven Properties for AsyncAPI Code Generation from HTTP resources in pom.xml
<profile> <id>asyncapi-http-files</id> <properties> <asyncapiPrefix>https://raw.githubusercontent.com/ZenWave360/zenwave-playground/refs/heads/main/examples/asyncapi-shopping-cart/apis</asyncapiPrefix> <asyncapi.inputSpec>${asyncapiPrefix}/asyncapi.yml</asyncapi.inputSpec> <asyncapi.avro.imports> ${asyncapiPrefix}/avro/Item.avsc, ${asyncapiPrefix}/avro/ShoppingCart.avsc, </asyncapi.avro.imports> </properties> </profile>Note: When using remote HTTP resources,
asyncapi.avro.importsrequires pointing to each avsc file individually because it is not possible to scan folders or packages from remote HTTP resources.
Note: You do not need to worry about ordering
asyncapi.avro.importseven if you are using Avro prior to 1.12.0 because ZenWaveSDK will sort them for you.
Using Generated Code in Your Application
Section titled “Using Generated Code in Your Application”The generated producer interface integrates seamlessly into your Spring Boot application. Here’s how the generated code is used in the service implementation:
@Service@lombok.AllArgsConstructorpublic class ShoppingCartServiceImpl implements ShoppingCartService {
// [...] private final ShoppingCartEventsProducer eventsProducer; // <-- this is generated code
public ShoppingCart addItem(Long customerId, Item input) { log.debug("Request addItem: {} {}", customerId, input);
var shoppingCart = loadShoppingCart(customerId); shoppingCart.getItems().add(input); shoppingCartRepository.save(shoppingCart); // emit events var shoppingCartItemAdded = eventsMapper.asShoppingCartItemAdded(shoppingCart, input); eventsProducer.onShoppingCartItemAdded(shoppingCartItemAdded); return shoppingCart; }}The service implementation receives the generated ShoppingCartEventsProducer via dependency injection and uses it to publish events after performing business operations. The producer interface provides type-safe methods for each operation defined in the AsyncAPI specification.
Unit Testing with In-Memory Event Producer
Section titled “Unit Testing with In-Memory Event Producer”ZenWave SDK generates an in-memory implementation of the events producer for unit testing purposes, allowing you to test your business logic without requiring a running Kafka instance:
class ShoppingCartServiceTest {
ServicesInMemoryConfig context = new ServicesInMemoryConfig(); ShoppingCartServiceImpl shoppingCartService = context.shoppingCartService();
@BeforeEach void setUp() { context.reloadTestData(); }
@Test void createShoppingCartTest() { Long customerId = 0L; // non existing shopping cart var shoppingCart = shoppingCartService.loadShoppingCart(customerId);
Assertions.assertNotNull(shoppingCart); var capturedMessages = context.getEventsProducerInMemoryContext().shoppingCartEventsProducer() .getOnShoppingCartCreatedCapturedMessages(); Assertions.assertEquals(1, capturedMessages.size()); }}The in-memory producer captures all published events, allowing you to verify that your service publishes the correct events with the expected payloads. This approach provides fast, reliable unit tests without external dependencies.
Shopping Cart Client Application
Section titled “Shopping Cart Client Application”The client application demonstrates how to consume events from Kafka using ZenWave SDK generated code. It uses the same AsyncAPI definition but with reversed roles.
Generating Consumer Code
Section titled “Generating Consumer Code”The client uses similar Maven configuration to generate all required code to consume events from the Kafka topic. The key difference is using <role>client</role> in the ZenWave SDK AsyncAPI Generator configuration, which reverses how code is generated: generating a consumer where the API defines a producer and vice versa.
Client Maven Configuration in pom.xml
<plugin> <groupId>io.zenwave360.sdk</groupId> <artifactId>zenwave-sdk-maven-plugin</artifactId> <version>${zenwave.version}</version> <configuration> <inputSpec>${asyncapi.inputSpec}</inputSpec> <skip>false</skip> <addCompileSourceRoot>true</addCompileSourceRoot> <addTestCompileSourceRoot>true</addTestCompileSourceRoot> </configuration> <executions> <execution> <id>generate-asyncapi</id> <phase>generate-sources</phase> <goals> <goal>generate</goal> </goals> <configuration> <generatorName>AsyncAPIGenerator</generatorName> <configOptions> <role>client</role> <templates>${zenwave.asyncapiGenerator.templates}</templates> <!-- required for json, here it uses avro package <modelPackage>${asyncApiModelPackage}</modelPackage>--> <producerApiPackage>${asyncApiProducerApiPackage}</producerApiPackage> <consumerApiPackage>${asyncApiConsumerApiPackage}</consumerApiPackage> <avroCompilerProperties.imports>${asyncapi.avro.imports}</avroCompilerProperties.imports>
<!-- <operationIds>onCustomerDeletedEvent</operationIds>--> </configOptions> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>io.zenwave360.sdk.plugins</groupId> <artifactId>asyncapi-generator</artifactId> <version>${zenwave.version}</version> </dependency> <dependency> <groupId>org.apache.avro</groupId> <artifactId>avro-compiler</artifactId> <version>${avro.version}</version> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> </exclusion> <exclusion> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.example.asyncapi.shoppingcart</groupId> <artifactId>shopping-cart-apis</artifactId> <version>1.0.0</version> </dependency> </dependencies> </plugin>Naming Convention for Operations
Section titled “Naming Convention for Operations”Because the generator uses the same operationIds to generate Java code, we recommend following this naming convention for consistency between provider and client:
- Use prefix
on*for operations that produce event messages (e.g.,onShoppingCartItemAdded) - Use prefix
do*for operations that consume command messages (e.g.,doCheckoutShoppingCart)
Implementing Event Consumers
Section titled “Implementing Event Consumers”After code is generated, you can focus on your business logic by implementing the generated consumer interface:
ShoppingCartChannelConsumerService.java - Event Consumer Implementation
@Componentpublic class ShoppingCartChannelConsumerService implements IShoppingCartChannelConsumerService {
private static final Logger log = LoggerFactory.getLogger(ShoppingCartChannelConsumerService.class);
@Override public void onShoppingCartCreated(ShoppingCartCreated payload, ShoppingCartCreatedHeaders headers) { log.info("onShoppingCartCreated: {}", payload); }The consumer service implements the generated interface, providing type-safe methods for handling each event type. Spring Cloud Streams handles all the Kafka consumer configuration, deserialization, and message routing automatically.
Running the Shopping Cart Application
Section titled “Running the Shopping Cart Application”Prerequisites
Section titled “Prerequisites”- JDK 25+
- Maven 3.8+
- Docker & Docker Compose - If you don’t have Docker Compose installed, we recommend Rancher Desktop configured with
dockerdengine (notcontainerd), which includes bothdockeranddocker-composecommands. - Your favorite IDE
Quick Start
Section titled “Quick Start”Follow these steps to run the complete application:
-
Package AsyncAPI+Avro definition files:
ZenWaveSDK AsyncAPI generator can source AsyncAPI+Avro definition files from local files, classpath resources, or remote HTTP resources. Build and install the
apismodule in your local Maven repository:Terminal window mvn clean install -f apis/pom.xmlAlternatively, you can append
-Pasyncapi-http-filesto the commands below to source the AsyncAPI+Avro definition files from GitHub HTTP URLs. -
Start infrastructure services:
Terminal window docker-compose -f shopping-cart/docker-compose.yml up -d -
Run the Shopping Cart Provider application:
Terminal window mvn clean spring-boot:run -f shopping-cart/pom.xmlNote: Append
-Pasyncapi-http-filesto the previous command if you prefer to source the AsyncAPI+Avro definition files from remote HTTP resources. -
Access the API:
- Open Swagger UI in your browser
- Use Basic Authentication: username
admin, passwordpassword
-
Test the endpoints:
- Try creating a shopping cart and adding items
- Update item quantities
- Check out the shopping cart
Verifying Event Flow
Section titled “Verifying Event Flow”Logging Kafka Events
Section titled “Logging Kafka Events”You can verify that events are being published to Kafka by using the Kafka Avro console consumer:
docker-compose -f shopping-cart/docker-compose.yml \ exec -T schema-registry bash -c "kafka-avro-console-consumer --bootstrap-server kafka:19093 \ --topic shopping-cart \ --from-beginning \ --property schema.registry.url=http://schema-registry:8081"You should see the events produced by the shopping cart provider application:
{"customerId":7}{"customerId":7,"shoppingCart":{"id":2,"customerId":7,"items":[{"name":"Item Name","quantity":10}]},"item":{"name":"Item Name","quantity":10}}{"customerId":7,"shoppingCart":{"id":2,"customerId":7,"items":[{"name":"Item Name","quantity":10}]},"item":{"name":"Item Name","quantity":10},"previousItem":{"name":"Item Name","quantity":10}}Running the Client Consumer
Section titled “Running the Client Consumer”Start the shopping cart client application to consume and process events:
mvn clean spring-boot:run -f client/pom.xmlYou should see the client application logging the events as they are consumed:
2025-11-22T11:58:29.112+01:00 INFO 10652 --- [ntainer#0-0-C-1] s.c.e.ShoppingCartChannelConsumerService : onShoppingCartCreated: {"customerId": 1}2025-11-22T11:58:29.116+01:00 INFO 10652 --- [ntainer#0-0-C-1] s.c.e.ShoppingCartChannelConsumerService : onShoppingCartItemAdded: {"customerId": 1, "shoppingCart": {"id": 1, "customerId": 1, "items": [{"name": "string", "quantity": 0}]}, "item": {"name": "string", "quantity": 0}}2025-11-22T11:58:29.124+01:00 INFO 10652 --- [ntainer#0-0-C-1] s.c.e.ShoppingCartChannelConsumerService : onShoppingCartCreated: {"customerId": 7}2025-11-22T11:58:29.126+01:00 INFO 10652 --- [ntainer#0-0-C-1] s.c.e.ShoppingCartChannelConsumerService : onShoppingCartItemAdded: {"customerId": 7, "shoppingCart": {"id": 2, "customerId": 7, "items": [{"name": "Item Name", "quantity": 10}]}, "item": {"name": "Item Name", "quantity": 10}}2025-11-22T11:58:29.128+01:00 INFO 10652 --- [ntainer#0-0-C-1] s.c.e.ShoppingCartChannelConsumerService : onShoppingCartItemUpdated: {"customerId": 7, "shoppingCart": {"id": 2, "customerId": 7, "items": [{"name": "Item Name", "quantity": 10}]}, "item": {"name": "Item Name", "quantity": 10}, "previousItem": {"name": "Item Name", "quantity": 10}}What’s Running
Section titled “What’s Running”- Kafka (port 9092) - Event streaming platform
- Schema Registry (port 8081) - Avro schema management
- Shopping Cart Provider (port 8080) - REST API and event producer
- Shopping Cart Client - Event consumer and processor
Happy Coding! 🚀