From grimoire
Assigns system event handling to non-UI controller classes, separating business logic from UI. Use when logic leaks into UI handlers or operations are triggered from multiple interfaces.
How this skill is triggered — by the user, by Claude, or both
Slash command
/grimoire:apply-controller-patternThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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.
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. Why best: 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.
Sources: Larman, "Applying UML and Patterns" (Prentice Hall, 2004); Martin, "Clean Architecture" (Prentice Hall, 2017); Cockburn, "Hexagonal Architecture" (2005)
Two valid options — choose based on system complexity:
Facade Controller: One class represents the entire system. Appropriate for simple systems with few use cases.
class SystemController:
def place_order(self, order_data): ...
def cancel_order(self, order_id): ...
def register_user(self, user_data): ...
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.
class PlaceOrderController:
def execute(self, order_data): ...
class CancelOrderController:
def execute(self, order_id): ...
Default to Use-Case Controllers when a Facade Controller exceeds ~10 operations.
The Controller handles the system operation — it does not know how the result will be displayed.
# Bad — Controller renders HTML; tied to web UI
class OrderController:
def place_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 it
class PlaceOrderController:
def execute(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())
The Controller coordinates — it delegates to domain objects for actual business logic.
# Bad — Controller contains business rules
class PlaceOrderController:
def execute(self, order_data):
if order_data["total"] > 10000: # business rule in Controller
raise Exception("Order exceeds maximum value")
if not self.inventory.all_in_stock(order_data): # domain logic in Controller
raise Exception("Items out of stock")
# Controller should not know these rules
# Good — domain objects enforce their own rules; Controller only coordinates
class PlaceOrderController:
def execute(self, order_data):
order = self.order_factory.create(order_data) # creation delegated
order.validate() # rules in domain object
self.inventory.reserve(order) # domain service handles stock
self.order_repo.save(order)
return OrderResult.from_order(order)
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 framework
def test_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"
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")
def web_place_order(request):
result = PlaceOrderController(...).execute(request.json)
return jsonify(result)
# CLI — delegates to same Controller
def cli_place_order(args):
result = PlaceOrderController(...).execute(vars(args))
print(result)
# Test — calls Controller directly, no HTTP
def test_place_order():
result = PlaceOrderController(...).execute(test_data)
assert result.order_id is not None
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.
repo.save(data) adds a layer with no value; in simple CRUD, the framework handler and the repository are sufficientnpx claudepluginhub jeffreytse/grimoire --plugin grimoireEvaluates architecture options (onion, MVC, simple) enforcing dependency direction and YAGNI-based abstraction decisions. Calls when deciding layer structure or dependency rules.
Provides a unified, simplified interface to a complex subsystem, reducing client coupling and hiding internal complexity. Useful for wrapping intricate APIs or libraries.
Implements Clean Architecture, Hexagonal Architecture, and Domain-Driven Design for backend systems. Use when architecting new systems or refactoring for maintainability.