Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
@@ -1,3 +1,169 @@
= helidon-mongodb-cqrs

CQRS Java sample using Helidon and MongoDB

== Overview

This project demonstrates a *CQRS* (Command Query Responsibility Segregation) architecture built with:

* https://helidon.io[Helidon MicroProfile 3.x] — lightweight Java microservices framework
* https://www.mongodb.com[MongoDB] — document database for persistence
* *Domain-Driven Design (DDD)* — aggregates, value objects, domain events, repositories
* *Package-by-feature* — each feature is self-contained with its own API, command, query, domain, and infrastructure layers

== Architecture

=== Package-by-feature layout

----
com.soujava.helidon.cqrs
├── shared/ ← cross-cutting CQRS abstractions
│ ├── command/
│ │ ├── Command.java ← marker interface
│ │ ├── CommandBus.java ← dispatch interface
│ │ ├── CommandHandler.java ← handler interface
│ │ └── SimpleCommandBus.java ← in-process registry-based bus
│ ├── query/
│ │ ├── Query.java
│ │ ├── QueryBus.java
│ │ ├── QueryHandler.java
│ │ └── SimpleQueryBus.java
│ └── domain/
│ ├── AggregateRoot.java ← base class; collects domain events
│ └── DomainEvent.java ← base class for all domain events
└── order/ ← "Order" bounded context / feature
├── api/ ← REST resources & DTOs (JAX-RS)
│ ├── OrderResource.java
│ ├── CreateOrderRequest.java
│ └── DomainExceptionMapper.java
├── command/ ← write side
│ ├── CreateOrderCommand.java
│ ├── CreateOrderCommandHandler.java
│ ├── ConfirmOrderCommand.java
│ ├── ConfirmOrderCommandHandler.java
│ ├── CancelOrderCommand.java
│ └── CancelOrderCommandHandler.java
├── query/ ← read side
│ ├── FindOrderByIdQuery.java
│ ├── FindOrderByIdQueryHandler.java
│ ├── FindAllOrdersQuery.java
│ ├── FindAllOrdersQueryHandler.java
│ └── OrderView.java ← read model (separate from domain aggregate)
├── domain/ ← DDD domain model
│ ├── Order.java ← aggregate root
│ ├── OrderId.java ← value object
│ ├── OrderItem.java ← entity
│ ├── OrderStatus.java ← enum value object
│ ├── OrderRepository.java ← repository interface (port)
│ └── event/
│ ├── OrderCreatedEvent.java
│ ├── OrderConfirmedEvent.java
│ └── OrderCancelledEvent.java
├── infrastructure/ ← adapters / persistence
│ ├── MongoClientProducer.java
│ └── MongoOrderRepository.java
└── OrderHandlerRegistrar.java ← wires handlers into buses at startup
----

=== DDD concepts applied

[cols="1,3"]
|===
| Concept | Where

| *Aggregate Root* | `Order` — all state changes go through it; enforces invariants
| *Value Object* | `OrderId`, `OrderStatus` — immutable, identity-less
| *Entity* | `OrderItem` — has data but lives inside the aggregate boundary
| *Domain Event* | `OrderCreatedEvent`, `OrderConfirmedEvent`, `OrderCancelledEvent`
| *Repository* | `OrderRepository` interface in the domain; MongoDB implementation in infrastructure
| *Factory method* | `Order.create(...)` — the only way to create a new order
|===

=== CQRS flow

----
REST Request
OrderResource
├─[POST /orders]────────► CommandBus.dispatch(CreateOrderCommand)
│ │
│ ▼
│ CreateOrderCommandHandler
│ │
│ ▼
│ Order.create() → OrderRepository.save()
└─[GET /orders/{id}]───► QueryBus.dispatch(FindOrderByIdQuery)
FindOrderByIdQueryHandler
OrderRepository.findById() → OrderView
----

== REST API

[cols="1,1,3"]
|===
| Method | Path | Description

| `POST` | `/orders` | Place a new order
| `GET` | `/orders` | List all orders
| `GET` | `/orders/{id}` | Get a single order by id
| `PUT` | `/orders/{id}/confirm` | Confirm a pending order
| `PUT` | `/orders/{id}/cancel` | Cancel an order
|===

