I’m designing a system where various “handlers” apply business rules to an object. Initially I had each handler receive the full domain object:
// A large domain object with many properties and methods
public class Order {
private List<Item> items;
private Customer customer;
private Address billingAddress;
private Address shippingAddress;
private PaymentInfo payment;
private DiscountRules discountRules;
// … lots more fields and methods …
}
// Rule interface that takes the entire Order
public interface Rule {
void apply(Order order);
}
// Example handler only cares about items and customer status:
public class LoyaltyDiscountRule implements Rule {
@Override
public void apply(Order order) {
if (order.getCustomer().isGoldMember()) {
order.applyDiscount(0.05);
}
}
}
Passing the whole Order felt too heavy—each rule only needs a couple of fields, yet has full access to mutate the order.
Refactored approach
I introduced a slim context object exposing only what a rule may need, plus an interface for allowed side-effects:
// Exposes just the data each rule might use
public class RuleContext {
private final List<Item> items;
private final Customer customer;
private final OrderActions actions;
public RuleContext(List<Item> items, Customer customer, OrderActions actions) {
this.items = items;
this.customer = customer;
this.actions = actions;
}
public List<Item> getItems() { return items; }
public Customer getCustomer() { return customer; }
public OrderActions getActions() { return actions; }
}
// Only these modifications are allowed
public interface OrderActions {
void addDiscount(double rate);
void markFreeShipping();
}
Now each rule implements:
public interface Rule {
void apply(RuleContext ctx);
}
And a handler becomes:
public class BulkPurchaseRule implements Rule {
@Override
public void apply(RuleContext ctx) {
if (ctx.getItems().size() > 10) {
ctx.getActions().addDiscount(0.10);
}
}
}
Questions
- Is this “Context + Actions” pattern a well-known or recommended design?
- What trade-offs does it introduce compared to passing the full domain object or individual parameters?
- Are there alternative patterns for limiting each handler’s access to only the data and mutations it needs?