Information and behavior belong together
If an object has the facts required to enforce a rule, that is often a clue it should own the rule.
Responsibility-driven design is where OOAD becomes sharply practical. Instead of asking which class should contain a method because of existing structure, you ask which object should own the behavior because of its role in the model. That shift prevents the slow drift toward utility-heavy and service-heavy Java designs.
Checkout, payment, shipment, refund, and notification flows often become easier to implement by centralizing logic in one service. They become harder to maintain for exactly the same reason. Responsibility-driven design helps distribute behavior according to domain meaning, orchestration role, and policy boundaries.
Responsibility-driven design is closely related to cohesion, GRASP, and Tell, Don't Ask. It pushes you to assign behavior where it fits naturally, rather than where it is easiest to call from. In Java, that often means balancing rich domain behavior with focused application services and explicit policies.
One useful test is to narrate the workflow in plain language. If the story says, "the order decides," "the shipment transitions," or "the pricing policy calculates," that is usually a sign the design can carry those responsibilities directly rather than routing everything through one orchestration class.
The result is not always fewer classes. It is clearer ownership and less ambiguous behavior.
If an object has the facts required to enforce a rule, that is often a clue it should own the rule.
Application-level orchestration is valid, but it should not swallow every domain decision.
Discounts, fraud checks, and reservation logic often belong in explicit policy or strategy objects.
package org.javaomnibus.ecommerce.process;
public final class Order {
private final Money total;
private PaymentStatus paymentStatus = PaymentStatus.PENDING;
public Order(Money total) {
this.total = total;
}
public void authorizePayment(PaymentAuthorizer paymentAuthorizer, PaymentMethod paymentMethod) {
PaymentResult paymentResult = paymentAuthorizer.authorize(total, paymentMethod);
if (!paymentResult.approved()) {
throw new IllegalStateException("Payment was not approved");
}
paymentStatus = PaymentStatus.AUTHORIZED;
}
}
interface PaymentAuthorizer {
PaymentResult authorize(Money amount, PaymentMethod paymentMethod);
}
Not every object should become highly behavioral. Some data-transfer boundaries remain intentionally simple. The key is to add responsibility where the domain actually benefits from protection and meaning.