GRASP — General Responsibility Assignment Software Patterns
Nine principles for assigning responsibilities to classes and objects, introduced by Craig Larman in Applying UML and Patterns (1997). Where SOLID tells you how to design good classes, GRASP tells you what each class should be responsible for.
For SOLID see ../01_theory_foundations/01_solid_principles.md. For GoF patterns see ../01_theory_foundations/05_creational_patterns.md and siblings.
The nine principles
| # | Name | Tells you |
|---|---|---|
| 1 | Information Expert | who has the data needed for a responsibility |
| 2 | Creator | who creates a new object |
| 3 | Controller | who handles a system event |
| 4 | Low Coupling | how to keep classes independent |
| 5 | High Cohesion | how to keep classes focused |
| 6 | Indirection | how to decouple via a middleman |
| 7 | Polymorphism | how to handle type-based behavior variation |
| 8 | Pure Fabrication | when to invent a class with no domain analog |
| 9 | Protected Variations | how to shield from change |
1. Information Expert
Assign responsibility to the class that has the information needed to fulfill it.
class Order:
def __init__(self):
self.items = []
def total(self) -> Decimal:
return sum(item.subtotal() for item in self.items)
class LineItem:
def __init__(self, product, quantity):
self.product = product
self.quantity = quantity
def subtotal(self) -> Decimal:
return self.product.price * self.quantity
Order knows its items → it computes the total. LineItem knows its product and quantity → it computes its subtotal. The data and behavior live together.
Anti-pattern: a separate OrderCalculator class that reaches into Order and LineItem to compute totals. It pulls data out instead of asking the owner.
2. Creator
B creates A when:
- B contains or aggregates A (a parent creating children).
- B closely uses A.
- B has the data to initialize A.
class Order:
def add_item(self, product, quantity):
self.items.append(LineItem(product, quantity)) # Order creates LineItem
Order aggregates LineItem → Order creates them. Not a separate LineItemFactory unless creation is genuinely complex.
When to break this rule: complex construction logic (use a Builder), creating instances of unknown subtypes (Factory), or shared initialization (Abstract Factory).
3. Controller
The first object beyond the UI layer that handles a system event.
# Web framework view = controller
@app.post("/orders")
def create_order(request, order_data: OrderCreate):
order = OrderService.create(order_data) # delegate to domain
return OrderResponse.from_orm(order)
The controller:
- Receives the request (HTTP, message, CLI command).
- Validates input shape.
- Coordinates domain logic (delegates).
- Returns response.
Anti-pattern: “Fat controllers” with business logic inside. Controllers should be thin orchestrators; business logic lives in domain services / entities.
4. Low Coupling
Minimize dependencies between classes.
# BAD — tight coupling
class OrderService:
def __init__(self):
self.db = PostgresOrderRepository() # bound to concrete impl
self.email = SendgridEmailService() # bound to concrete impl
# GOOD — low coupling via abstractions
class OrderService:
def __init__(self, repo: OrderRepository, email: EmailService):
self.repo = repo
self.email = email
The good version depends on abstractions, not concrete implementations. Swap them out (real impl in prod, fakes in tests) without changing the service.
Coupling matters because it spreads change: tight coupling means modifying one class forces changes in many. Loose coupling localizes change.
Measure: how many other classes does this class import / call? More = more coupled.
5. High Cohesion
Each class should do one thing well — its members should serve a unified purpose.
# BAD — low cohesion (kitchen sink)
class User:
def authenticate(self, password): ...
def send_email(self, message): ...
def calculate_invoice(self): ...
def render_avatar(self): ...
# GOOD — split by purpose
class User:
def authenticate(self, password): ...
class EmailService:
def send(self, user, message): ...
class InvoiceCalculator:
def for_user(self, user): ...
A cohesive class has all members contributing to one responsibility. A non-cohesive class is “what does this class do?” answered with “uh, lots of things.”
Coupling and cohesion are paired: lower coupling + higher cohesion = better design. Together they’re the heart of “good OO.”
6. Indirection
Introduce an intermediate class to decouple two others.
# Direct coupling
class OrderService:
def place(self, order):
slack.send_message(f"new order: {order.id}") # tightly coupled
# Indirection via a notifier
class Notifier:
def order_placed(self, order):
# Slack, email, push — change here without touching OrderService
slack.send_message(f"new order: {order.id}")
class OrderService:
def __init__(self, notifier: Notifier):
self.notifier = notifier
def place(self, order):
self.notifier.order_placed(order)
Notifier decouples OrderService from slack. Want to switch notification channels? Edit Notifier. OrderService is untouched.
Many GoF patterns are forms of indirection: Adapter, Facade, Proxy, Mediator.
Cost: more classes, more indirection to follow when reading code. Only add when the decoupling pays off.
7. Polymorphism
Handle type-based behavior variation through polymorphic methods, not if/elif/else.
# BAD — type-based switching
def calculate_shipping(order):
if order.type == "standard":
return order.weight * 1.5
elif order.type == "express":
return order.weight * 3.0
elif order.type == "overnight":
return order.weight * 5.0 + 10
# GOOD — polymorphism
class ShippingMethod(Protocol):
def cost(self, order) -> Decimal: ...
class StandardShipping:
def cost(self, order): return order.weight * Decimal("1.5")
class ExpressShipping:
def cost(self, order): return order.weight * Decimal("3.0")
class OvernightShipping:
def cost(self, order): return order.weight * Decimal("5.0") + 10
# Usage
def calculate_shipping(order, method: ShippingMethod):
return method.cost(order)
Adding a new shipping type = adding a new class, not modifying the if/elif. Open/Closed principle in action.
8. Pure Fabrication
When no domain class makes sense for a responsibility, invent one.
# UserRepository doesn't correspond to a real-world thing —
# it's a "pure fabrication" for persistence concerns
class UserRepository:
def find_by_id(self, id): ...
def save(self, user): ...
def delete(self, user): ...
# Domain class stays clean
class User:
def __init__(self, id, name, email):
self.id = id
self.name = name
self.email = email
A User shouldn’t know about the database. Persistence is invented as UserRepository. Likewise: OrderProcessor, EmailSender, PasswordHasher. They have no domain analog, but they exist to keep domain classes pure.
Without pure fabrication, you’d cram persistence into entities → “active record” pattern — fine for small apps, painful at scale.
9. Protected Variations
Identify what’s likely to change; wrap it behind a stable interface.
# Protect from variations in payment providers
class PaymentGateway(Protocol):
def charge(self, amount, card): ...
class StripeGateway: ...
class BraintreeGateway: ...
class MockGateway: ... # for tests
class CheckoutService:
def __init__(self, gateway: PaymentGateway):
self.gateway = gateway
CheckoutService is protected from “what payment provider do we use this quarter?” The interface absorbs the change.
Same idea as the Dependency Inversion Principle from SOLID. GRASP generalizes: identify the variation point, wrap it.
GRASP vs SOLID — overlap
| GRASP | SOLID equivalent |
|---|---|
| High Cohesion | Single Responsibility Principle |
| Polymorphism | Open/Closed Principle, Liskov |
| Protected Variations | Dependency Inversion |
| Low Coupling | (informal across SOLID) |
| Pure Fabrication | (no direct analog) |
| Information Expert | (no direct analog) |
| Creator, Controller, Indirection | (no direct analogs) |
GRASP came first (1997); SOLID was popularized later (Uncle Bob, 2000s). They’re complementary. GRASP feels more “object-design heuristics”; SOLID feels more “class-design principles.”
Applying GRASP — the workflow
When designing a feature:
- List the responsibilities (the things the system must do).
- For each: find the information expert (who has the data?).
- Verify high cohesion + low coupling — if a class is doing too much, split.
- Identify variation points — wrap them (protected variations).
- Pure-fabricate cross-cutting concerns (logging, persistence, notifications).
- Controllers handle entry points; domain objects do the work.
In code review:
- “Does this class have a single purpose?” → cohesion check.
- “How many other classes does this depend on?” → coupling check.
- “If we add a third shipping method, what changes?” → polymorphism / OCP check.
Common pitfalls
- Misapplied Information Expert: putting logic on the wrong class because it “has the data.” If the responsibility belongs to a different concept (e.g., calculation strategy), use a dedicated class.
- Over-fabrication: every operation becomes a service class. Ends in 50 classes for what could be 5 methods on an entity.
- God controllers: business logic creeps into the controller layer. Keep controllers thin.
- Polymorphism everywhere: not every
ifneeds a class hierarchy. Two cases: skip the hierarchy. - Indirection for its own sake: adding a wrapper class with no decoupling benefit.
Common interview confusions
- “GRASP and SOLID are the same.” — overlap (cohesion ≈ SRP; polymorphism ≈ OCP/LSP), but GRASP has unique principles (Information Expert, Creator, Controller, Pure Fabrication).
- “Information Expert just means ‘methods go on classes’.” — it’s specifically about who has the data. Without data, no behavior — so place the method where the data lives.
- “Pure Fabrication is anti-OO.” — it’s pragmatic. Some responsibilities don’t fit existing domain classes; inventing a service class is normal.
Interview angle
- “What’s GRASP?” — General Responsibility Assignment Software Patterns. Nine principles by Craig Larman for assigning responsibilities to classes: Information Expert, Creator, Controller, Low Coupling, High Cohesion, Indirection, Polymorphism, Pure Fabrication, Protected Variations.
- “GRASP vs SOLID — how do they relate?” — overlapping; GRASP came first (1997). High Cohesion ≈ SRP; Polymorphism ≈ OCP; Protected Variations ≈ DIP. GRASP adds unique principles around responsibility placement (Information Expert, Creator).
- “What’s the Information Expert principle?” — assign a responsibility to the class that has the information needed to fulfill it.
Orderknows its items, soOrder.total()lives onOrder. Co-locating data and behavior reduces dependencies. - “When would you use Pure Fabrication?” — when no domain class fits a responsibility. Persistence, logging, notifications, hashing — these don’t correspond to domain concepts. Inventing
UserRepository,Notifier,Loggerkeeps domain entities pure. - “How does Protected Variations relate to dependency inversion?” — same idea. Identify points where change is likely (payment providers, storage backends); wrap behind a stable interface. The stable interface absorbs the variation; high-level code is shielded.
- “What’s the difference between Low Coupling and High Cohesion?” — coupling is across classes (how dependent class A is on class B); cohesion is within a class (how focused are its members on one purpose). Lower coupling + higher cohesion = better design.
- “Where do you apply Controller in a web app?” — view functions / route handlers are controllers. They receive HTTP requests, validate, delegate to domain services, return responses. Business logic does NOT live in the controller.