Saltar al contenido >_
Blog Java
DÍA 14 · #100ArchitectureDays
Java Spring Boot Architecture 100ArchitectureDays

Día 14: Cambiaste una clase y rompiste 10 más. Efecto dominó.

Alejandro Lafourcade Alejandro Lafourcade
23 abr 2026 6 min read
Día 14: Cambiaste una clase y rompiste 10 más. Efecto dominó.

Cambiaste un campo en un DTO. Un rename inocente. Y de repente tienes 10 clases con errores de compilación, 3 tests rotos y un compañero preguntándote “¿qué tocaste?”.

No tocaste nada raro. Tocaste una clase. El problema es que esa clase está conectada a todo como si fuera el centro del universo. Y en tu codebase, probablemente lo es.

Esto no es solo de Java

El anti-pattern es el mismo en cualquier stack. Solo cambia la sintaxis:

  • Java / Spring Boot: un UserDTO compartido entre controller, service y repository
  • TypeScript / NestJS: un user.dto.ts que se usa en controllers, guards, pipes y servicios
  • Python / Django: un serializer gigante que sirve para crear, leer, actualizar y exportar
  • C# / .NET: un UserModel compartido entre API, business layer y data access
  • Ruby on Rails: un modelo User con as_json sobreescrito 4 veces para diferentes contextos
  • PHP / Laravel: un UserResource que hace transform distinto según quién lo llame

El patrón es siempre el mismo: un objeto compartido entre capas con necesidades distintas. Cuando lo tocas, se rompe todo.

Alto acoplamiento, baja cohesión

Dos conceptos que suenan a examen de facultad pero que definen si tu código es mantenible o una bomba de tiempo:

Acoplamiento = cuánto depende una clase de otra. Si cambiar A rompe B, C y D, están acopladas.

Cohesión = cuánto se relacionan las cosas dentro de una clase. Si una clase hace cosas que no tienen nada que ver entre sí, tiene baja cohesión.

Lo ideal: bajo acoplamiento, alta cohesión. La realidad de la mayoría de los proyectos: exactamente lo opuesto.

El ejemplo: un DTO que gobierna todo

Un solo UserDTO compartido entre controller, service y hasta la capa de persistencia:

public class UserDTO {
    private Long id;
    private String name;
    private String email;
    private String password;
    private String role;
    private LocalDateTime createdAt;
    private List<OrderDTO> orders;
    private AddressDTO address;
    private String creditCardLast4;
}

Y se usa en todos lados:

@RestController
public class UserController {
    @PostMapping("/users")
    public UserDTO createUser(@RequestBody UserDTO dto) { ... }

    @GetMapping("/users/{id}")
    public UserDTO getUser(@PathVariable Long id) { ... }
}

@Service
public class UserService {
    public UserDTO createUser(UserDTO dto) { ... }
    public UserDTO findById(Long id) { ... }
}

@Service
public class OrderService {
    public List<OrderDTO> getOrders(UserDTO user) { ... }
}

@Service
public class NotificationService {
    public void sendWelcome(UserDTO user) { ... }
}

@Service
public class ReportService {
    public Report generateUserReport(UserDTO user) { ... }
}

Ahora renombras name a fullName. O eliminas creditCardLast4 porque ya no lo necesitas en la respuesta. O agregas un campo.

Todos esos servicios se rompen. El controller se rompe. Los tests se rompen. El frontend se rompe. Todo porque compartes un objeto entre 5 capas con necesidades distintas.

El OrderService no necesita el password. El NotificationService no necesita las órdenes. El ReportService no necesita la tarjeta de crédito. Pero todos dependen del mismo DTO gigante.

En TypeScript sería el mismo problema con una interfaz User compartida entre routes, middleware y services. En Python, un serializer con 15 campos que se usa para crear, listar y exportar. La tecnología cambia, el error no.

La solución: cada capa recibe lo que necesita

💡 Tip: Antes de seguir: si sentís que tu arquitectura es un castillo de naipes, bajate el Blueprint con las Red Flags que salvan proyectos.

Descargar Blueprint Gratis
// Lo que el frontend envía para crear un usuario
public record CreateUserRequest(
    String name,
    String email,
    String password
) {}

// Lo que el endpoint devuelve
public record UserResponse(
    Long id,
    String name,
    String email,
    String role,
    LocalDateTime createdAt
) {}

// Lo que el servicio de notificaciones necesita
public record UserNotificationInfo(
    String name,
    String email
) {}

