Use when deciding which non-UI class should handle a system operation or external event — especially when business logic leaks into UI handlers, or when the same system operation is triggered from multiple interfaces (web, CLI, API).
Use when deciding which non-UI class should handle a system operation or external event — especially when business logic leaks into UI handlers, or when the same system operation is triggered from multiple interfaces (web, CLI, API).
Assign the responsibility for receiving and handling system events to a non-UI class that represents either the overall system or a use-case scenario.
Why This Is Best Practice
Adopted by: GRASP (Larman, 2004); the foundation of MVC's C (Controller), MVP's Presenter, and MVVM's ViewModel. Explicitly required in layered architectures (Clean Architecture — Uncle Bob; Hexagonal Architecture — Alistair Cockburn). Any application built with Spring MVC, Django views, Rails controllers, or ASP.NET is implementing GRASP Controller.
Impact: When business logic is placed directly in UI event handlers (button click → database write), it cannot be tested without a UI, cannot be invoked from a second interface (API, CLI), and cannot be reused. Separating system operations into Controller classes — independent of any UI framework — means the same use case can be invoked from web, mobile, API, and test without duplication. Google's Application Architecture Guide (Android) and Apple's App Architecture Guide both mandate a Controller/ViewModel separation for exactly this reason.
The alternative — UI event handlers that contain business logic — produces fat views, untestable code, and logic that cannot be reused across interfaces. GRASP Controller is the design decision that makes layered architecture work: it is the class that sits at the boundary between UI and domain, receives external inputs, and delegates to domain objects.
Use-Case Controller: One class per use case or user session. Appropriate when the system has many operations and a Facade Controller would grow too large.
Default to Use-Case Controllers when a Facade Controller exceeds ~10 operations.
2. Keep UI logic out of the Controller
The Controller handles the system operation — it does not know how the result will be displayed.
# Bad — Controller renders HTML; tied to web UIclassOrderController:
defplace_order(self, request):
order = Order(request.POST)
order.save()
return render_template("order_success.html", order=order) # UI in Controller# Good — Controller returns a result; caller (view) decides how to present itclassPlaceOrderController:
defexecute(self, order_data) -> OrderResult:
order = self.order_factory.create(order_data)
self.order_repo.save(order)
return OrderResult(order_id=order.id, total=order.total())
3. Keep business logic out of the Controller
The Controller coordinates — it delegates to domain objects for actual business logic.
# Bad — Controller contains business rulesclassPlaceOrderController:
defexecute(self, order_data):
if order_data["total"] > 10000: # business rule in Controllerraise Exception("Order exceeds maximum value")
ifnotself.inventory.all_in_stock(order_data): # domain logic in Controllerraise Exception("Items out of stock")
# Controller should not know these rules# Good — domain objects enforce their own rules; Controller only coordinatesclassPlaceOrderController:
defexecute(self, order_data):
order = self.order_factory.create(order_data) # creation delegated
order.validate() # rules in domain objectself.inventory.reserve(order) # domain service handles stockself.order_repo.save(order)
return OrderResult.from_order(order)
4. Handle one use case per Controller (Use-Case Controller pattern)
A Use-Case Controller has one primary responsibility: coordinating the steps of a single use case. It should be testable by calling its execute method directly, with no UI setup.
# Test — Controller is independent of HTTP, HTML, and web frameworkdeftest_place_order_charges_payment():
controller = PlaceOrderController(
order_factory=FakeOrderFactory(),
inventory=FakeInventory(in_stock=True),
payment=FakePaymentGateway(),
order_repo=FakeOrderRepository(),
)
result = controller.execute({"items": [...], "payment": {...}})
assert result.payment_status == "charged"
5. Expose Controllers as the entry point for all interfaces
The same Controller class should handle the same use case regardless of which interface invokes it — web, CLI, message queue, or test.
# Web handler — delegates to Controller@app.post("/orders")defweb_place_order(request):
result = PlaceOrderController(...).execute(request.json)
return jsonify(result)
# CLI — delegates to same Controllerdefcli_place_order(args):
result = PlaceOrderController(...).execute(vars(args))
print(result)
# Test — calls Controller directly, no HTTPdeftest_place_order():
result = PlaceOrderController(...).execute(test_data)
assert result.order_id isnotNone
Common Mistakes
Fat Controller. A Controller that contains business rules, data transformation, validation, and persistence logic is an anemic domain model in reverse — all logic has moved from domain objects to Controllers. Controllers should coordinate; domain objects should compute.
One Controller per HTTP route. Mapping one Controller class to one URL endpoint is an MVC framework convention, not the GRASP pattern. A GRASP Controller maps to a use case, which may be exposed via multiple routes (REST, GraphQL, RPC).
Returning framework objects from Controllers. A Controller that returns HttpResponse, flask.Response, or express.Response is coupled to the web framework — it cannot be called from CLI or tests without the framework. Return plain domain objects or result types.
When NOT to Use
CRUD-only applications with no business logic — a Controller that just calls repo.save(data) adds a layer with no value; in simple CRUD, the framework handler and the repository are sufficient
Event-driven architectures — message consumers and event handlers serve the same role as Controllers; apply the same principles but don't add a separate Controller layer on top of a well-designed event handler
Microservices with a single operation — a service that does exactly one thing is already a Controller by design; there is no UI layer to separate from