Value Objects (VOs) are immutable objects that represent domain concepts through their attributes rather than identity. Unlike entities (which have IDs), value objects are defined by their values and enforce business rules at construction time.
Two value objects with the same values are considered equal, regardless of their memory address.
package com.example.demo.usuario.domain.vo;import com.example.demo.usuario.domain.exceptions.InvalidPasswordException;import lombok.Getter;@Getterpublic class ContrasenaVO { private final String value; public ContrasenaVO(String value) { if (value == null || value.isEmpty()) { throw new InvalidPasswordException("La contraseña no puede estar vacía"); } if (value.length() < 8) { throw new InvalidPasswordException("La contraseña debe tener al menos 8 caracteres"); } if (value.length() > 64) { throw new InvalidPasswordException("La contraseña no puede superar los 64 caracteres"); } if (!value.matches(".*[A-Za-z].*") || !value.matches(".*\\d.*")) { throw new InvalidPasswordException( "La contraseña debe contener al menos una letra y un número" ); } this.value = value; } public ContrasenaVO actualizarContrasena(String nuevaContrasena) { return new ContrasenaVO(nuevaContrasena); } @Override public String toString() { return "****"; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof ContrasenaVO)) return false; ContrasenaVO that = (ContrasenaVO) o; return value.equals(that.value); } @Override public int hashCode() { return value.hashCode(); }}
This guarantees that invalid passwords cannot exist in your domain model.
Immutability
The value field is final and cannot be changed after construction. Updates create new instances:
ContrasenaVO original = new ContrasenaVO("password123");ContrasenaVO nueva = original.actualizarContrasena("newpass456");// original and nueva are different objectsassert original != nueva;
Security Through toString()
The toString() method returns masked output to prevent accidental password exposure in logs:
ContrasenaVO contrasena = new ContrasenaVO("password123");System.out.println(contrasena); // Output: ****
Value Equality
Two ContrasenaVO objects with the same password are considered equal:
ContrasenaVO pass1 = new ContrasenaVO("password123");ContrasenaVO pass2 = new ContrasenaVO("password123");assert pass1.equals(pass2); // trueassert pass1 == pass2; // false (different objects)
package com.example.demo.producto.domain.vo;import lombok.Getter;@Getterpublic class PrecioVO { private final Float value; public PrecioVO(Float value) { if (value == null) { throw new IllegalArgumentException("El precio no puede ser nulo"); } if (value < 0) { throw new IllegalArgumentException("El precio no puede ser negativo"); } this.value = value; } public PrecioVO actualizarPrecio(Float nuevoValor) { return new PrecioVO(nuevoValor); } @Override public String toString() { return String.valueOf(value); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof PrecioVO)) return false; PrecioVO that = (PrecioVO) o; return value.equals(that.value); } @Override public int hashCode() { return value.hashCode(); }}
In the controller, validation happens at VO creation:
@PostMapping("/registro")public UsuarioPOJO registroUsuario(@RequestBody RegistrarUsuarioRequest request) { // Validation happens here - if password is invalid, exception is thrown RegistrarUsuarioCommand command = new RegistrarUsuarioCommand( request.usuario(), new ContrasenaVO(request.contrasena()) // Validates here ); return registrarUsuarioHandler.handle(command);}
Invalid data is rejected at creation time, preventing invalid states from propagating through the system.
Type Safety
Prevents primitive obsession (e.g., using String for everything) and makes method signatures more expressive:
// Bad: What does String represent?public void updatePassword(String password);// Good: Type clearly indicates a validated passwordpublic void updatePassword(ContrasenaVO password);
Centralized Validation
Validation logic lives in one place. No need to validate passwords in controllers, services, and repositories.
Domain-Driven Design
Value Objects are a core DDD pattern that makes your domain model more expressive and closer to the business language.
Thread Safety
Immutability makes value objects naturally thread-safe without synchronization.