Skip to content

GRASP — Find the Misassigned Responsibility

Ten snippets, each with a responsibility placed on the wrong class. For each: read the code, decide which GRASP pattern is violated and where the responsibility should live, then check the diagnosis. These are the misassignments you'll actually see in real PRs — Feature Envy, infra-coupled domain, type-switches, bloated controllers, missing experts, over-fabrication, and unearned indirection.


Snippet 1

class TaxReport {
    String render(Employee e) {
        return e.getFirstName() + " " + e.getLastName()
             + " | gross: " + e.getSalary()
             + " | net: " + e.getSalary().subtract(e.getSalary().multiply(e.getTaxRate()));
    }
}
What's wrong? **Information Expert violation (Feature Envy).** `render` uses *only* `Employee`'s data and none of `TaxReport`'s own. The net-pay calculation in particular operates entirely on `Employee`'s salary and tax rate. **Fix:** Move the calculation onto `Employee` (`Employee.netPay()`) and expose `e.fullName()`. The report then *asks* the expert instead of reaching into its fields. This also removes the repeated `getSalary()` chains and respects the Law of Demeter.

Snippet 2

class Order {
    private List<OrderLine> lines;
    void save() {
        var em = Persistence.createEntityManagerFactory("app").createEntityManager();
        em.getTransaction().begin();
        em.persist(this);
        em.getTransaction().commit();
    }
}
What's wrong? **Low Coupling violation — Information Expert misapplied.** `Order` has the data, so Information Expert *tempts* you to put `save()` here. But this couples the domain entity to JPA, makes it untestable without a persistence unit, and lowers cohesion (entity + persistence). **Fix:** Pure Fabrication — extract an `OrderRepository` interface (Indirection) with a `JpaOrderRepository` implementation. `Order` keeps zero persistence imports. Information Expert is the first guess; Low Coupling overrules it at infrastructure boundaries.

Snippet 3

class NotificationSender {
    void send(Notification n) {
        switch (n.getType()) {
            case "EMAIL": new SmtpClient().send(n.getTo(), n.getBody()); break;
            case "SMS":   new TwilioClient().text(n.getTo(), n.getBody()); break;
            case "PUSH":  new FcmClient().push(n.getTo(), n.getBody()); break;
        }
    }
}
What's wrong? **Polymorphism violation.** Behaviour varies by `type`, expressed as a `switch` on a string discriminator. Every new channel edits this method (an OCP violation too). **Fix:** A `sealed interface Channel { void send(String to, String body); }` with `EmailChannel`, `SmsChannel`, `PushChannel` implementations. The `switch` becomes a single polymorphic `channel.send(...)`. Adding a channel adds a class and edits nothing. Combine with Protected Variations since the channel set is a known variation point.

Snippet 4

class PlaceOrderController {
    Receipt place(CartDto dto) {
        BigDecimal total = BigDecimal.ZERO;
        for (var i : dto.items()) total = total.add(i.price().multiply(BigDecimal.valueOf(i.qty())));
        if (total.compareTo(new BigDecimal("100")) > 0) total = total.multiply(new BigDecimal("0.9"));
        new JdbcTemplate(ds).update("INSERT INTO orders ...", total);
        return new Receipt(total);
    }
}
What's wrong? **Controller bloat — three misassignments.** A Controller should delegate, not compute. Here it (1) calculates the total (Information Expert: belongs on `Order`/`Sale`), (2) applies a discount rule inline (Protected Variations: belongs behind a `DiscountPolicy`), and (3) runs SQL (Pure Fabrication: belongs in a repository). **Fix:** `Order` computes its total; a `DiscountPolicy` applies the discount; an `OrderRepository` persists. The controller becomes `order = Order.from(dto); discountPolicy.apply(order); repo.save(order); return Receipt.of(order);` — pure delegation.

Snippet 5

class Customer {
    String name; List<Order> orders;
    String getName() { return name; }
    List<Order> getOrders() { return orders; }
}
class CustomerService {
    boolean isVip(Customer c) {
        return c.getOrders().stream()
                .map(o -> o.getTotal())
                .reduce(BigDecimal.ZERO, BigDecimal::add)
                .compareTo(new BigDecimal("10000")) > 0;
    }
}
What's wrong? **Anemic domain — Information Expert ignored, Pure Fabrication over-applied.** `isVip` is a *business rule* operating purely on `Customer`'s own data (its orders), yet it lives in a service while `Customer` is a getter-only struct. **Fix:** Move `isVip()` (and a `lifetimeValue()` helper) onto `Customer` — it's the Expert. `CustomerService` is fine for *cross-aggregate* or *infrastructural* work, but a rule over a customer's own orders belongs on `Customer`. This is the anemic-domain anti-pattern: behaviour drained off the entity into a service.

Snippet 6

