skip to content

Developers often say "my HTTP endpoint class is my controller, so I already follow GRASP Controller." Explain how a web framework's controller (or MVC's 'C') relates to the GRASP Controller principle, and when that claim is wrong.

level: middleimportance: should knowfreq 48%

answer

  1. MVC 'C' = presentation adapter; GRASP 'Controller' = non-UI coordinator
  2. thin endpoint: bind → command → handler → map response
  3. if it imports Request/Response, it isn't a GRASP controller
  4. hexagonal: driving adapter in front of input port
  5. test without HTTP = the litmus test

basics

~20 s

A web/MVC controller belongs to the presentation layer — it knows about requests, responses and views. GRASP's controller must not know any of that. The claim is only true if the endpoint just translates input, calls a use-case object, and formats the reply.

solid answer

~60 s

They are different roles that share a name. An **MVC / web framework controller** is a *delivery-mechanism adapter*: it parses the URL and body, binds parameters, handles content negotiation, sets status codes and headers, and selects a view. Those responsibilities are inherently coupled to HTTP and to the framework. The **GRASP controller** is the first *non-UI* receiver of the system event — it must be callable from a test, a CLI, a queue consumer or a second UI with no HTTP present. If your endpoint class contains the use case, the use case is now welded to the web tier: it can't be reused, it drags a servlet/ASP.NET/Rails context into tests, and swapping or adding a delivery channel means copy-paste. The claim is legitimate only when the endpoint is *thin*: deserialize → build a command → call the use-case object → map the result to a response. Then the framework controller is an adapter and the GRASP controller lives behind it. If business rules, multi-step orchestration, or transaction scripts sit inside the endpoint, it is not a GRASP controller — it is the bloated-controller smell wearing a framework annotation.

code

pseudocode · 12 lines
pseudocode
// Presentation adapter — HTTP-aware, no business rules
@Post("/orders")
fun createOrder(body: CreateOrderJson, principal: Principal): HttpResponse {
    val cmd    = PlaceOrder(principal.userId, body.toItems())   // translate
    val result = placeOrderHandler.handle(cmd)                  // delegate
    return created("/orders/" + result.id, result.toJson())     // present
}

// Application/use-case layer — no HTTP types anywhere in this file
class PlaceOrderHandler(orders, customers, clock) {
    fun handle(cmd: PlaceOrder): OrderSummary { /* authorize, load, delegate, commit */ }
}

go deeper

for a junior

Say that the web controller belongs to the UI side and should stay thin: read input, call a service object, return the result.

for a middle

Enumerate what belongs on each side (binding/status/view vs orchestration/transaction) and give the reuse + testability argument.

for a senior

Map it onto hexagonal/Clean Architecture (driving adapter → input port → interactor), state where authorization and transactions sit and why, and name the cases where collapsing layers is a reasonable trade-off.

for a principal

Treat it as an organizational and evolution policy: one use-case surface serving many delivery channels, error taxonomy that stays protocol-neutral, framework upgrades confined to adapters, and a team convention for when trivial queries may bypass the layer.

