El patrón de arquitectura recomendado para aplicaciones Jakarta EE sin framework es el MVC Front Controller: un único Servlet recibe todas las peticiones, procesa la lógica y delega la presentación a JSPs.
Petición HTTP → Servlet (Controlador) → Servicio (Modelo) → JSP (Vista)
Ejemplo completo: lista de productos
// Modelo: entidad y servicio
record Producto(int id, String nombre, double precio) {}
public class ProductoService {
private static final List<Producto> DB = List.of(
new Producto(1, "Teclado mecánico", 89.99),
new Producto(2, "Ratón gaming", 49.95),
new Producto(3, "Monitor 27\"", 349.00)
);
public List<Producto> findAll() { return DB; }
public Producto findById(int id) {
return DB.stream().filter(p -> p.id() == id).findFirst().orElse(null);
}
}
// Controlador: ProductoServlet.java
@WebServlet("/productos/*")
public class ProductoServlet extends HttpServlet {
private final ProductoService service = new ProductoService();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String pathInfo = req.getPathInfo(); // null o "/123"
if (pathInfo == null || pathInfo.equals("/")) {
// Listar todos
req.setAttribute("productos", service.findAll());
req.getRequestDispatcher("/WEB-INF/views/lista.jsp")
.forward(req, resp);
} else {
// Detalle de uno
int id = Integer.parseInt(pathInfo.substring(1));
Producto p = service.findById(id);
if (p == null) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
req.setAttribute("producto", p);
req.getRequestDispatcher("/WEB-INF/views/detalle.jsp")
.forward(req, resp);
}
}
}
<!-- /WEB-INF/views/lista.jsp -->
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<!DOCTYPE html>
<html><head><title>Productos</title></head>
<body>
<h1>Catálogo de productos</h1>
<table>
<thead><tr><th>Nombre</th><th>Precio</th></tr></thead>
<tbody>
<c:forEach items="${productos}" var="p">
<tr>
<td><a href="productos/${p.id}"><c:out value="${p.nombre}"/></a></td>
<td><fmt:formatNumber value="${p.precio}" type="currency" currencySymbol="€"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</body></html>
Forward vs Redirect
// Forward: el Servlet delega a otro recurso SIN cambiar la URL del navegador
// Los atributos de request (setAttribute) se mantienen
req.getRequestDispatcher("/WEB-INF/views/lista.jsp").forward(req, resp);
// Redirect: envía 302 al navegador para que haga una nueva petición
// La URL del navegador cambia; los atributos de request se pierden
// Se usa para el patrón Post-Redirect-Get (PRG): después de un POST exitoso
resp.sendRedirect(req.getContextPath() + "/productos");
