Understanding the domain model, entities, value objects, and business rules in the User Management API
The User Management API applies Domain-Driven Design (DDD) principles to create a rich, expressive domain model that encapsulates business logic and rules. The domain is framework-agnostic and lives at the heart of the hexagonal architecture.
Notice this is not an anemic domain model (just getters/setters). The User entity encapsulates business behavior through methods like activate(), deactivate(), and applyUpdate().
Key characteristics:
Pure Java - No JPA annotations, no Spring dependencies
Identity - UUID id uniquely identifies each user
Rich behavior - Methods that enforce business rules
Immutability awareness - createdAt is never modified after creation
Automatic timestamps - updatedAt is set automatically when state changes
// src/main/java/com/fbaron/user/core/model/UserRole.javapackage com.fbaron.user.core.model;/** * Enumeration of valid user roles in the system. * Kept in the core domain to enforce role-based business rules * at the service level. */public enum UserRole { ADMIN, USER, GUEST}
public enum UserRole { ADMIN, USER, GUEST; public boolean canModifyUsers() { return this == ADMIN; } public boolean hasReadAccess() { return true; // All roles can read }}
Enums are perfect value objects - they’re immutable, type-safe, and can encapsulate behavior related to their values.
Deactivation sets active = false (soft delete, not physical deletion)
Both operations update the timestamp
Usage in service:
@Overridepublic User register(User user) { // ... validation ... user.activate(); // Business logic in domain model return userCommandRepository.save(user);}@Overridepublic void removeById(UUID userId) { User existing = userQueryRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException(userId)); existing.deactivate(); // Business logic in domain model userCommandRepository.save(existing);}
@Overridepublic User edit(UUID userId, User user) { User existing = userQueryRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException(userId)); // Validation... // Business logic in domain model existing.applyUpdate( user.getEmail(), user.getFirstName(), user.getLastName(), user.getRole(), user.isActive() ); return userCommandRepository.save(existing);}
// src/main/java/com/fbaron/user/core/exception/UserNotFoundException.javapackage com.fbaron.user.core.exception;import java.util.UUID;/** * Thrown when a requested user cannot be located in the system. * Unchecked to avoid polluting use-case signatures with * infrastructure noise. */public class UserNotFoundException extends RuntimeException { private final UUID userId; public UserNotFoundException(UUID userId) { super("User not found with id: " + userId); this.userId = userId; } public UUID getUserId() { return userId; }}
public User register(User user) { if (userQueryRepository.existsByUsername(user.getUsername())) { throw new UserAlreadyExistsException("username", user.getUsername()); } if (userQueryRepository.existsByEmail(user.getEmail())) { throw new UserAlreadyExistsException("email", user.getEmail()); } // ... proceed with registration}
Domain exceptions are unchecked (extend RuntimeException) to keep method signatures clean. They’re caught and translated to HTTP responses by the global exception handler.