## Two different things called 'controller' ### The MVC 'C' In **Model–View–Controller**, the controller is the piece of the *user interface* that interprets user input and updates the model/view. It is by definition part of the presentation triad. In web frameworks (Spring MVC `@RestController`, ASP.NET `Controller`, Rails `ActionController`, Django views, Express route handlers), the same word names the class the framework routes a request to. Its intrinsic responsibilities: - route matching, path/query/body binding, deserialization; - content negotiation (JSON vs HTML vs CSV), status codes, headers, cookies, redirects; - request-scoped concerns: multipart uploads, streaming, caching headers, ETags; - view selection or response serialization; - HTTP-shaped error mapping (404 vs 409 vs 422). Every one of those is meaningless outside a web request. ### The GRASP 'Controller' GRASP Controller asks: *which non-UI object receives the system event?* Answer: a facade controller (system/subsystem) or a use-case/session controller. Its defining constraint is **presentation independence** — no request/response types, no view models, no framework request context. ## Why the distinction is not pedantry Put the use case inside the endpoint and you lose, concretely: 1. **A second channel.** A mobile BFF, a gRPC service, an admin CLI, a scheduled job, or a queue consumer needs the same operation. With logic in the endpoint you either duplicate it or start calling HTTP from inside your own process. 2. **Fast tests.** Testing the use case now requires booting the web framework (or mocking `HttpServletRequest`-shaped objects). Test time and flakiness go up; teams respond by testing less. 3. **A place for policy.** Transactions, authorization, idempotency and audit want a boundary that isn't HTTP-specific — otherwise the same rules must be re-implemented for the queue consumer. 4. **Framework mobility.** Framework major versions and framework swaps become rewrites of business logic rather than of adapters. ## The healthy arrangement ``` HTTP request │ (framework controller = adapter, presentation layer) ├─ bind + validate syntax → PlaceOrderRequest ├─ authenticate (who are you) ├─ build command → PlaceOrder(customerId, items) ▼ PlaceOrderHandler ← GRASP controller (application/use-case layer) ├─ authorize (may you) ├─ begin unit of work ├─ load aggregates, call domain methods ← business rules live here ├─ commit, publish events ▼ OrderSummary (plain data) │ (framework controller again) └─ map to 201 + JSON body / view ``` This is the same shape that **hexagonal architecture** calls a *primary/driving adapter* in front of an *input port*, and that **Clean Architecture** calls a *controller* in front of an *interactor/use case*. Ports & adapters and Clean Architecture are, on this point, GRASP Controller with more vocabulary. ## Where the line sits (contested but defensible) | Concern | Framework controller | GRASP controller | |---|---|---| | Deserialization, param binding | yes | no | | **Syntactic** validation (required, format, range) | yes | (may re-check) | | **Business rule** validation ("credit limit exceeded") | no | delegates to domain | | Authentication (identity) | yes | no | | Authorization (permission on this resource) | often delegated down | yes — so non-HTTP callers get it too | | Transaction boundary | no | yes (typical) | | HTTP status / view selection | yes | no | | Mapping domain errors → HTTP codes | yes | raises domain-level errors only | ## When collapsing the layers is acceptable Pragmatism is allowed. For a small service with a single delivery channel and no realistic second one, an endpoint that delegates to repositories directly for **simple reads** is often fine — the indirection buys little. What is rarely fine is state-changing, multi-step, rule-bearing logic living in the endpoint: that's the case where reuse and testability actually bite. A common team rule: *commands go through use-case objects; trivial queries may go straight to a read model.* ## Naming hygiene Because the collision causes real confusion, many codebases avoid calling the inner object a controller at all — `…UseCase`, `…Handler`, `…Service`, `…Interactor` — and reserve `Controller` for the framework class. GRASP is about the responsibility, not the suffix.

  • What is the simplest test that tells you whether your endpoint is a real GRASP controller?
    Try to execute the use case from a plain unit test with no web framework and no HTTP types. If you can't, the use case lives in the presentation layer.
  • Should authorization live in the framework controller or the use-case object?
    Authentication (establishing identity) belongs to the adapter; authorization (may this identity perform this operation on this resource) is safer inside the use case, so every delivery channel — queue consumer, CLI, scheduled job — inherits the same rule instead of re-implementing it.
  • Is it ever acceptable to skip the use-case layer entirely?
    For trivial reads in a single-channel service, yes — the indirection buys little. The risk concentrates in state-changing, multi-step, rule-bearing operations, which is where reuse, transactions, and testability actually matter.

The framework controller is the reception desk: it reads your form, checks your ID, and speaks the building's protocol. The GRASP controller is the case worker behind it who actually processes your application — and can do so whether the request arrived at the desk, by post, or by phone.

saying these in an interview costs you the question

  • Asserting that having a class named *Controller satisfies the principle
  • Putting business rules in the endpoint and calling the layering 'pragmatic' regardless of channel count or complexity
  • Passing HTTP request/response objects (or session/cookie objects) into the use-case layer
  • Mapping domain errors to HTTP status codes inside the use case, which welds it to the web tier
  • Claiming MVC's controller and GRASP's controller are the same because both 'handle input'

context