Mateu in hexagonal architecture
Mateu fits naturally in systems designed with:
- DDD tactical patterns
- hexagonal architecture
- CQRS
- event-driven integration
- microservices by bounded context
The key idea is simple:
The UI is just another inbound adapter.
It belongs next to APIs, event consumers and other inbound ports.
System structure
A typical system can be organized like this:
Application
├─ use cases
├─ ports / interfaces
│ ├─ queries
│ ├─ repositories
│ └─ gateways
Domain
├─ aggregates
├─ entities
├─ value objects
└─ domain services
Infrastructure
├─ in
│ ├─ api
│ ├─ async consumers
│ └─ ui ← Mateu
└─ out
├─ persistence
└─ gateways
Mateu lives in in/ui.
It adapts user interaction into application use cases and query calls.
Why this matters
In many systems, teams build APIs only because the frontend needs them.
With Mateu, that is not always necessary.
If the UI is an inbound adapter, it can call:
- application use cases
- query services
- repositories through ports
- gateways through ports
directly from the backend.
If the UI is just another inbound adapter, you do not need to build an API only for your UI.
This reduces duplicated contracts, duplicated models and unnecessary glue code.
CQRS fit
Mateu works very well with CQRS.
Write side
Use DDD and aggregates for commands:
Button / ColumnAction
↓
Application use case
↓
Aggregate
↓
Repository
↓
Events
The domain protects invariants.
Business rules stay as close to the domain as possible:
- value object if possible
- aggregate if needed
- domain service only when the logic exceeds one aggregate
Read side
Use queries and DTOs for listings and screens:
Query service
↓
DTO / projection
↓
Row model
↓
ListingData
↓
Mateu UI
The read side does not need domain entities.
It can use:
- JDBC
- SQL projections
- denormalized read models
- external APIs
- gRPC clients
UI rows are read models
A listing row is a UI read model.
For example:
public record ChangeRow(
@Hidden String id,
String page,
String country,
String language,
Status status,
ColumnAction action
) implements Identifiable {}
This row is not a domain entity.
It is a representation optimized for the UI.
It can contain:
- formatted values
- status badges
- hidden ids
- row actions
- derived fields
Actions call use cases
Actions should express intent.
new ColumnAction("compare", "Compare")
The action id is a contract between the UI and the backend.
The backend decides what happens:
compare
↓
ComparePagesUseCase
↓
domain / query / workflow
This keeps logic out of the frontend.
Lookups use query services
Lookups are also part of the read side:
@Lookup(search = LabelOptionsSupplier.class, label = LabelLabelSupplier.class)
String labelId;
The suppliers can call query services:
LookupOptionsSupplier
↓
Query service
↓
DTOs
↓
Option
This keeps the UI decoupled from domain entities.
Microservices
A good default is:
- one microservice per bounded context or subdomain
- not one microservice per technical component
Each microservice can own:
- its domain
- its database
- its use cases
- its read models
- its Mateu UI module
Mateu then allows these UI modules to be composed into a distributed backoffice.
Database strategy
A common pattern is:
Write side
→ DDD aggregates
→ repositories
→ JPA / ORM if useful
Read side
→ query services
→ JDBC / SQL / projections
→ DTOs / rows
For cross-service joins, use a read database fed by events from the different services.
Events, outbox and inbox
For reliable event-driven systems:
- use an outbox to publish events atomically with state changes
- use an inbox to avoid processing the same event twice
This keeps integration reliable without coupling services tightly.
Mateu does not replace these patterns.
It fits on top of them as an inbound UI adapter.
Stateless UI
Mateu does not keep UI state on the server.
Each request:
- instantiates the view model
- hydrates it
- executes the action
- returns the result
This fits naturally with:
- ephemeral pods
- horizontal scaling
- microservices
- no sticky sessions
Value objects
A useful rule:
Value Object
→ never null
→ always valid
Field
→ decides whether the value exists
This keeps domain correctness inside the domain model.
The UI can expose optional fields, but once a value object exists, it should be valid.
Mental model
Mateu is not a layer outside the architecture.
It is part of the architecture:
User
↓
Mateu UI adapter
↓
Application use cases / query services
↓
Domain / read model / gateways
This is why Mateu works especially well for business UIs.
It does not force you to create a separate frontend application.
It lets your backend architecture expose a UI directly.