Para interiorizarnos en el modelo N-Tier construiremos una aplicación de reservas de salas en un coworking. Este problema introduce requisitos interesantes: disponibilidad horaria, interacción con sistemas externos para notificaciones y la necesidad de auditar cambios. Organizaremos la solución en tres capas bien definidas y mantendremos la comunicación mediante interfaces para garantizar flexibilidad.
El dominio se centra en gestionar salas, reservas y usuarias. Las reglas principales incluyen prevenir solapamientos, validar credenciales y notificar al equipo de recepción. Podemos resumir el diseño en los siguientes componentes:
La siguiente ilustración enumera responsabilidades clave y actores involucrados. Mantener este mapa conceptual facilita derivar clases, interfaces y flujos de datos.
Exponemos un API REST que recibe solicitudes para reservar salas, valida credenciales básicas y convierte la petición en comandos. Este módulo se empaqueta dentro de coworking-api con controladores y DTOs.
package com.example.coworking.api.controller;
import com.example.coworking.application.usecase.ReservarSala;
public class ReservaController {
private final ReservarSala reservarSala;
public ReservaController(ReservarSala reservarSala) {
this.reservarSala = reservarSala;
}
public ReservaResponse crear(ReservaRequest request) {
var command = request.toCommand();
return ReservaResponse.from(reservarSala.ejecutar(command));
}
}
El módulo coworking-application contiene los casos de uso que aplican las reglas del dominio. Compartimos entidades e interfaces desde coworking-domain para evitar dependencias circulares.
com.example.coworking
├── api
│ └── com.example.coworking.api (controller, dto)
├── application
│ └── com.example.coworking.application
│ └── usecase
├── domain
│ └── com.example.coworking.domain (model, port)
└── infrastructure
└── com.example.coworking.infrastructure
package com.example.coworking.application.usecase;
import com.example.coworking.domain.model.Reserva;
import com.example.coworking.domain.port.ReservaRepositorio;
import com.example.coworking.domain.port.CalendarioSala;
import com.example.coworking.domain.port.ServicioNotificacion;
public class ReservarSala {
private final ReservaRepositorio reservas;
private final CalendarioSala calendario;
private final ServicioNotificacion notificaciones;
public ReservarSala(ReservaRepositorio reservas,
CalendarioSala calendario,
ServicioNotificacion notificaciones) {
this.reservas = reservas;
this.calendario = calendario;
this.notificaciones = notificaciones;
}
public Reserva ejecutar(ReservarSalaCommand command) {
calendario.verificarDisponibilidad(command.salaId(), command.desde(), command.hasta());
Reserva reserva = Reserva.agendar(
command.reservaId(),
command.usuarioId(),
command.salaId(),
command.desde(),
command.hasta()
);
reservas.guardar(reserva);
notificaciones.enviarConfirmacion(reserva);
return reserva;
}
}
El módulo coworking-infrastructure implementa los puertos definidos por el dominio: repositorios, adaptadores de correo y auditoría. Podemos desplegarlo en un servidor independiente o junto al módulo de aplicación.
package com.example.coworking.infrastructure.persistence;
import java.sql.*;
import javax.sql.DataSource;
import com.example.coworking.domain.model.Reserva;
import com.example.coworking.domain.port.ReservaRepositorio;
public class ReservaRepositorioJdbc implements ReservaRepositorio {
private final DataSource dataSource;
public ReservaRepositorioJdbc(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void guardar(Reserva reserva) {
String sql = "INSERT INTO reservas(id, sala_id, usuario_id, desde, hasta) VALUES (?, ?, ?, ?, ?)";
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, reserva.id());
statement.setString(2, reserva.salaId());
statement.setString(3, reserva.usuarioId());
statement.setTimestamp(4, Timestamp.valueOf(reserva.desde()));
statement.setTimestamp(5, Timestamp.valueOf(reserva.hasta()));
statement.executeUpdate();
} catch (SQLException ex) {
throw new RuntimeException("No se pudo guardar la reserva " + reserva.id(), ex);
}
}
}
Otros adaptadores incluyen un cliente SMTP para notificar a recepción y un registrador de auditoría que traza cada cambio en las reservas.
Una vez definido el diseño, la implementación se puede abordar por iteraciones: primero entidades y puertos, luego casos de uso, más tarde la capa de presentación y finalmente los adaptadores técnicos. Este enfoque incremental mantiene las dependencias bajo control.
El ejemplo del coworking demuestra cómo una aplicación en tres capas permite extender funcionalidades y cambiar proveedores sin romper el dominio. En el siguiente tema exploraremos las ventajas del modelo N-Tier en más detalle, abarcando aspectos de mantenibilidad y escalado.