Hibernate Enterprise Patterns

Advanced Architecture Patterns for Large-Scale Applications
Enterprise applications require sophisticated patterns to handle complexity, scalability, and maintainability. This guide covers essential Hibernate enterprise patterns including Repository, Unit of Work, Domain-Driven Design, CQRS, and Event Sourcing with practical implementations.
Enterprise Pattern Overview
Core Enterprise Patterns
- Repository Pattern: Abstraction layer for data access
- Unit of Work: Transaction management and change tracking
- Domain-Driven Design: Rich domain models with business logic
- CQRS: Command Query Responsibility Segregation
- Event Sourcing: Event-driven architecture with audit trails
- Specification Pattern: Encapsulating business rules
1. Repository Pattern Implementation
Generic Repository Base
A robust generic repository provides a foundation for all data access operations while maintaining type safety and consistency.
Generic Repository Interface
public interface GenericRepository<T, ID> {
T save(T entity);
Optional<T> findById(ID id);
List<T> findAll();
List<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
List<T> findAll(Specification<T> spec);
Page<T> findAll(Specification<T> spec, Pageable pageable);
long count();
long count(Specification<T> spec);
boolean existsById(ID id);
void deleteById(ID id);
void delete(T entity);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}
Generic Repository Implementation
JPA Generic Repository
@Repository
public class JpaGenericRepository<T, ID> implements GenericRepository<T, ID> {
@PersistenceContext
private EntityManager entityManager;
private final Class<T> entityClass;
@SuppressWarnings("unchecked")
public JpaGenericRepository() {
this.entityClass = (Class<T>) ((ParameterizedType) getClass()
.getGenericSuperclass()).getActualTypeArguments()[0];
}
@Override
@Transactional
public T save(T entity) {
if (entityManager.contains(entity)) {
return entityManager.merge(entity);
} else {
entityManager.persist(entity);
return entity;
}
}
@Override
public Optional<T> findById(ID id) {
return Optional.ofNullable(entityManager.find(entityClass, id));
}
@Override
public List<T> findAll() {
CriteriaQuery<T> query = entityManager.getCriteriaBuilder()
.createQuery(entityClass);
return entityManager.createQuery(query).getResultList();
}
@Override
public List<T> findAll(Specification<T> spec) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<T> query = builder.createQuery(entityClass);
Root<T> root = query.from(entityClass);
Predicate predicate = spec.toPredicate(root, query, builder);
if (predicate != null) {
query.where(predicate);
}
return entityManager.createQuery(query).getResultList();
}
@Override
public Page<T> findAll(Specification<T> spec, Pageable pageable) {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<T> query = builder.createQuery(entityClass);
Root<T> root = query.from(entityClass);
Predicate predicate = spec.toPredicate(root, query, builder);
if (predicate != null) {
query.where(predicate);
}
// Apply sorting
if (pageable.getSort().isSorted()) {
List<Order> orders = new ArrayList<>();
pageable.getSort().forEach(order -> {
if (order.isAscending()) {
orders.add(builder.asc(root.get(order.getProperty())));
} else {
orders.add(builder.desc(root.get(order.getProperty())));
}
});
query.orderBy(orders);
}
// Apply pagination
List<T> content = entityManager.createQuery(query)
.setFirstResult((int) pageable.getOffset())
.setMaxResults(pageable.getPageSize())
.getResultList();
// Count total elements
long total = count(spec);
return new PageImpl<>(content, pageable, total);
}
}
Domain-Specific Repository
Product Repository
@Repository
public class ProductRepository extends JpaGenericRepository<Product, Long> {
public List<Product> findByCategoryAndPriceRange(String category,
BigDecimal minPrice,
BigDecimal maxPrice) {
return findAll(ProductSpecifications.byCategoryAndPriceRange(category, minPrice, maxPrice));
}
public Page<Product> findActiveProducts(Pageable pageable) {
return findAll(ProductSpecifications.isActive(), pageable);
}
public List<Product> findProductsWithLowStock(int threshold) {
return findAll(ProductSpecifications.hasLowStock(threshold));
}
@Query("SELECT p FROM Product p WHERE p.category.name = :categoryName " +
"AND p.price BETWEEN :minPrice AND :maxPrice")
List<Product> findProductsByCategoryAndPriceRange(@Param("categoryName") String categoryName,
@Param("minPrice") BigDecimal minPrice,
@Param("maxPrice") BigDecimal maxPrice);
}
2. Unit of Work Pattern
Unit of Work Implementation
The Unit of Work pattern maintains a list of objects affected by a business transaction and coordinates writing out changes and resolving concurrency problems.
Unit of Work Interface
public interface UnitOfWork {
void registerNew(Object entity);
void registerDirty(Object entity);
void registerRemoved(Object entity);
void commit();
void rollback();
void clear();
boolean hasChanges();
List<Object> getNewEntities();
List<Object> getDirtyEntities();
List<Object> getRemovedEntities();
}
Hibernate Unit of Work
Unit of Work Implementation
@Component
@Transactional
public class HibernateUnitOfWork implements UnitOfWork {
@PersistenceContext
private EntityManager entityManager;
private final Set<Object> newEntities = new HashSet<>();
private final Set<Object> dirtyEntities = new HashSet<>();
private final Set<Object> removedEntities = new HashSet<>();
@Override
public void registerNew(Object entity) {
if (!dirtyEntities.contains(entity) && !removedEntities.contains(entity)) {
newEntities.add(entity);
}
}
@Override
public void registerDirty(Object entity) {
if (!newEntities.contains(entity) && !removedEntities.contains(entity)) {
dirtyEntities.add(entity);
}
}
@Override
public void registerRemoved(Object entity) {
if (newEntities.remove(entity)) {
return;
}
dirtyEntities.remove(entity);
if (!removedEntities.contains(entity)) {
removedEntities.add(entity);
}
}
@Override
public void commit() {
// Persist new entities
for (Object entity : newEntities) {
entityManager.persist(entity);
}
// Update dirty entities
for (Object entity : dirtyEntities) {
entityManager.merge(entity);
}
// Remove deleted entities
for (Object entity : removedEntities) {
entityManager.remove(entity);
}
// Clear tracking sets
clear();
}
@Override
public void rollback() {
clear();
entityManager.clear();
}
@Override
public void clear() {
newEntities.clear();
dirtyEntities.clear();
removedEntities.clear();
}
@Override
public boolean hasChanges() {
return !newEntities.isEmpty() || !dirtyEntities.isEmpty() || !removedEntities.isEmpty();
}
}
3. Domain-Driven Design with Hibernate
Rich Domain Model
Order Aggregate Root
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
private OrderNumber orderNumber;
@Embedded
private CustomerInfo customerInfo;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
@Enumerated(EnumType.STRING)
private OrderStatus status;
@Embedded
private Money totalAmount;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
// Business methods
public void addItem(Product product, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot modify order in " + status + " status");
}
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
OrderItem item = new OrderItem(this, product, quantity);
items.add(item);
recalculateTotal();
}
public void removeItem(OrderItem item) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot modify order in " + status + " status");
}
items.remove(item);
recalculateTotal();
}
public void confirm() {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot confirm order in " + status + " status");
}
if (items.isEmpty()) {
throw new IllegalStateException("Cannot confirm empty order");
}
status = OrderStatus.CONFIRMED;
// Publish domain event
DomainEvents.publish(new OrderConfirmedEvent(this));
}
public void cancel(String reason) {
if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED) {
throw new IllegalStateException("Cannot cancel order in " + status + " status");
}
status = OrderStatus.CANCELLED;
// Publish domain event
DomainEvents.publish(new OrderCancelledEvent(this, reason));
}
private void recalculateTotal() {
Money total = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
this.totalAmount = total;
}
// Getters and other methods...
}
Value Objects
Money Value Object
@Embeddable
public class Money {
@Column(name = "amount", precision = 19, scale = 2)
private BigDecimal amount;
@Column(name = "currency", length = 3)
private String currency;
public static final Money ZERO = new Money(BigDecimal.ZERO, "USD");
protected Money() {} // For JPA
public Money(BigDecimal amount, String currency) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount must be non-negative");
}
if (currency == null || currency.length() != 3) {
throw new IllegalArgumentException("Currency must be 3 characters");
}
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(BigDecimal factor) {
return new Money(this.amount.multiply(factor), this.currency);
}
public boolean isGreaterThan(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot compare different currencies");
}
return this.amount.compareTo(other.amount) > 0;
}
// Getters, equals, hashCode...
}
4. CQRS Implementation
Command and Query Separation
CQRS separates read and write operations, allowing for optimized data models and improved scalability.
Command Handler
@Component
@Transactional
public class CreateOrderCommandHandler {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ProductRepository productRepository;
@Autowired
private DomainEventPublisher eventPublisher;
public OrderId handle(CreateOrderCommand command) {
// Validate command
validateCommand(command);
// Create order aggregate
Order order = new Order(
new OrderNumber(command.getOrderNumber()),
new CustomerInfo(command.getCustomerId(), command.getCustomerName()),
command.getShippingAddress()
);
// Add items
for (CreateOrderItemCommand itemCommand : command.getItems()) {
Product product = productRepository.findById(itemCommand.getProductId())
.orElseThrow(() -> new ProductNotFoundException(itemCommand.getProductId()));
order.addItem(product, itemCommand.getQuantity());
}
// Save order
orderRepository.save(order);
// Publish events
eventPublisher.publishAll(order.getDomainEvents());
order.clearDomainEvents();
return order.getId();
}
private void validateCommand(CreateOrderCommand command) {
if (command.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have at least one item");
}
// Additional validation...
}
}
Query Handler
Order Query Handler
@Component
@Transactional(readOnly = true)
public class OrderQueryHandler {
@Autowired
private OrderReadModelRepository readModelRepository;
public OrderView getOrderById(OrderId orderId) {
return readModelRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
public Page<OrderSummary> getOrdersByCustomer(CustomerId customerId, Pageable pageable) {
return readModelRepository.findByCustomerId(customerId, pageable);
}
public List<OrderSummary> getOrdersByStatus(OrderStatus status) {
return readModelRepository.findByStatus(status);
}
public OrderStatistics getOrderStatistics(LocalDate from, LocalDate to) {
return readModelRepository.getStatistics(from, to);
}
}
// Read Model
@Entity
@Table(name = "order_read_models")
public class OrderReadModel {
@Id
private String orderId;
private String orderNumber;
private String customerId;
private String customerName;
@Enumerated(EnumType.STRING)
private OrderStatus status;
@Column(precision = 19, scale = 2)
private BigDecimal totalAmount;
private String currency;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Getters and setters...
}
5. Event Sourcing Pattern
Event Store Implementation
Event Store
@Repository
public class EventStore {
@PersistenceContext
private EntityManager entityManager;
public void saveEvents(String aggregateId, List<DomainEvent> events, int expectedVersion) {
// Check for concurrency conflicts
int currentVersion = getCurrentVersion(aggregateId);
if (currentVersion != expectedVersion) {
throw new ConcurrencyException("Expected version " + expectedVersion +
", but was " + currentVersion);
}
// Save events
for (int i = 0; i < events.size(); i++) {
StoredEvent storedEvent = new StoredEvent(
aggregateId,
events.get(i).getClass().getName(),
serializeEvent(events.get(i)),
currentVersion + i + 1
);
entityManager.persist(storedEvent);
}
}
public List<DomainEvent> getEvents(String aggregateId) {
List<StoredEvent> storedEvents = entityManager
.createQuery("SELECT se FROM StoredEvent se WHERE se.aggregateId = :aggregateId " +
"ORDER BY se.version", StoredEvent.class)
.setParameter("aggregateId", aggregateId)
.getResultList();
return storedEvents.stream()
.map(this::deserializeEvent)
.collect(Collectors.toList());
}
public List<DomainEvent> getEventsFromVersion(String aggregateId, int fromVersion) {
List<StoredEvent> storedEvents = entityManager
.createQuery("SELECT se FROM StoredEvent se WHERE se.aggregateId = :aggregateId " +
"AND se.version > :fromVersion ORDER BY se.version", StoredEvent.class)
.setParameter("aggregateId", aggregateId)
.setParameter("fromVersion", fromVersion)
.getResultList();
return storedEvents.stream()
.map(this::deserializeEvent)
.collect(Collectors.toList());
}
private int getCurrentVersion(String aggregateId) {
Long version = entityManager
.createQuery("SELECT MAX(se.version) FROM StoredEvent se WHERE se.aggregateId = :aggregateId", Long.class)
.setParameter("aggregateId", aggregateId)
.getSingleResult();
return version != null ? version.intValue() : 0;
}
private String serializeEvent(DomainEvent event) {
// Use JSON serialization
try {
return objectMapper.writeValueAsString(event);
} catch (JsonProcessingException e) {
throw new EventSerializationException("Failed to serialize event", e);
}
}
private DomainEvent deserializeEvent(StoredEvent storedEvent) {
try {
Class<?> eventClass = Class.forName(storedEvent.getEventType());
return (DomainEvent) objectMapper.readValue(storedEvent.getEventData(), eventClass);
} catch (Exception e) {
throw new EventDeserializationException("Failed to deserialize event", e);
}
}
}
6. Specification Pattern
Business Rule Encapsulation
Product Specifications
public class ProductSpecifications {
public static Specification<Product> isActive() {
return (root, query, builder) -> builder.equal(root.get("active"), true);
}
public static Specification<Product> hasCategory(String categoryName) {
return (root, query, builder) ->
builder.equal(root.get("category").get("name"), categoryName);
}
public static Specification<Product> hasPriceBetween(BigDecimal minPrice, BigDecimal maxPrice) {
return (root, query, builder) ->
builder.between(root.get("price"), minPrice, maxPrice);
}
public static Specification<Product> hasLowStock(int threshold) {
return (root, query, builder) ->
builder.lessThanOrEqualTo(root.get("stockQuantity"), threshold);
}
public static Specification<Product> byCategoryAndPriceRange(String category,
BigDecimal minPrice,
BigDecimal maxPrice) {
return Specification.where(hasCategory(category))
.and(hasPriceBetween(minPrice, maxPrice));
}
public static Specification<Product> isActiveAndInStock() {
return Specification.where(isActive())
.and((root, query, builder) ->
builder.greaterThan(root.get("stockQuantity"), 0));
}
public static Specification<Product> searchByKeyword(String keyword) {
return (root, query, builder) -> {
String likePattern = "%" + keyword.toLowerCase() + "%";
return builder.or(
builder.like(builder.lower(root.get("name")), likePattern),
builder.like(builder.lower(root.get("description")), likePattern)
);
};
}
}
7. Integration Patterns
Service Layer Pattern
Application Service
@Service
@Transactional
public class OrderApplicationService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ProductRepository productRepository;
@Autowired
private UnitOfWork unitOfWork;
@Autowired
private DomainEventPublisher eventPublisher;
public OrderId createOrder(CreateOrderRequest request) {
// Validate business rules
validateCreateOrderRequest(request);
// Create order aggregate
Order order = new Order(
OrderNumber.generate(),
new CustomerInfo(request.getCustomerId(), request.getCustomerName()),
request.getShippingAddress()
);
// Add items with business validation
for (CreateOrderItemRequest itemRequest : request.getItems()) {
Product product = productRepository.findById(itemRequest.getProductId())
.orElseThrow(() -> new ProductNotFoundException(itemRequest.getProductId()));
// Business rule: Check stock availability
if (product.getStockQuantity() < itemRequest.getQuantity()) {
throw new InsufficientStockException(product.getId(),
product.getStockQuantity(),
itemRequest.getQuantity());
}
order.addItem(product, itemRequest.getQuantity());
}
// Register with unit of work
unitOfWork.registerNew(order);
// Commit transaction
unitOfWork.commit();
// Publish domain events
eventPublisher.publishAll(order.getDomainEvents());
order.clearDomainEvents();
return order.getId();
}
@Transactional(readOnly = true)
public OrderView getOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
return OrderView.from(order);
}
public void confirmOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.confirm();
unitOfWork.registerDirty(order);
unitOfWork.commit();
eventPublisher.publishAll(order.getDomainEvents());
order.clearDomainEvents();
}
private void validateCreateOrderRequest(CreateOrderRequest request) {
if (request.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have at least one item");
}
// Additional validation...
}
}
Best Practices Summary
Enterprise Pattern Best Practices
- Keep Aggregates Small: Focus on consistency boundaries
- Use Domain Events: Decouple domain logic from infrastructure
- Implement Proper Validation: Both at domain and application levels
- Handle Concurrency: Use optimistic locking and event sourcing
- Separate Concerns: Clear boundaries between layers
- Test Business Logic: Focus on domain behavior, not persistence
- Monitor Performance: Track aggregate size and event volume