Skip to content

Complete order fulfillment microservice built with Kotlin, demonstrating DDD patterns and event-driven architecture with ZenWave SDK.

Order Fulfillment (Kotlin)

This is a complete example of a ZenWave designed and generated project built with Kotlin, implementing an Order Fulfillment domain with JPA persistence and Domain Events published to Kafka. You can find the Complete Source Code at GitHub.

  • Model a DDD aggregate with state transitions using ZDL
  • Generate Kotlin code from domain models
  • Implement REST APIs with OpenAPI and Kotlin
  • Publish domain events with AsyncAPI and Kafka
  • Build a complete Spring Boot microservice in Kotlin

We assume you have already read the Getting Started section and installed ZenWave SDK CLI and IntelliJ Plugin and are somewhat familiar with the concepts of DDD and Event-Driven Architecture.

What we will be building: An Order Fulfillment Service

Section titled “What we will be building: An Order Fulfillment Service”

We will be building a Kotlin/Spring Boot microservice that manages the complete order fulfillment lifecycle, from order placement through payment and shipping, exposing REST APIs and publishing Domain Events to Kafka.

The Order entity is the root of the aggregate, managing the order lifecycle through distinct states:

  • PLACEDPAIDSHIPPEDCANCELLED

The Order aggregate contains:

  • Order metadata (orderNumber, status, totalAmount, currency)
  • Payment information (paymentReference)
  • Shipping information (trackingNumber)
  • An array of OrderItem entities stored in a JSON column

Business Rules:

  • Order numbers must be unique
  • Shipped orders cannot be cancelled
PlantUML diagram

State transitions are defined in OrdersService service interface using @transition decorator, see Domain Modeling below.

PlantUML diagram

The application exposes REST endpoints for managing the order lifecycle:

  • POST /api/orders - Place a new order
  • POST /api/orders/{orderNumber}/pay - Pay for an order
  • POST /api/orders/{orderNumber}/ship - Ship an order
  • POST /api/orders/{orderNumber}/cancel - Cancel an order
  • GET /api/orders/{orderNumber} - Get order details
OpenAPI Definition for Order Fulfillment Service

Navigate to source

The application publishes domain events to Kafka topics for each state transition:

  • orders.placed - OrderPlaced events
  • orders.paid - OrderPaid events
  • orders.shipped - OrderShipped events
  • orders.cancelled - OrderCancelled events

Events include CloudEvents headers and Kafka message keys for proper routing and traceability.

AsyncAPI Definition for Order Events

Navigate to source

Building with ZenWave Domain Model and SDK

Section titled “Building with ZenWave Domain Model and SDK”

When modeling a microservice with ZenWave SDK we usually do it using two main files:

  • A zenwave-model.zdl file containing the domain model and service definitions, we use this file to iterate and refine the domain model.
  • A zenwave-scripts.zw file containing the plugin configurations and executions, you can run each plugin individually from ZenWave Model Editor for IntelliJ to generate different aspects of the application.

The full application we are building in this example was defined in the following ZDL model:

`zenwave-model.zdl` for Order Fulfillment Service

Navigate to source

`zenwave-scripts.zw` for Order Fulfillment Service

Navigate to source

NOTE: You can name these files as you wish, just mind the file extension .zdl for the domain model and .zw for the scripts and in .zw pointing to the proper zdlFile file containing the domain model.

So let’s dive into the details of how this application was built using ZenWave SDK.

The Order aggregate is modeled with a natural ID (orderNumber) and includes order items stored as a JSON array. The domain model defines the complete order lifecycle with state transitions and business rules.

Order Entity in ZDL
/**
* Order aggregate root
*/
@aggregate
@auditing
@lifecycle(field: status, initial: PLACED)
entity Order(order_table) {
/** prefix doc comment */
@naturalId
orderNumber String required unique maxlength(36)
status OrderStatus required /** suffix inline doc comment */
totalAmount BigDecimal required
currency String required maxlength(3)
paymentReference String
trackingNumber String
/**
* Order lines stored as JSON for simplicity in the demo
*/
@json items OrderItem[] minlength(1) {
productId String required
productName String required maxlength(254)
quantity Integer required min(1)
unitPrice BigDecimal required
}
}
policies (Order) {
orderNumberUnique "orderNumber must be unique"
}
enum OrderStatus {
PLACED(1),
PAID(2),
SHIPPED(3),
CANCELLED(4)
}