=== Example — create an order

[source,bash]
----
curl -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": "customer-42",
"items": [
{ "productId": "prod-1", "quantity": 2, "unitPrice": 19.99 },
{ "productId": "prod-2", "quantity": 1, "unitPrice": 5.00 }
]
}'
----

== Configuration

Configuration is provided via `src/main/resources/META-INF/microprofile-config.properties`:

[source,properties]
----
mongodb.uri=mongodb://localhost:27017
mongodb.database=orders_db
server.port=8080
----

== Running locally

. Start MongoDB:
+
[source,bash]
----
docker run -d -p 27017:27017 mongo:7
----

. Build and run:
+
[source,bash]
----
mvn package
java -jar target/helidon-mongodb-cqrs-1.0-SNAPSHOT.jar
----

== Running tests

[source,bash]
----
mvn test
----
112 changes: 112 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.soujava</groupId>
<artifactId>helidon-mongodb-cqrs</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>helidon-mongodb-cqrs</name>
<description>CQRS Java sample using Helidon and MongoDB, following DDD and package-by-feature design</description>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<helidon.version>3.2.2</helidon.version>
<mongodb.version>4.11.1</mongodb.version>
<mainClass>com.soujava.helidon.cqrs.Main</mainClass>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.helidon</groupId>
<artifactId>helidon-bom</artifactId>
<version>${helidon.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- Helidon MicroProfile -->
<dependency>
<groupId>io.helidon.microprofile.bundles</groupId>
<artifactId>helidon-microprofile</artifactId>
</dependency>

<!-- MongoDB Java Driver -->
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>${mongodb.version}</version>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>io.helidon.microprofile.tests</groupId>
<artifactId>helidon-microprofile-tests-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<archive>
<manifest>
<mainClass>${mainClass}</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>libs/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/libs</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
<useBaseVersion>false</useBaseVersion>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
17 changes: 17 additions & 0 deletions src/main/java/com/soujava/helidon/cqrs/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.soujava.helidon.cqrs;

import io.helidon.microprofile.server.Server;

/**
* Application entry point. Bootstraps the Helidon MicroProfile server,
* which in turn starts the CDI container and registers all JAX-RS resources.
*/
public class Main {

private Main() {
}

public static void main(String[] args) {
Server.create().start();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.soujava.helidon.cqrs.order;

import com.soujava.helidon.cqrs.order.command.CancelOrderCommandHandler;
import com.soujava.helidon.cqrs.order.command.ConfirmOrderCommandHandler;
import com.soujava.helidon.cqrs.order.command.CreateOrderCommandHandler;
import com.soujava.helidon.cqrs.order.query.FindAllOrdersQueryHandler;
import com.soujava.helidon.cqrs.order.query.FindOrderByIdQueryHandler;
import com.soujava.helidon.cqrs.shared.command.SimpleCommandBus;
import com.soujava.helidon.cqrs.shared.query.SimpleQueryBus;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Initialized;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;

/**
* Registers all command and query handlers for the Order feature with the
* respective buses at application startup.
*
* <p>This keeps the bus decoupled from any specific feature while still
* allowing features to wire themselves in a single, explicit location.</p>
*/
@ApplicationScoped
public class OrderHandlerRegistrar {

@Inject
private SimpleCommandBus commandBus;

@Inject
private SimpleQueryBus queryBus;

@Inject
private CreateOrderCommandHandler createOrderCommandHandler;

@Inject
private ConfirmOrderCommandHandler confirmOrderCommandHandler;

@Inject
private CancelOrderCommandHandler cancelOrderCommandHandler;

@Inject
private FindOrderByIdQueryHandler findOrderByIdQueryHandler;

@Inject
private FindAllOrdersQueryHandler findAllOrdersQueryHandler;

void onApplicationStart(@Observes @Initialized(ApplicationScoped.class) Object event) {
commandBus.register(createOrderCommandHandler);
commandBus.register(confirmOrderCommandHandler);
commandBus.register(cancelOrderCommandHandler);

queryBus.register(findOrderByIdQueryHandler);
queryBus.register(findAllOrdersQueryHandler);
}
}
Loading