GRASP — General Responsibility Assignment Software Patterns

7 interview angles 8 min read source

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 LineItemOrder 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:

  1. List the responsibilities (the things the system must do).
  2. For each: find the information expert (who has the data?).
  3. Verify high cohesion + low coupling — if a class is doing too much, split.
  4. Identify variation points — wrap them (protected variations).
  5. Pure-fabricate cross-cutting concerns (logging, persistence, notifications).
  6. 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 if needs 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. Order knows its items, so Order.total() lives on Order. 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, Logger keeps 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.