Skip to main content

Controller

Assign system event handling to a non-UI class that mediates between requests and domain logic

TL;DR

Assign responsibility for handling system events to a non-UI class that acts as a middleman between the UI/API layer and your domain logic. This Controller class mediates between external requests and the objects that perform the actual work, keeping UI concerns out of your domain model.

Learning Objectives

  • Understand why domain objects shouldn't directly handle UI or API requests
  • Learn when to create a Controller and what responsibilities it should have
  • Recognize different types of Controllers and their appropriate uses
  • Avoid mixing UI concerns with domain logic
  • Design systems with clear separation between presentation and business logic

Motivating Scenario

A user clicks an "Checkout" button in your e-commerce application. The button handler could directly access Order objects and manipulate them, tightly coupling the UI to domain logic. Instead, a CheckoutController receives the button click event, coordinates with Order and Payment objects, and returns results to the UI. Now the domain remains independent of UI technology.

Core Concepts

The Controller pattern addresses a critical architectural concern: how should external requests (from UI, API, messages) reach your domain logic without coupling them together? A Controller is a non-UI class that receives system events and coordinates the response by delegating to other objects.

Controllers serve several key purposes:

  1. Decouple UI from Domain: Domain objects don't know about screens, buttons, or HTTP requests
  2. Coordinate Complex Operations: Controllers can orchestrate multiple objects to fulfill a use case
  3. Handle Session and Request Data: Controllers can manage request-specific state
  4. Control Access: Controllers can enforce security, validation, or authorization rules
  5. Bridge Technology Gaps: Different UIs (web, desktop, mobile) can use the same Controllers

A good Controller is a "use case controller"—one Controller per use case or user goal. It delegates the actual work to Information Experts rather than doing the work itself. Controllers should be thin and focused, not bloated with business logic.

Controller: Request Handling Flow

Practical Example

Consider a checkout system where users submit orders. Instead of having the UI directly manipulate Orders, we create a CheckoutController:

controller_example.py
from dataclasses import dataclass
from typing import List

@dataclass
class Product:
id: str
name: str
price: float

class Order:
def __init__(self, customer_id: str):
self.customer_id = customer_id
self.line_items: List[tuple] = []
self.status = "pending"

def add_line_item(self, product: Product, quantity: int):
self.line_items.append((product, quantity))

def validate(self) -> bool:
return len(self.line_items) > 0

def get_total(self) -> float:
return sum(p.price * q for p, q in self.line_items)

class PaymentProcessor:
def process(self, amount: float, card_token: str) -> bool:
# Simulate payment processing
return card_token and amount > 0

class CheckoutController:
"""Controller: mediates between UI and domain objects"""
def __init__(self):
self.payment_processor = PaymentProcessor()

def process_checkout(self, customer_id: str,
items: List[tuple],
card_token: str) -> dict:
"""Coordinate checkout use case"""
# Create domain object
order = Order(customer_id)

# Populate with domain-relevant data
for product, quantity in items:
order.add_line_item(product, quantity)

# Validate business rules
if not order.validate():
return {"success": False, "error": "Order is empty"}

# Delegate to expert
if not self.payment_processor.process(order.get_total(), card_token):
return {"success": False, "error": "Payment failed"}

order.status = "confirmed"
return {"success": True, "order_id": id(order)}

# Usage
controller = CheckoutController()

apple = Product("P001", "Apple", 1.50)
orange = Product("P002", "Orange", 2.00)

result = controller.process_checkout(
customer_id="cust-123",
items=[(apple, 5), (orange, 3)],
card_token="valid-token"
)

print(result)

When to Use / When Not to Use

Use
  1. Creating entry points for use cases or features
  2. Mediating between UI/API and domain logic
  3. Handling request-specific data and session state
  4. Coordinating multiple domain objects for a transaction
  5. Implementing authorization and validation at system boundary
Avoid
  1. Putting business logic directly in Controllers
  2. Creating one controller for all system events
  3. Having Controllers directly expose domain data to UI
  4. Using Controllers to implement cross-cutting concerns
  5. Making Controllers stateful beyond a request scope

Patterns and Pitfalls

Controller Implementation Patterns

Create one controller per use case: If you have a "Checkout" use case and a "Return Order" use case, create separate Controllers for each. This keeps responsibilities focused.

Delegate to domain objects: Controllers coordinate but shouldn't perform business logic. Ask Order objects to validate themselves; ask PaymentProcessor to charge cards.

Keep Controllers thin: Controllers should orchestrate, not implement. Most of your logic belongs in domain objects, not Controllers.

God Controllers: Don't create one massive Controller that handles all system events. This defeats the purpose of the pattern.

Exposing domain data directly: Controllers should adapt domain objects for UI consumption, not expose them raw. Use DTOs or view models.

Putting business logic in Controllers: If you're writing complex if/else chains or calculations, that logic belongs in domain objects, not Controllers.

Design Review Checklist

  • Is each Controller responsible for a single use case?
  • Does the Controller delegate actual work to domain objects?
  • Is the Controller free of business logic?
  • Does the Controller avoid directly exposing domain objects to the UI?
  • Can domain objects be used independently without the Controller?
  • Is authorization and validation handled at the system boundary?

Self-Check

  1. What's the primary purpose of a Controller? To mediate between external requests (UI, API) and domain logic, keeping them decoupled.

  2. Should a Controller contain business logic? No. Controllers should orchestrate and delegate, not implement business logic. That belongs in domain objects.

  3. How many Controllers do I need? Generally one per use case. If you're tempted to create one massive Controller, split it into focused, use-case-specific Controllers.

info

One Takeaway: Controllers are thin mediators between your system's entry point and your domain logic. They orchestrate and delegate, never implement business rules directly.

Next Steps

References

  1. GRASP (Object-Oriented Design) - Wikipedia ↗️
  2. Applying UML and Patterns by Craig Larman ↗️