This project demonstrates Spring Modulith - a modular monolith architecture using Spring Boot. It showcases how to build a well-structured application with clear module boundaries, inter-module communication via Spring Events, and event externalization to Apache Kafka. Later on if needed, this one of module can be separated out into a Β΅-services.
- Package-based Module Separation: Application is organized into distinct modules (
customer,order,product) - Event-Driven Communication: Modules communicate using Spring's
ApplicationEventPublisher - Kafka Event Externalization: Internal events are published to Kafka topics using custom event publishers
- Event Publication Tracking: JDBC-based event publication registry automatically creates
event_publicationtable to track event status - OAuth2/OIDC Authentication: Secured with Keycloak for enterprise-grade authentication and authorization
- Role-Based Access Control: Fine-grained authorization using Keycloak realm and client roles
- Modularity Verification: Unit tests verify module structure and dependencies
- Auto-generated Documentation: PlantUML diagrams and module documentation generated from code
spring-modulith-demo/
βββ src/main/java/com/paxier/spring_modulith_demo/
β βββ SpringModulithDemoApplication.java
β βββ customer/ # Customer module (JPA)
β β βββ Address.java
β β βββ Customer.java
β β βββ CustomerController.java
β β βββ CustomerService.java
β β βββ CustomerRepository.java
β β βββ AddressRepository.java
β β βββ package-info.java
β βββ order/ # Order module
β β βββ Order.java
β β βββ OrderService.java
β β βββ OrderController.java
β β βββ OrderPlaceEvent.java # Externalized event for kafka
β β βββ OrderRepository.java
β β βββ LineItem.java
β β βββ package-info.java
| |
β βββ product/ # Product module
β βββ ProductsService.java
β β βββ package-info.java
βββ src/test/java/
βββ ModularityTests.java # Module structure verification
βββ customer/
βββ CustomerIntegrationTest.java
βββ CustomerServiceTest.java
βββ CustomerControllerTest.java
- Technology: Spring Data JPA with separate tables
- Entities:
Customer(customers table) andAddress(addresses table) - Relationship: OneToOne with cascade operations
- Endpoints:
POST /customers- Create customer with addressGET /customers- Get all customersGET /customers/{id}- Get customer by ID
- Testing: Full integration tests with H2 database
- Technology: Spring Data JDBC with event externalization
- Communication: Publishes
OrderPlaceEventto Kafka - Event Tracking: Uses
event_publicationtable
- Service Layer: Business logic for products
- Order Creation:
OrderControllerreceives HTTP POST request - Internal Event:
OrderServicepublishesOrderPlaceEventusingApplicationEventPublisher - Kafka Publishing: Annotation
@Externalizedautomatically send event to topicorder-created - Event Tracking: Spring Modulith JDBC stores spring publisher event status in
event_publicationtable
Spring Modulith automatically creates the event_publication table to track event publishing status:
CREATE TABLE event_publication (
id UUID PRIMARY KEY,
event_type VARCHAR(255),
listener_id VARCHAR(255),
publication_date TIMESTAMP,
serialized_event TEXT,
completion_date TIMESTAMP
);This enables:
- Reliability: Failed events can be republished on restart
- Observability: Track which events were published successfully
- Transactional Outbox Pattern: Events are stored in the same transaction as business data
- Java 21
- Maven 3.6+
- Docker & Docker Compose
-
Start Infrastructure (PostgreSQL + Kafka + Keycloak):
docker compose up -d
This will start:
- PostgreSQL on
localhost:5432 - Kafka on
localhost:9092 - Keycloak on
localhost:8180
- PostgreSQL on
-
Configure Keycloak (First-time setup):
Follow the detailed setup guide in KEYCLOAK_SETUP.md to:
- Create the
spring-modulithrealm - Configure OAuth2 client
- Create test users and roles
- Get access tokens
Quick Setup:
- Access Keycloak Admin Console: http://localhost:8180 (admin/admin)
- Create realm:
spring-modulith - Create client:
spring-modulith-client - Create user:
testuser/password - Assign roles:
user,admin
- Create the
-
Run the Application:
mvn spring-boot:run
The application will:
- Start on
http://localhost:8080 - Auto-create the
event_publicationtable - Connect to Kafka at
localhost:9092 - Validate JWT tokens from Keycloak
- Start on
-
Get Access Token:
TOKEN=$(curl -X POST http://localhost:8180/realms/spring-modulith/protocol/openid-connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "client_id=spring-modulith-client" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "username=testuser" \ -d "password=password" \ -d "grant_type=password" | jq -r '.access_token')
-
Create an Order (Protected Endpoint):
curl -X POST http://localhost:8080/orders \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "orderId": 1, "lineItems": [ {"id": 1, "product": 101, "quantity": 2} ] }'
-
Create a Customer with Address (Protected Endpoint):
curl -X POST http://localhost:8080/customers \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "John Doe", "address": { "street": "123 Main Street", "city": "New York", "zipCode": "10001" } }'
This demonstrates JPA's OneToOne relationship - Customer and Address are saved in separate tables but linked via foreign key.
-
Get All Customers:
curl http://localhost:8080/customers
-
Verify Kafka Message:
# Enter Kafka container docker exec -it <kafka-container-id> bash # Consume messages from topic kafka-console-consumer --bootstrap-server localhost:9092 \ --topic order-created --from-beginning
You'll see JSON messages like:
{ "orderId": 1, "lineItems": [ {"id": 1, "product": 101, "quantity": 2} ] }
Run tests to verify module structure:
./mvnw testThe ModularityTests class:
- Verifies module boundaries: Ensures modules only access allowed dependencies
- Generates documentation: Creates PlantUML diagrams in
target/spring-modulith-docs/ - Validates structure: Fails if modules violate architectural rules
After running tests, check target/spring-modulith-docs/:
components.puml- Overall architecture diagrammodule-order.puml- Order module diagrammodule-customer.puml- Customer module diagrammodule-product.puml- Product module diagramall-docs.adoc- Complete documentation
spring:
modulith:
events:
republish-outstanding-events-on-restart: true # Retry failed events
jdbc:
schema-initialization:
enabled: true # Auto-create event_publication table
kafka:
bootstrap-servers: localhost:9092
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8180/realms/spring-modulith
jwk-set-uri: http://localhost:8180/realms/spring-modulith/protocol/openid-connect/certsThe following endpoints don't require authentication:
/actuator/**- Actuator endpoints/apidoc/**- Swagger UI/v3/api-docs/**- OpenAPI documentation/swagger-ui/**- Swagger UI resources
All other endpoints require a valid JWT token from Keycloak.
This application uses OAuth2/OIDC with Keycloak for authentication and authorization.
- User logs in to Keycloak (or gets token programmatically)
- Keycloak issues JWT token with user information and roles
- Client sends token in
Authorization: Bearer <token>header - Spring validates token against Keycloak's public keys
- Roles are extracted from token claims (realm_access, resource_access)
- Access granted/denied based on user's roles
You can secure methods using @PreAuthorize:
@PreAuthorize("hasRole('admin')")
public void deleteOrder(int orderId) {
// Only admins can delete
}
@PreAuthorize("hasAnyRole('user', 'customer')")
public Order getOrder(int orderId) {
// Users and customers can view
}See KEYCLOAK_SETUP.md for detailed instructions on:
- Creating realm and client
- Managing users and roles
- Getting access tokens
- Testing with curl/Postman
Each top-level package under the main package is a module:
com.paxier.spring_modulith_demo.customerβ Customer modulecom.paxier.spring_modulith_demo.orderβ Order modulecom.paxier.spring_modulith_demo.productβ Product module
Modules communicate via Spring Application Events:
// Publishing module
publisher.publishEvent(new OrderPlaceEvent(orderId, lineItems));
// Subscribing module
@ApplicationModuleListener
void onOrderPlaced(OrderPlaceEvent event) {
// Handle event
}The event_publication table tracks:
- Pending events: Not yet published to external systems
- Completed events: Successfully published
- Failed events: Can be republished on restart
Spring Modulith provides Actuator endpoints to view the application module structure and metadata:
View Module Structure:
curl http://localhost:8080/actuator/modulithThis endpoint shows:
- Module boundaries and relationships
- Module dependencies
- Event listeners and publishers
- Externalized events configuration
SELECT * FROM event_publication;docker exec -it <kafka-container-id> kafka-topics \
--list --bootstrap-server localhost:9092docker exec -it <kafka-container-id> kafka-console-consumer \
--bootstrap-server localhost:9092 \
--topic order-created \
--from-beginning- Spring Boot 3.5.7
- Spring Modulith 1.4.3
- Spring Security 6.x with OAuth2 Resource Server
- Keycloak 26.0.7 for Authentication & Authorization
- Spring Data JDBC & Spring Data JPA
- Spring Kafka
- PostgreSQL
- Apache Kafka 7.6.1
- Java 21
This is a demo project for learning purposes.
