GRASP: Assigning Object Responsibilities

Learn all nine GRASP patterns through a Java order workflow, with code, trade-offs, failure cases, and design review questions for intermediate developers.

published: reading time: 16 min read author: GeekWorkBench
Quick Summary

GRASP gives teams nine heuristics for assigning object responsibilities, from Information Expert and Creator to Polymorphism and Protected Variations. An order-checkout example shows how to balance domain ownership with cohesion, coupling, controllers, policies, and adapters, while production failure scenarios and a trade-off table make the costs visible. Use the step-by-step review and checklist to decide where behavior belongs, when an abstraction protects a real change, and how to keep design choices testable and observable.

GRASP: Assigning Object Responsibilities

Introduction

Object-oriented design often gets stuck on a deceptively simple question: which object should do this work? Put every rule in a service and the domain objects become passive data. Put every workflow in one object and that object becomes difficult to change. GRASP gives you a set of named heuristics for making these assignments deliberately.

GRASP stands for General Responsibility Assignment Software Patterns. Craig Larman presents nine patterns in Applying UML and Patterns: Information Expert, Creator, Controller, Low Coupling, High Cohesion, Polymorphism, Pure Fabrication, Indirection, and Protected Variations. These are design questions, not a recipe that dictates one class diagram. We will use an order checkout example to see how the questions guide a design and where each answer has a cost.

What GRASP Helps You Decide

A responsibility can mean knowing information, doing work, or coordinating an operation. For each piece of behavior, ask who has the data needed to do it, who should create a related object, and what dependencies the assignment introduces. Then look at the whole class: does the new job fit its existing purpose, or is the class collecting unrelated reasons to change?

The nine patterns work together. Information Expert and Creator help choose a likely owner. Controller gives system events a clear entry point. Low Coupling and High Cohesion help evaluate the result. Polymorphism handles type-based variation. Pure Fabrication and Indirection give you alternative homes for responsibilities when the domain model alone would create poor dependencies. Protected Variations helps keep likely changes behind stable interfaces.

A Compact Order Example

Suppose a customer checks out an order containing products. The order knows its line items, each line knows its quantity and product, and a pricing policy calculates discounts. A checkout controller coordinates the request and delegates the business work.

flowchart LR
    Web[Checkout endpoint] --> Controller[CheckoutController]
    Controller --> Order[Order]
    Order --> Item[OrderLine]
    Item --> Product[Product]
    Order --> Policy[PricingPolicy]
    Controller --> Gateway[PaymentGateway]

The Order has the information to calculate a merchandise total, so Information Expert points there. A policy interface can handle discount rules that vary by customer or promotion. The application controller coordinates checkout, but it should not calculate prices or decide whether a line belongs to an order.

import java.math.BigDecimal;
import java.util.List;

record Product(String sku, BigDecimal unitPrice) {}
record OrderLine(Product product, int quantity) {
    BigDecimal subtotal() {
        if (quantity <= 0) throw new IllegalArgumentException("quantity must be positive");
        return product.unitPrice().multiply(BigDecimal.valueOf(quantity));
    }
}

interface PricingPolicy {
    BigDecimal discountFor(Order order);
}

final class Order {
    private final List<OrderLine> lines;

    Order(List<OrderLine> lines) {
        if (lines.isEmpty()) throw new IllegalArgumentException("order must contain a line");
        this.lines = List.copyOf(lines);
    }