class ReportController {
    void daily() {
        var rows = jdbc.query("SELECT * FROM sales WHERE day = CURRENT_DATE");
        var csv = rows.stream().map(r -> r.get("id") + "," + r.get("total")).collect(joining("\n"));
        Files.writeString(Path.of("/reports/daily.csv"), csv);
        new SmtpClient().send("ops@acme.com", "Daily report", csv);
    }
}
What's wrong? **High Cohesion violation + Controller bloat.** One method queries the database, formats CSV, writes a file, *and* emails — four unrelated responsibilities (low cohesion) all in a controller that should only coordinate. **Fix:** `SalesQuery` (repository/Expert) fetches; `CsvFormatter` (Pure Fabrication) formats; a `ReportStore` writes; a `Notifier` (Indirection) emails. The controller delegates the four steps. Each fabrication is independently testable and changes for one reason.

Snippet 7

class Game {
    Board board;
    Game() { this.board = BoardFactory.getInstance().createStandardBoard(); }
}
class BoardFactory {
    private static final BoardFactory I = new BoardFactory();
    static BoardFactory getInstance() { return I; }
    Board createStandardBoard() { return new Board(8, 8); }
}
What's wrong? **Creator misapplied + unnecessary Indirection (over-engineering).** A `BoardFactory` singleton for a single, parameterless construction is ceremony. Creator says: `Game` *aggregates* its `Board` and has the initializing data (the dimensions), so `Game` is the natural creator. The factory's global singleton adds a hidden static coupling for no variation. **Fix:** `this.board = new Board(8, 8);` directly in `Game`. Introduce a factory *only* when creation becomes complex (variants, pooling, conditional logic) or when board type is a predicted variation point. Indirection has a cost; don't pay it on spec.

Snippet 8

class ShippingCalculator {
    BigDecimal cost(Order order) {
        BigDecimal weight = BigDecimal.ZERO;
        for (var line : order.getLines())
            weight = weight.add(line.getProduct().getWeight().multiply(BigDecimal.valueOf(line.getQty())));
        return weight.multiply(new BigDecimal("2.50"));
    }
}
What's wrong? **Information Expert violation (Feature Envy) — the weight calculation is in the wrong place.** `ShippingCalculator` reaches three dots deep (`order.getLines()` → `line.getProduct()` → `getWeight()`) to compute *the order's total weight*, which `Order` is the Expert for. **Fix:** `Order.totalWeight()` (recursive Expert: `OrderLine.weight()` asks its product). The calculator then does `order.totalWeight().multiply(rate)` — it owns only the *rate-application* responsibility, which is legitimately its own. This splits the responsibility correctly: weight to the Expert, rate logic to the calculator.

Snippet 9

interface IOrderValidator { boolean validate(Order o); }
class OrderValidator implements IOrderValidator { public boolean validate(Order o) { return o.total().signum() > 0; } }
interface IOrderMapper { OrderDto map(Order o); }
class OrderMapper implements IOrderMapper { public OrderDto map(Order o) { /* ... */ } }
interface IPriceFormatter { String format(BigDecimal p); }
class PriceFormatter implements IPriceFormatter { public String format(BigDecimal p) { /* ... */ } }
What's wrong? **Unearned Indirection (speculative generality).** Every class has a single-implementor interface with no test-seam need and no predicted second implementation. Protected Variations/Indirection has a real cognitive cost and only pays off against *actual or predicted* variation. Here it's pure ceremony — an interface per concrete class. **Fix:** Delete the interfaces; depend on the concrete classes directly. These are pure functions with no infrastructure to stub, so there's no test seam to justify. Extract an interface *when* a second implementation actually arrives (a five-minute IDE refactor). Reserve interfaces for genuine variation points (payment providers, storage backends) and un-mockable boundaries.

Snippet 10

class OrderService {
    boolean canPlace(Customer customer, Order order) {
        BigDecimal outstanding = customer.getOpenOrders().stream()
                                         .map(Order::getTotal)
                                         .reduce(BigDecimal.ZERO, BigDecimal::add);
        return outstanding.add(order.getTotal()).compareTo(customer.getCreditLimit()) <= 0;
    }
}
What's wrong? **This one is *correct* — the trap is "fix" it by forcing it onto an entity.** The rule "can this customer place this order?" spans *two* equally-relevant objects: `Customer` (credit limit, open orders) and `Order` (its total). There is *no single Information Expert*. Forcing it onto `Customer` or `Order` would make one entity reach into the other's data. **Verdict:** A Pure Fabrication / domain service (`OrderService.canPlace(...)`, or an `OrderPlacementPolicy`) is the *right* home for cross-aggregate logic — this is exactly the case Information Expert *doesn't* cover. The only refinement: name it for the rule (`OrderPlacementPolicy`) rather than a vague `OrderService`, and lean on each entity's own experts (`customer.outstandingTotal()`, `order.total()`) so the policy only *orchestrates*. The lesson: not every service method is an anemic-domain smell — cross-aggregate rules legitimately live in fabrications.

Pattern recap of the ten: Feature Envy → Expert (1, 8); infra-coupled entity → Low Coupling + Pure Fabrication (2); type-switch → Polymorphism (3); controller bloat → delegate to Expert/Policy/Repository (4, 6); anemic domain → Expert reclaimed (5); over-engineering → Creator + drop Indirection (7); unearned interfaces → drop Indirection (9); and the correct cross-aggregate service that you must not force onto an entity (10). The recurring senior lesson: Information Expert is the default, but Low Coupling, cross-aggregate reality, and YAGNI each overrule it in specific, recognisable situations.