How salahuddin09/hexagonal-kafka-outbox works
1. This is a Spring Boot hexagonal application centered on an order-processing workflow. The core runtime is Java 25 with Spring Web MVC, Spring Data JPA, Spring Validation, Spring Mail, Flyway, and Apache Kafka. The main infrastructure dependencies are PostgreSQL for state, Kafka for async events, and SMTP/Mailpit for email delivery.
2. The inbound HTTP boundary is under adapter.in.web. It exposes REST controllers for users, products, orders, and outbox events. OrderController is the primary write entrypoint and returns 202 Accepted for order creation, emphasizing asynchronous processing rather than immediate completion.
3. The application layer under application defines the use-case ports and orchestration services. CreateOrderService, ListOrdersService, ListUsersService, ListProductsService, and ListOutboxEventsService are the main application services. Ports split the direction of dependency: inbound use cases (CreateOrderUseCase, etc.) and outbound persistence/messaging contracts (OrderPersistencePort, OutboxEventPort, EmailSenderPort, and query ports for users/products).
4. The domain layer under domain contains the business model and enums. Key aggregates and events include Order, OrderItem, User, Product, OutboxEvent, OrderCreatedEvent, and invoice-related models such as InvoiceEmail and InvoiceEmailOutboxPayload. Status enums like OrderStatus and OutboxStatus drive lifecycle transitions.
5. Persistence is isolated in adapter.out.persistence. It maps domain objects to JPA entities such as OrderJpaEntity, OrderItemJpaEntity, UserJpaEntity, ProductJpaEntity, OutboxEventJpaEntity, and InvoiceEmailJpaEntity, with Spring Data repositories behind them. PersistenceMapper and the adapter classes translate between domain and database shapes, while Flyway migrations define the schema in src/main/resources/db/migration.
6. The key reliability mechanism is the outbox table. When an order is created, the application stores the order and an ORDER_CREATED record in outbox_events within the same transaction. That table also holds retry metadata such as attempt count, status, error message, and timestamps, which makes downstream publishing retryable and observable.
7. adapter.out.messaging contains the asynchronous delivery side. OutboxPublisher is the scheduled poller that reads pending outbox rows and dispatches them through type-specific publishers. For order events it uses KafkaOrderEventPublisher; for invoice-email events it uses EmailInvoiceOutboxDispatcher and SmtpInvoiceEmailSender.
8. Kafka is used as the first async hop after order creation. OrderCreatedInvoiceConsumer in adapter.in.messaging consumes the order-created topic and kicks off invoice-email generation. This consumer bridges the Kafka event back into an application use case (CreateInvoiceEmailFromOrderCreatedEventService) rather than handling business logic directly.
9. The invoice-email flow is a second outbox-backed stage. The consumer builds invoice content using helpers like LoadOrderUserService, LoadOrderProductsService, and InvoiceEmailBodyFactory, persists an invoice_emails record, and emits an EMAIL_INVOICE_REQUESTED outbox event. That event is later picked up by OutboxPublisher, sent via SMTP, and the order can move to INVOICE_DELIVERED.
10. Configuration is centralized in config. KafkaProducerConfiguration, MailConfiguration, AsyncConfiguration, ModelMapperConfiguration, and OpenApiConfiguration wire the infrastructure. Runtime behavior can be toggled with flags such as enabling the invoice consumer or the outbox publisher, which is important for diagramming optional async subsystems.
11. The project also has a small bootstrap/data-seeding concern in DataFixtureConfiguration, which inserts default user/product rows only when the tables are empty. That means the application can start with usable catalog data without depending on manual setup.
12. Operationally, the app exposes Swagger/OpenAPI and Actuator health endpoints, but the architectural observability focus is Kafka UI for topic inspection and Mailpit for captured emails. The test suite uses Testcontainers against PostgreSQL and Kafka, so the integration boundary is intentionally exercised against real infrastructure rather than mocks.
Main entry points: CreateOrderService.java Create order · CreateInvoiceEmailFromOrderCreatedEventService.java Invoice flow · CreateOrderUseCase.java Order ports · OutboxPublisher.java Outbox poller
Components 21
Inbound adapters
HTTP and Kafka entrypoints into the application
- Order API · Creates orders asynchronously and exposes the main write path
src/main/java/com/api/sample/adapter/in/web/OrderController.java - Catalog APIs · Read endpoints for users, products, and outbox events
src/main/java/com/api/sample/adapter/in/web/UserController.java - Invoice consumer · Consumes order-created events and starts invoice email generation
src/main/java/com/api/sample/adapter/in/messaging/OrderCreatedInvoiceConsumer.java
Application core
Use cases, orchestration services, and ports
- Create order · Coordinates order creation and outbox recording
src/main/java/com/api/sample/application/usecase/CreateOrderService.java - Query services · Lists orders, users, products, and outbox events
src/main/java/com/api/sample/application/usecase/ListOrdersService.java - Invoice flow · Builds invoice email content from order-created events
src/main/java/com/api/sample/application/usecase/CreateInvoiceEmailFromOrderCreatedEventService.java - Order ports · Use-case and persistence contracts for orders, outbox, and catalog queries
src/main/java/com/api/sample/application/port/in/CreateOrderUseCase.java - Invoice ports · Contracts for invoice email persistence and delivery
src/main/java/com/api/sample/application/port/out/InvoiceEmailPort.java
Domain model
Business entities, events, and lifecycle enums
- Orders · Order, order item, status, and order-created event model
src/main/java/com/api/sample/domain/model/Order.java - Outbox · Outbox entries, types, statuses, and retry metadata
src/main/java/com/api/sample/domain/model/OutboxEvent.java - Invoices · Invoice email payload and invoice email aggregate
src/main/java/com/api/sample/domain/model/InvoiceEmail.java - Catalog · Users and products used by order and invoice workflows
src/main/java/com/api/sample/domain/model/User.java
Outbound adapters
Persistence and messaging implementations
- Order store · Maps orders and catalog entities to JPA repositories
src/main/java/com/api/sample/adapter/out/persistence/OrderPersistenceAdapter.java - Outbox store · Persists and queries outbox events with retry state
src/main/java/com/api/sample/adapter/out/persistence/OutboxEventPersistenceAdapter.java - Invoice store · Persists invoice emails for the second async stage
src/main/java/com/api/sample/adapter/out/persistence/InvoiceEmailPersistenceAdapter.java - Outbox poller · Reads pending outbox rows and routes them to type-specific publishers
src/main/java/com/api/sample/adapter/out/messaging/OutboxPublisher.java - Kafka publisher · Publishes order-created events to Kafka
src/main/java/com/api/sample/adapter/out/messaging/KafkaOrderEventPublisher.java - Email dispatcher · Delivers invoice-email outbox events through SMTP
src/main/java/com/api/sample/adapter/out/messaging/EmailInvoiceOutboxDispatcher.java - SMTP sender · Sends invoice emails to the configured mail server
src/main/java/com/api/sample/adapter/out/messaging/SmtpInvoiceEmailSender.java
Platform config
Spring wiring, bootstrap, and runtime integration
- App config · Kafka, mail, async, mapper, and OpenAPI wiring
src/main/java/com/api/sample/config/KafkaProducerConfiguration.java - Data fixture · Seeds default users and products when tables are empty
src/main/java/com/api/sample/adapter/out/persistence/DataFixtureConfiguration.java
Connections 20
- Order API → Create order: create order · REST write request enters the create-order use case
- Catalog APIs → Query services: read data · User, product, and outbox endpoints delegate to query services
- Invoice consumer → Invoice flow: consume event · Kafka message triggers invoice-email orchestration
- Create order → Order ports: uses ports · Order creation depends on inbound use case and outbound persistence contracts
- Invoice flow → Invoice ports: uses ports · Invoice flow persists invoice emails and accesses supporting data through ports
- Query services → Order ports: read ports · Query services rely on persistence/query ports
- Order ports → Order store: implemented by · Persistence adapter fulfills order and catalog persistence contracts
- Order ports → Outbox store: implemented by · Outbox persistence adapter fulfills outbox contract
- Invoice ports → Invoice store: implemented by · Invoice persistence adapter stores invoice-email records
- Create order → Orders: creates · Order use case produces order aggregate and order-created event
- Create order → Outbox: writes outbox · Order creation stores an order-created outbox record in the same transaction
- Invoice consumer → Outbox poller: async stage · Invoice generation is followed by outbox-driven delivery
- Outbox poller → Kafka publisher: routes order events · Poller dispatches order-created outbox records to Kafka
- Outbox poller → Email dispatcher: routes invoice events · Poller dispatches invoice-email outbox records to email delivery
- Email dispatcher → SMTP sender: sends mail · Dispatcher hands invoice delivery to the SMTP sender
- Invoice flow → Invoices: builds invoice · Invoice use case produces invoice email content and payloads
- Invoice flow → Catalog: loads data · Invoice flow loads user and product details for the email body
- App config → Outbox poller: enables · Runtime flags can turn the poller on or off
- App config → Invoice consumer: enables · Runtime flags can turn the invoice consumer on or off
- Data fixture → Catalog: seeds · Bootstrap configuration inserts default catalog rows