    BigDecimal merchandiseTotal() {
        return lines.stream()
                .map(OrderLine::subtotal)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

final class CheckoutController {
    private final PricingPolicy pricing;
    private final PaymentGateway payments;

    CheckoutController(PricingPolicy pricing, PaymentGateway payments) {
        this.pricing = pricing;
        this.payments = payments;
    }

    Receipt checkout(Order order, PaymentDetails details) {
        BigDecimal total = order.merchandiseTotal().subtract(pricing.discountFor(order));
        if (total.signum() < 0) throw new IllegalStateException("discount exceeds order total");
        return payments.charge(details, total);
    }
}

The example is intentionally small. A production checkout would also need currency rules, tax, shipping, authorization and idempotency. The design question remains the same: which object owns each rule, and what does that assignment make harder or easier to change?

The Nine GRASP Patterns

1. Information Expert

Assign a responsibility to the object that has the information needed to fulfill it. OrderLine knows its product and quantity, so it can calculate its subtotal. Order knows its lines, so it can sum them. This keeps calculations close to the data they depend on.

Expert does not mean “put every calculation in the object with the most fields.” A tax calculation that requires jurisdiction lookup, exemptions, and external rates may be better owned by a tax service or policy. Follow the information to its owner, then check cohesion and dependencies.

2. Creator

Give object B responsibility for creating object A when B contains, aggregates, records, or closely uses A. An Order can create an OrderLine because it owns the collection and needs to preserve order invariants. A factory can be a better creator when construction needs configuration or varies by runtime context.

Creation is behavior, too. If callers can add a line without the order validating quantity or duplicate rules, the object boundary is not protecting the domain.

3. Controller

Assign the handling of a system event to a non-UI object that represents the overall system, a use case, or a session. CheckoutController receives the checkout request and coordinates collaborators. It does not need to know how each product is priced or how a payment provider talks over HTTP.

Keep controllers thin enough to coordinate. If one controller becomes the home for calculations, authorization policy, persistence details, and domain decisions, it has accumulated responsibilities that belong elsewhere.

4. Low Coupling

Prefer assignments that keep classes from depending on many other classes or unstable details. An interface such as PricingPolicy lets checkout depend on a pricing contract rather than a particular promotion implementation. Lower coupling makes local changes and focused tests easier.

Coupling cannot be eliminated. The goal is to make dependencies intentional and manageable. A new interface at every method boundary can add more concepts than the dependency it hides.

5. High Cohesion

Keep a class’s responsibilities closely related, with a focused purpose. Order validating its lines and calculating order-derived values can be cohesive. Adding HTML rendering, database persistence, email delivery, and payment transport to Order would give it unrelated reasons to change.

High cohesion is a useful review lens, not a demand that every class have exactly one method or one sentence of responsibility. Some orchestration classes naturally coordinate several related steps.

6. Polymorphism

When behavior varies by type, assign the variation to implementations behind a shared operation rather than scattering type checks through callers. For example, PricingPolicy may have RegularPricing and MemberPricing implementations. Checkout asks the policy for a discount without branching on customer type.

Use polymorphism when the variants represent meaningful behavior and the set can evolve. A small, closed set of simple cases may be clearer as a switch expression until variation grows.

7. Pure Fabrication

Create a service object that is not a domain concept when assigning the work to a domain object would hurt cohesion or coupling. A PaymentGateway adapter can translate the application’s payment request into a provider API call. A repository can handle persistence without forcing database details into Order.

“Fabrication” here means a made-up software responsibility holder, not fabricated data. It is a way to protect the domain model and keep technical concerns in their own collaborators.

8. Indirection

Insert an intermediary object to keep two components from depending directly on each other. An adapter between checkout and a payment provider is indirection: checkout speaks to a stable interface while the adapter handles the provider’s SDK and response format.

Indirection has a price in navigation and debugging. Add it when it isolates a real boundary, such as a third-party provider or a replaceable integration, and avoid it when it only renames a direct call.

9. Protected Variations

Identify a point of likely change and put a stable interface around it so that the change does not ripple into clients. PricingPolicy protects checkout from pricing variations. A PaymentGateway protects the application from provider-specific details.

This pattern is about a boundary, not prediction for its own sake. Protect a variation that exists or has a credible reason to change. Speculative extension points can make today’s code harder to follow.

Assigning a Responsibility Step by Step

Take one operation, such as “add a product to an order,” and make the decision visible:

  1. State the rule: an order cannot contain a line with a non-positive quantity.
  2. Find the information: the order owns its line collection; the proposed line has the quantity.
  3. Give the operation to a suitable expert: Order.addLine(product, quantity) can validate and create the line.
  4. Check the resulting class: does adding this behavior keep Order focused on order state and invariants?
  5. Check dependencies: does Order now need a database, web framework, or vendor SDK? If so, revisit the assignment.
  6. Test the behavior through the public operation and verify the invariant cannot be bypassed.

Repeat the review from the caller’s point of view. If the controller has to fetch several fields, calculate a result, then push the result back into the order, it may be asking for behavior that belongs with the order. This is the practical connection between GRASP and “Tell, Don’t Ask.” For alternatives to inheritance-driven reuse in the same kind of design discussion, see Composition over Inheritance in Java.

When to Use

  • A class is taking on behavior because it is convenient to reach, and you need to check whether another object has a better claim to it.
  • A use case has become a long sequence of data reads and writes with domain decisions in the middle.
  • A dependency on a vendor, database, or volatile rule is spreading through otherwise stable code.
  • You are reviewing a design and need a shared vocabulary for discussing ownership and trade-offs.
  • A design pattern is being proposed, and you want to first describe the responsibility or variation it addresses.

When NOT to Use

  • The design is small, stable, and clear already. Do not add abstractions merely to prove that a GRASP pattern was considered.
  • A one-off conditional has two simple cases with no meaningful type-specific behavior. A polymorphic hierarchy may obscure it.
  • You cannot name the variation or dependency that an interface is protecting. Wait for evidence before adding the seam.
  • A class has a few related methods and is easy to change. Splitting it just to maximize a vague measure of cohesion can make the design harder to navigate.
  • A framework or domain boundary already owns the responsibility clearly. Duplicating that coordination in a second controller usually adds confusion.

Production Failure Scenarios

A bloated checkout controller

The controller queries persistence, applies discount rules, computes tax, mutates order state, charges a provider, and sends email. A tax change breaks checkout tests, while a provider timeout leaves the order marked paid. The GRASP signals are weak cohesion and excessive coupling. Move domain rules to domain experts or policies, isolate provider effects, and define the order/payment state transition explicitly.

Stale totals from duplicated knowledge

The API computes the order total from line items, but the invoice service uses a cached total field that was not updated after a partial cancellation. Each component believes it is the expert. Establish one source of truth for the calculation and define how snapshots are versioned or invalidated.

A pricing abstraction that hides the wrong rule

Checkout calls a generic policy, but a promotion has eligibility rules that depend on an authenticated customer and a campaign window. If the policy receives incomplete or untrusted context, it may grant a discount incorrectly. Make the policy inputs explicit, enforce eligibility on the trusted server side, and audit the decision without logging sensitive payment data.

Provider failures leak across the boundary

Code throughout the application depends on a vendor SDK’s exception types and response objects. A provider change becomes a broad rewrite, and retries may charge twice. Keep provider details in an adapter, model idempotent payment requests, and translate failures into application-level outcomes.

Trade-Off Table

Pattern Main question Benefit Cost or risk
Information Expert Who has the needed information? Behavior stays near relevant state Experts can become overloaded if cohesion is ignored
Creator Who should construct this object? Invariants can be protected at creation A convenient creator may become a construction hub
Controller Who receives the system event? A clear boundary for use-case coordination A controller can turn into a god object
Low Coupling Which dependencies can be reduced? Changes stay more local Too many interfaces can hide the real collaboration
High Cohesion Do these responsibilities belong together? Classes remain understandable Over-splitting can fragment one coherent behavior
Polymorphism Does behavior vary by type? Variation is localized behind a contract Extra types and dispatch can be needless for fixed cases
Pure Fabrication Would a domain object be a poor technical owner? Technical concerns stay out of domain objects Service layers can become generic dumping grounds
Indirection Should these components know each other directly? A boundary absorbs translation or change More layers complicate tracing and debugging
Protected Variations What change point needs insulation? Volatile details have a stable seam Speculative seams add maintenance before value exists

Observability Checklist

GRASP does not define a telemetry standard, but responsibility boundaries can make operational signals easier to place. For an order workflow, review whether:

  • Checkout requests carry a correlation ID through controller, pricing, and payment adapter logs.
  • Pricing decisions record a rule or campaign identifier and the resulting amount, while omitting unnecessary personal data.
  • Payment attempts and state transitions are traceable by an idempotency key or internal payment ID.
  • Adapter metrics distinguish provider latency, timeouts, declines, and application validation failures.
  • Logs identify the responsible component and outcome without dumping full objects or payment details.
  • Alerts follow user-visible failure rates and stuck workflow states, rather than raw exception counts alone.

Security and Compliance Notes

Responsibility assignment does not replace security controls. Keep authorization checks at a trusted application boundary and verify that the authenticated user may act on the order before coordinating payment. Never trust a client-supplied total or discount; derive amounts from validated server-side state and pricing rules.

Keep payment credentials and card data inside the approved payment-provider flow. Do not place secrets or sensitive data in domain events, logs, traces, or exception messages. Limit who can read pricing and payment audit records, and retain only the data required by the product and applicable policy. If a payment integration or data-handling requirement changes, the adapter and its contracts should make the affected boundary clear, while compliance decisions still require review against the system’s actual obligations.

Common Pitfalls and Anti-Patterns

  • Treating GRASP as a rigid checklist: the patterns are heuristics. Optimize for understandable responsibilities in the actual context.
  • Making the “expert” responsible for everything: access to data is a starting point; check cohesion, dependencies, and external side effects.
  • Confusing Controller with domain owner: the controller receives and coordinates a request. It should not absorb the domain model’s decisions.
  • Creating a service for every noun: pure fabrication is useful when it improves the design, not as a mandate to wrap every method.
  • Adding interfaces without a protected variation: each abstraction has a reading and maintenance cost. Name the boundary it buys.
  • Using polymorphism for data differences: if behavior does not vary, multiple subclasses may only multiply code paths.
  • Calling low coupling “no dependencies”: objects must collaborate. Aim for dependencies that are explicit and point toward stable contracts.
  • Relying on diagrams alone: a class diagram cannot prove invariants are enforced. Check actual call paths and tests.

Quick Recap Checklist

  • Can I state the responsibility as a behavior or decision, not just a class name?
  • Which object has the information needed to perform it?
  • Does the proposed creator protect the new object’s invariants?
  • Is the system-event controller coordinating, or doing domain work itself?
  • Does the assignment preserve high cohesion and manageable coupling?
  • Is the variation real enough to justify polymorphism or an interface?
  • Would a pure fabrication or indirection boundary isolate a technical concern?
  • Have I named what is likely to vary before applying Protected Variations?
  • Can tests and production signals follow the responsibility to its owner?

Interview Questions

1. What is Information Expert, and why does it not mean that one class should own every calculation?

Information Expert assigns a responsibility to an object with the information needed to fulfill it. The assignment still needs to preserve cohesion and avoid unwanted dependencies. An order can sum its lines, while an external tax-rate lookup may belong to a dedicated service because it depends on jurisdiction data and an external source.

2. How is a GRASP Controller different from a domain object?

A Controller receives a system event and coordinates the use case. A domain object owns domain state and rules. A checkout controller can call an order and payment gateway, but the order should still enforce its own invariants and derive values from its state.

3. When would you choose Pure Fabrication or Indirection over Information Expert?

Choose a Pure Fabrication when giving a technical task to a domain object would reduce cohesion or introduce infrastructure coupling, such as assigning persistence to a repository. Choose Indirection when an intermediary should separate collaborators, such as an adapter translating a payment provider API. The extra object should solve a real design pressure.

Further Reading

Conclusion

GRASP helps you place behavior by asking who knows the necessary information, who should create related objects, and how the assignment affects cohesion and coupling. The patterns also give you ways to handle variation and technical boundaries without forcing every responsibility into a domain entity.

In the order example, the order owns calculations based on its lines, the checkout controller coordinates the use case, and policies and adapters contain behavior that varies or crosses an external boundary. Use those assignments as hypotheses: test the rules, observe how changes travel, and revise the design when the evidence says a responsibility is misplaced.

Category

Related Posts

Discovering Domain Concepts and Behaviors

Turn use cases and domain language into a small, behavior-rich model. Learn how to find candidates, protect invariants, and avoid making every noun a class.

#object-oriented-design #domain-modeling #design

SOLID Principles in Practice

Apply all five SOLID principles to a small checkout design, see where they help, and learn when extra interfaces or indirection make code harder to change.

#object-oriented-design #solid #design-principles

Tell, Don't Ask and the Law of Demeter

Apply Tell, Don't Ask and the Law of Demeter to keep object rules near their data, reduce fragile navigation, and see when a direct query is clearer in context.

#object-oriented-design #tell-dont-ask #law-of-demeter