// Lo que el servicio de reportes necesita
public record UserReportData(
    Long id,
    String name,
    String role,
    LocalDateTime createdAt,
    int totalOrders
) {}

Cada clase recibe exactamente lo que necesita. Ni más, ni menos.

@RestController
public class UserController {
    @PostMapping("/users")
    public UserResponse createUser(@RequestBody CreateUserRequest request) { ... }

    @GetMapping("/users/{id}")
    public UserResponse getUser(@PathVariable Long id) { ... }
}

@Service
public class NotificationService {
    public void sendWelcome(UserNotificationInfo user) { ... }
}

Ahora renombras un campo en CreateUserRequest. ¿Qué se rompe? Solo el controller y su test. Nada más. El servicio de notificaciones ni se entera. El de reportes sigue funcionando. Cero efecto dominó.

Los números

AntesDespués
Clases afectadas por un cambio de DTO10+1-2
Campos innecesarios viajando por la red5-60
Riesgo de exponer password en respuestaAltoNulo

La otra cara: cohesión

Acoplamiento bajo sin cohesión alta no sirve de mucho. Mira esta clase:

@Service
public class UserService {
    public User createUser(CreateUserRequest req) { ... }
    public void sendWelcomeEmail(User user) { ... }
    public Report generateMonthlyReport() { ... }
    public void syncWithExternalCRM() { ... }
    public byte[] exportToCsv() { ... }
}

Crear usuarios, mandar mails, generar reportes, sincronizar con un CRM y exportar a CSV. ¿Qué tienen en común? Que involucran usuarios. Y eso no es suficiente para estar en la misma clase.

Alta cohesión significa que todo lo que hace una clase está estrechamente relacionado. Los métodos trabajan sobre los mismos datos para el mismo propósito.

@Service
public class UserService {
    public User create(CreateUserRequest req) { ... }
    public User findById(Long id) { ... }
    public User update(Long id, UpdateUserRequest req) { ... }
    public void deactivate(Long id) { ... }
}

@Service
public class UserNotificationService {
    public void sendWelcomeEmail(User user) { ... }
    public void sendDeactivationEmail(User user) { ... }
}

@Service
public class UserExportService {
    public byte[] exportToCsv(UserExportCriteria criteria) { ... }
    public Report generateMonthlyReport() { ... }
}

Cada clase tiene una razón para existir. Cada método trabaja con los mismos conceptos. Eso es cohesión alta.

Si ayer leíste el artículo del God Object, esto te va a sonar familiar. SRP, acoplamiento y cohesión son tres formas de mirar el mismo problema: clases que hacen demasiado y están conectadas a todo.

La regla práctica

Cuando necesitas hacer un cambio, hazte estas tres preguntas:

  1. ¿Cuántas clases tengo que tocar? Si son más de 2 o 3, tienes acoplamiento alto.
  2. ¿Los métodos de esta clase cambian juntos? Si no, tienes cohesión baja.
  3. ¿Estoy compartiendo un objeto entre capas que tienen necesidades diferentes? Si sí, necesitas modelos separados.

El error de fondo

El efecto dominó no es un problema de Spring, ni de Java, ni de ningún framework. Es un problema de diseño. Compartir objetos entre capas porque es “más rápido” y “menos código” funciona al principio. Después pagas el precio cada vez que tocas algo.

Un poco de duplicación explícita (modelos separados por capa) es infinitamente mejor que un acoplamiento implícito que te explota en la cara cuando menos lo esperas.

La próxima vez que vayas a compartir un DTO entre capas, hazte una pregunta: ¿realmente necesitan exactamente los mismos campos? Casi seguro que no.

Día 14 de #100ArchitectureDays.

>_ INGENIERÍA SIN FILTROS
ARCHITECTURE
RED FLAGS
& The Modern Backend Blueprint
VERSIÓN 2026 ALAFOURCA.DEV
Lead Magnet Gratuito

Architecture Red Flags & The Modern Backend Blueprint

La guía definitiva para detectar fallos de diseño y el mapa de referencia para construir sistemas resilientes.

Recibí el War Manual en tu inbox:

Prometido: nada de spam, solo ingeniería cruda cada 15 días.

¿Necesitás ayuda con tu proyecto? Agendá una sesión 1:1 →

Tags: Java Spring Boot Architecture 100ArchitectureDays
Compartir artículo:
// comentarios

¿Qué opinás?

Logueate con tu cuenta de GitHub para dejar tu comentario.

// related_posts

También te puede interesar