Navigate to source

This generates the following Kotlin entity:

Order.kt - Generated Kotlin Entity

Navigate to source

OrderStatus.kt - Generated Kotlin Enum

Navigate to source

OrderItem.kt - Generated Kotlin Value Object

Navigate to source

Aggregates will also get generated a Spring Data Repository interface and an InMemory implementation for testing purposes.

OrderRepository.kt - Generated Repository Interface

Navigate to source

Services are the entry point to the core domain, and are generated as Spring @Service classes. The Order service defines commands for each state transition in the order lifecycle.

State transitions are defined by the combination of @lifecycle and @transition annotations in the ZDL model.

OrdersService in ZDL with State Transitions annotations
@rest("/orders")
service OrderService for (Order) {
/** Customer places an order */
@post
@transition(to: PLACED)
placeOrder(PlaceOrderInput) Order withEvents OrderPlaced
/** Set Order as paid */
@post("/{orderNumber}/pay")
@transition(from: PLACED, to: PAID)
payOrder(@natural id, PayOrderInput) Order withEvents OrderPaid
/** Set Order as shipped */
@post("/{orderNumber}/ship")
@transition(from: PAID, to: SHIPPED)
shipOrder(@natural id, ShipOrderInput) Order withEvents OrderShipped
/** Set Order as cancelled */
@post("/{orderNumber}/cancel")
@transition(from: [PLACED, PAID], to: CANCELLED)
cancelOrder(@natural id) Order withEvents OrderCancelled
/** Query order */
@get("/{orderNumber}")
getOrder(@natural id) Order?
}

Navigate to source

This ZDL service will generate the following Kotlin service interface and implementation:

OrderService.kt - Generated Service Interface
package io.zenwave360.example.orderfulfillment
import io.zenwave360.example.orderfulfillment.domain.*
import io.zenwave360.example.orderfulfillment.dtos.*
import java.math.*
import java.time.*
/** Inbound Service Port for managing [Order]. */
interface OrderService {
/**
* Customer places an order
*
* With Events: [OrderPlaced].
*/
fun placeOrder(input: PlaceOrderInput): Order
/**
* Set Order as paid
*
* With Events: [OrderPaid].
*/
fun payOrder(orderNumber: String, input: PayOrderInput): Order
/**
* Set Order as shipped
*
* With Events: [OrderShipped].
*/
fun shipOrder(orderNumber: String, input: ShipOrderInput): Order
/**
* Set Order as cancelled
*
* With Events: [OrderCancelled].
*/
fun cancelOrder(orderNumber: String): Order
/** Query order */
fun getOrder(orderNumber: String): Order?
}

Navigate to source

OrderServiceImpl.kt - Generated Service Implementation

Navigate to source

The REST API is generated from the OpenAPI specification, creating Spring MVC controllers that delegate to the service layer.

OrderApiController.kt - Generated REST Controller

Navigate to source

OrderDTOsMapper.kt - Generated DTO Mapper

Navigate to source

Service commands publish Domain Events as part of their operations. Events are defined in the ZDL model and generated code handles the publishing to Kafka.

OrderServiceImpl.kt - Using Generated Events Producer

Navigate to source

OrderEventsProducer is generated from the AsyncAPI definition using ZenWave SDK Maven Plugin.

  • JDK 24
  • Maven 3.8+
  • Docker & Docker Compose - If you don’t have Docker Compose installed, we recommend Rancher Desktop configured with dockerd engine (not containerd), which includes both docker and docker-compose commands.
  • Your favorite IDE

Follow these steps to run the complete application:

  1. Start infrastructure services:

    docker-compose up -d
  2. Run the Spring Boot application:

    mvn spring-boot:run
  3. Access the API:

    • Open Swagger UI in your browser
    • Use Basic Authentication: username admin, password password
  4. Test the order lifecycle:

    • Create a new order via POST /api/orders
    • Pay for the order via POST /api/orders/{orderNumber}/pay
    • Ship the order via POST /api/orders/{orderNumber}/ship
    • Retrieve order details via GET /api/orders/{orderNumber}
  • PostgreSQL (port 5432) - Order data persistence
  • Kafka (port 9092) - Domain events messaging
  • Spring Boot App (port 8080) - REST API and business logic

Happy Coding! 🚀