Entities, Value Objects, Aggregates, Invariantes y la distinción entre Domain Services y Application Services
public class Order { private final OrderId id; // identidad private OrderStatus status; // mutable private Money total; // igualdad por identidad, no por atributos @Override public boolean equals(Object o) { if (!(o instanceof Order other)) return false; return id.equals(other.id); } }
// record = VO perfecto: inmutable + equals por valor public record Money(BigDecimal amount, String currency) { public Money { // validación en el constructor if (amount.compareTo(BigDecimal.ZERO) < 0) throw new InvalidMoneyException(...); } public Money add(Money other) { return new Money(amount.add(other.amount), currency); } }
public class Order { // La lógica vive en el dominio, no en el servicio public void addItem(OrderItem item) { if (status != DRAFT) throw new OrderNotEditableException(); items.add(item); this.total = recalculate(); // invariante } public void confirm() { if (items.isEmpty()) throw new EmptyOrderException(); this.status = CONFIRMED; registerEvent(new OrderConfirmed(this.id)); } }
// Vive en domain/ — lógica pura sin frameworks public class TransferDomainService { public void transfer( Account from, Account to, Money amount) { // regla de negocio: ¿puede transferir? if (!from.hasSufficientFunds(amount)) throw new InsufficientFundsException(); from.debit(amount); // método rico en la entidad to.credit(amount); // método rico en la entidad } } // Nótese: sin @Service, sin @Transactional, sin repos
@UseCase public class TransferMoneyUseCase { private final AccountRepository accounts; private final TransferDomainService transferService; private final EventPublisher events; public void execute(TransferCommand cmd) { // 1. Obtener Account from = accounts.findById(cmd.fromId()); Account to = accounts.findById(cmd.toId()); // 2. Ejecutar (lógica en el Domain Service) transferService.transfer(from, to, cmd.amount()); // 3. Persistir accounts.save(from); accounts.save(to); // 4. Publicar eventos del dominio events.publish(from.domainEvents()); } }