Enterprise 4 — MVC and Front Controller
In one line
Model-View-Controller separates data and rules (model), presentation (view) and request handling (controller). A Front Controller is a single entry point that receives every request, applies shared steps (auth, logging), and dispatches to the right controller: Spring MVC's DispatcherServlet.
Think of it like this
A restaurant. The kitchen prepares food (model), the plating and menu card decide how it looks (view), and waiters take orders and bring results (controllers). A single host at the door greets every guest, checks reservations and sends each party to the right waiter: the front controller.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- MVC
- Model-View-Controller: splitting an application into data/rules, presentation, and request handling.
- Front controller
- One component that receives every request, runs shared steps and forwards to the right handler.
- Handler
- The method that handles one route, such as
GET /orders/{id}. - Interceptor / filter
- Code that runs before or after handlers for every request, such as authentication or logging.
Step by step
01The problem: every endpoint repeats the same plumbing
In an early Tiffin prototype, each servlet checked the auth header, logged the request, caught exceptions and formatted errors itself. One forgot the auth check; another returned stack traces to users. Business logic was mixed into the same methods.
02A tiny front controller
One dispatch method handles cross-cutting work once, then looks up the handler for the route. Handlers (controllers) only translate between HTTP and the model.
import java.util.*;
import java.util.function.Function;
record Request(String method, String path, String token) {}
record Response(int status, String body) {}
final class OrderModel { // model: data + rules
private final Map<String, String> status = new HashMap<>(Map.of("o1", "PREPARING"));
Optional<String> statusOf(String id) { return Optional.ofNullable(status.get(id)); }
}
final class FrontController {
private final Map<String, Function<Request, Response>> routes = new HashMap<>();
private final List<String> log = new ArrayList<>();
void register(String method, String prefix, Function<Request, Response> handler) { routes.put(method + " " + prefix, handler); }
Response dispatch(Request req) {
log.add(req.method() + " " + req.path()); // shared step: logging
if (!"valid".equals(req.token())) return new Response(401, "{\"error\":\"unauthorised\"}"); // shared step: auth
String key = req.method() + " /" + req.path().split("/")[1];
Function<Request, Response> handler = routes.get(key);
if (handler == null) return new Response(404, "{\"error\":\"no route\"}");
try { return handler.apply(req); }
catch (RuntimeException e) { return new Response(500, "{\"error\":\"internal\"}"); } // shared step: errors
}
int requestsSeen() { return log.size(); }
}
final class OrderController { // controller: HTTP <-> model, view = JSON text
private final OrderModel model;
OrderController(OrderModel model) { this.model = model; }
Response get(Request r) {
String id = r.path().split("/")[2];
return model.statusOf(id)
.map(s -> new Response(200, "{\"id\":\"" + id + "\",\"status\":\"" + s + "\"}"))
.orElse(new Response(404, "{\"error\":\"order not found\"}"));
}
}03The same shape in Spring MVC
DispatcherServlet receives every request; servlet filters and HandlerInterceptors run shared steps (Spring Security's filter chain handles authentication); @RequestMapping/@GetMapping annotations build the route table; @ControllerAdvice with @ExceptionHandler centralises errors; and message converters (Jackson) render the model as JSON, which is the view for an API.
Try it yourself
- 1
Dispatch three requests
A valid lookup, a missing token, and an unknown route. Every request is logged by the front controller.
terminal$ jshell> /open FrontController.javajshell> var fc = new FrontController(); var orders = new OrderController(new OrderModel())jshell> fc.register("GET", "/orders", orders::get)jshell> fc.dispatch(new Request("GET", "/orders/o1", "valid"))jshell> fc.dispatch(new Request("GET", "/orders/o1", null))jshell> fc.dispatch(new Request("GET", "/menu/today", "valid")).status() + " after " + fc.requestsSeen() + " requests"── expected output ──$5 ==> Response[status=200, body={"id":"o1","status":"PREPARING"}]$6 ==> Response[status=401, body={"error":"unauthorised"}]$7 ==> "404 after 3 requests"
In your stack
- →
Spring MVC:
DispatcherServlet(front controller),@RestController,HandlerInterceptor,@ControllerAdvice,HttpMessageConverter(views for APIs), Thymeleaf for HTML views. Jakarta EE has the same roles with servlets, filters and JAX-RS resources.
Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Business rules hidden in a controller
The "free delivery above ₹499" rule is implemented inside CheckoutController. A new Kafka consumer that places scheduled orders calls the service directly.
Myth vs fact
Myth
MVC is only for server-rendered web pages.
Fact
REST APIs use the same split: the controller handles HTTP, the model holds the rules, and the JSON representation is the view.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Treat every entry point (HTTP, messaging, scheduled jobs, CLI) as a thin adapter over the same application services: this is the core of hexagonal (ports and adapters) architecture, and it's what makes adding a new channel cheap.
Remember this
- 1
Model: domain objects and business logic. View: how a response is presented (an HTML template, or a JSON document for APIs). Controller: reads the request, calls the model, chooses the view or response.
- 2
The separation lets each part change independently: a new UI or a mobile API reuses the same model and rules.
- 3
A Front Controller centralises what every request needs (authentication, logging, error handling, content negotiation) and then dispatches by URL and method to a handler. In Spring MVC,
DispatcherServletis the front controller and your@Controller/@RestControllermethods are the handlers. - 4
Shared steps before and after handlers are usually a chain (filters, interceptors: Chain of Responsibility, Behavioral 6), and the dispatch table is a map from route to handler.
- 5
Keep controllers thin: parse, validate, call one application service, map the result. Business rules in controllers can't be reused by other entry points such as message consumers or scheduled jobs.
Explain it without notes
What does each part of MVC own?
What does a front controller centralise, and what does Spring use for it?
Practice
Add a POST /orders/{id}/cancel route to the tiny front controller that only cancels orders still PREPARING.
Where should request validation (missing fields, wrong types) live versus business validation (can this order be cancelled)?
Trade-offs
- ↔
Clear separation, consistent cross-cutting behaviour and reusable rules, versus more layers and indirection for very small applications.
Done when you can
I can explain MVC for both HTML pages and JSON APIs.
I can describe how DispatcherServlet acts as a front controller and where shared concerns plug in.