Design Patterns
Design patterns in the domain model layer provide solutions to common persistence challenges.
Audit Fields Pattern
Problem: Need to track who created/modified records and when for compliance.
Solution: Add immutable creation fields and updateable timestamp fields.
@Column(name = "CREATED_BY", nullable = false, updatable = false)
private String createdBy; // Set from SecurityContext, never changes
@CreationTimestamp
@Column(name = "CREATED_AT", updatable = false)
private LocalDateTime createdAt; // Auto-set on INSERT, immutable
@UpdateTimestamp
@Column(name = "UPDATED_AT")
private LocalDateTime updatedAt; // Auto-set on INSERT and UPDATEBenefits: - Complete audit trail of who did what and when - Immutability prevents accidental or malicious changes - Automatic timestamp management (no manual code) - Compliance with data protection regulations
Optimistic Locking Pattern
Problem: Multiple users editing the same record simultaneously can cause conflicts.
Solution: Use version field to detect concurrent modifications.
@Version
@Column(name = "VERSION")
private Long version; // Incremented on each updateHow It Works:
User A reads Supplier (version = 3)
User B reads Supplier (version = 3)
User B updates Supplier → version = 4
User A tries to update → FAILS (version mismatch)
User A must refresh and retry
Benefits: - No database locks needed (read/write efficiency) - Detects concurrent modifications automatically - Forces retry logic on conflicts - Better performance than pessimistic locking
Usage:
// Repository handles OptimisticLockException
try {
supplierService.update(id, dto);
} catch (OptimisticLockException e) {
// Retry or notify user to refresh and try again
}Soft Deletes Pattern
Problem: Need to delete records but maintain referential integrity and audit trails.
Solution: Mark records as deleted instead of removing them.
@Column(name = "DELETED_AT")
private LocalDateTime deletedAt; // NULL = active, not NULL = deleted
// Query for active records only
@Query("SELECT s FROM Supplier s WHERE s.deletedAt IS NULL")
List<Supplier> findActive();
// Include in where clauses
@Query("SELECT s FROM Supplier s WHERE s.deletedAt IS NULL AND s.name = :name")
Optional<Supplier> findActiveByName(@Param("name") String name);Benefits: - Preserves historical data and audit trails - Prevents cascading constraint violations - Can restore deleted records if needed - Maintains referential integrity
Trade-offs: - Every query must filter
deletedAt IS NULL - Database size grows larger -
Requires discipline in query writing
Denormalization for Analytics Pattern
Problem: Analytics queries require expensive JOINs across multiple tables.
Solution: Duplicate frequently-needed fields in audit/history tables.
// Instead of:
// SELECT sh.*, ii.name, s.name FROM stock_history sh
// JOIN inventory_item ii ON sh.item_id = ii.id
// JOIN supplier s ON ii.supplier_id = s.id
// Denormalize supplier into stock_history:
@Column(name = "SUPPLIER_ID")
private String supplierId; // Redundant but avoids joins
// Now queries are simpler and faster:
// SELECT * FROM stock_history WHERE supplier_id = 'abc123'Benefits: - Significantly faster analytics queries - Reduced database CPU usage - Simpler query logic - Better scalability for reporting
Trade-offs: - Data redundancy (storage cost) - Must keep denormalized fields in sync - Adds complexity to write operations
Synchronization Example:
// When creating stock history, also store supplier ID
StockHistory history = StockHistory.builder()
.itemId(item.getId())
.supplierId(item.getSupplierId()) // Denormalized
.reason(StockChangeReason.SALE)
.oldQuantity(previous)
.newQuantity(current)
.build();