5. Ejemplo práctico: aplicación en tres capas

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.

5.1 Diseño conceptual de la aplicación N-Tier

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:

  • Capa de presentación: expone un API REST para que clientes web y móviles registren y consulten reservas.
  • Capa de aplicación: orquesta casos de uso como crear, cancelar y consultar reservas, aplicando las reglas del dominio.
  • Capa de infraestructura: implementa persistencia en base de datos, integra un proveedor de correos y registra auditorías.

La siguiente ilustración enumera responsabilidades clave y actores involucrados. Mantener este mapa conceptual facilita derivar clases, interfaces y flujos de datos.

Capa de presentación

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));
    }
}
Comando
Respuesta

Capa de aplicación

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;
    }
}
Persistencia
Confirmación

Capa de infraestructura

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.

5.2 Implementación en Java paso a paso

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.

5.3 Próximos pasos

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.