Persistence Modeling Guidelines
Principle
Model the persistence layer for data storage, not for business logic. The persistence layer is an implementation detail of the application. It should remain isolated from both the domain model and the presentation layer.
These guidelines aim to create a persistence layer that is:
- Simple
- Explicit
- Testable
- Performant
- Replaceable
- Independent of business logic
The persistence layer should never dictate the architecture of the application.
Guiding Principles
Section titled “Guiding Principles”Persistence is an Adapter
Section titled “Persistence is an Adapter”In a Hexagonal Architecture, persistence is an infrastructure concern.
┌──────────────────────────────┐│ Presentation │└──────────────┬───────────────┘ │┌──────────────▼───────────────┐│ Application │└──────────────┬───────────────┘ │ Repository Contract┌──────────────▼───────────────┐│ Domain │└──────────────┬───────────────┘ │───────────────┼────────────────────── │┌──────────────▼───────────────┐│ Persistence Adapter ││ Quarkus + Hibernate ││ PostgreSQL │└──────────────────────────────┘The domain never knows how data is stored.
Choose the Right Persistence Strategy
Section titled “Choose the Right Persistence Strategy”Not every use case requires the same persistence approach.
| Use Case | Recommended Approach |
|---|---|
| Simple CRUD | Quarkus Data Repository |
| Aggregate Loading | Repository + Entity Mapping |
| Complex Filtering | Fluent SQL Builder |
| Reporting | Raw SQL |
| Analytics | Raw SQL |
| Bulk Updates | Raw SQL |
| Bulk Imports | Batch SQL |
| Data Export | Streaming SQL |
| Read Models | DTO/Projection |
Rule
Use the simplest persistence mechanism that solves the problem.
Do not force every query through Hibernate.
Repository Responsibilities
Section titled “Repository Responsibilities”Repositories exist to retrieve and persist aggregates.
Repositories are not query engines.
Repositories are not business services.
Repositories are not report generators.
Good:
interface CustomerRepository {
fun findById(id: CustomerId): Customer?
fun save(customer: Customer)
}Bad:
interface CustomerRepository {
fun findCustomersByAgeAndCityAndStatusAndRegistrationDate(...)
fun exportCustomerReport(...)
fun calculateRevenue(...)
}Persistence Entities
Section titled “Persistence Entities”Persistence entities represent the database schema.
Nothing more.
@Entity@Table(name = "customers")class CustomerEntity(
@Id val id: UUID,
val firstName: String,
val lastName: String,
val email: String)Persistence entities:
- represent tables
- contain persistence annotations
- should remain inside the persistence adapter
They should never leak into:
- Application
- Domain
- REST API
Domain Models
Section titled “Domain Models”Domain models represent business concepts.
data class Customer(
val id: CustomerId,
val name: CustomerName,
val email: EmailAddress
)A domain model should never contain:
- @Entity
- @Column
- @ManyToOne
- @OneToMany
- Hibernate-specific annotations
Separate Models
Section titled “Separate Models”Every architectural layer owns its own model.
REST DTO
↓
Domain Model
↓
Persistence EntityEach model has a different responsibility.
| Model | Purpose |
|---|---|
| DTO | API contract |
| Domain | Business rules |
| Entity | Database storage |
Mapping
Section titled “Mapping”Mapping is required at architectural boundaries.
Avoid unnecessary mapping inside a single layer.
Good:
DTO
↓
Domain
↓
EntityBad:
DTO
↓
Command
↓
Model
↓
BusinessModel
↓
PersistenceModel
↓
EntityEvery additional mapping should have a clear architectural reason.
Simple CRUD should use Quarkus Data repositories.
@ApplicationScopedclass CustomerRepositoryImpl( private val repository: CustomerJpaRepository) : CustomerRepository {
override fun findById(id: CustomerId): Customer? = repository.findById(id.value) ?.toDomain()
}Keep repositories small.
Keep repositories focused.
Complex Queries
Section titled “Complex Queries”Complex filtering should not abuse ORM.
Prefer explicit SQL generation.
Example:
val query = CustomerQueryBuilder() .active() .locatedIn(country) .registeredAfter(date) .build()This keeps queries:
- composable
- readable
- testable
Reporting
Section titled “Reporting”Reporting is fundamentally different from aggregate loading.
Use SQL.
val sql = """SELECT customer_name, revenueFROM customer_revenueWHERE year = ?"""Do not construct enormous object graphs just to generate reports.
Read Models
Section titled “Read Models”When only a subset of data is required, use projections.
Good:
data class CustomerSummary(
val id: UUID,
val name: String
)Instead of:
CustomerEntitywhen only two columns are required.
Bulk Operations
Section titled “Bulk Operations”ORMs are optimized for aggregates.
Not millions of rows.
Use SQL for:
- imports
- exports
- batch updates
- batch deletes
Good:
UPDATE ordersSET archived = trueWHERE created_at < ?Bad:
orders.forEach {
it.archived = true
repository.persist(it)
}Streaming
Section titled “Streaming”Never load huge datasets into memory.
Prefer streaming.
Example:
connection.prepareStatement(sql).use { statement ->
statement.fetchSize = 500
val rs = statement.executeQuery()
while (rs.next()) {
// process row
}}Transactions
Section titled “Transactions”Transactions belong to the application layer.
Keep transactions:
- short
- explicit
- focused
Never:
- call external APIs
- send emails
- publish events synchronously
inside a database transaction.
Performance
Section titled “Performance”Performance rules:
- Simplicity first
- Measure before optimizing
- Avoid N+1 queries
- Fetch only required columns
- Use indexes intentionally
- Prefer SQL for heavy workloads
Testing
Section titled “Testing”Persistence should primarily be tested through integration tests.
Preferred:
- Testcontainers
- PostgreSQL
- Real SQL
- Real transactions
Avoid mocking persistence behavior.
✅ Keep persistence inside the infrastructure layer.
✅ Keep entities persistence-only.
✅ Map at architectural boundaries.
✅ Prefer repositories for CRUD.
✅ Use SQL for reporting.
✅ Use SQL for analytics.
✅ Stream large datasets.
✅ Use projections for read models.
✅ Measure performance.
✅ Write integration tests.
Don’ts
Section titled “Don’ts”❌ Put business logic inside entities.
❌ Expose entities through REST.
❌ Share entities with the domain.
❌ Load entire aggregates for reports.
❌ Use Hibernate for bulk updates.
❌ Load millions of rows into memory.
❌ Build repositories with hundreds of query methods.
❌ Create unnecessary mapping layers.
❌ Optimize before measuring.
❌ Mock your database in integration tests.
AI Agent Guidelines
Section titled “AI Agent Guidelines”When generating persistence code, AI assistants must follow these rules.
Architecture
Section titled “Architecture”- Treat persistence as an infrastructure adapter.
- Never leak persistence entities outside the persistence layer.
- Respect Hexagonal Architecture.
Repository Design
Section titled “Repository Design”Generate repositories only for aggregate persistence.
Do not generate repositories that contain business logic.
Query Selection
Section titled “Query Selection”Choose the persistence strategy based on the use case.
| Situation | Preferred Solution |
|---|---|
| CRUD | Quarkus Data Repository |
| Search | Fluent SQL Builder |
| Reporting | SQL |
| Export | Streaming SQL |
| Analytics | SQL |
Mapping
Section titled “Mapping”Generate only necessary mappings.
Never introduce extra DTOs or models without architectural value.
Performance
Section titled “Performance”Always prefer:
- projections over entities
- SQL over ORM for reporting
- streaming over loading collections
- batch operations over row-by-row updates
Code Style
Section titled “Code Style”Generated code should be:
- explicit
- readable
- immutable where possible
- easy to test
- free from framework magic
Avoid introducing abstractions that are not justified by the architecture.
Summary
Section titled “Summary”The persistence layer exists to store and retrieve data.
It is not the domain.
It is not the API.
It is not the business layer.
Choose the simplest persistence mechanism that fits the use case, keep persistence concerns isolated, and optimize only where measurements demonstrate the need.