GRASP Design Principles in Java: 9 Responsibility Patterns Every Senior Developer Must Know 2026
Most developers learn GoF design patterns but skip GRASP — the foundational responsibility-assignment principles that determine which class should own which behavior. Coined by Craig Larman, GRASP gives you nine concrete heuristics for making object-oriented design decisions that lead to maintainable, testable, and evolvable systems. This deep-dive covers all nine with real Spring Boot e-commerce examples.
TL;DR — GRASP in One Sentence
"GRASP (General Responsibility Assignment Software Patterns) provides 9 principles for deciding which class should own which responsibility — from assigning behavior to the class that has the data (Information Expert), to protecting the system from third-party change (Protected Variations). Master these and your class designs become self-evidently correct."
Table of Contents
- What Is GRASP & Why It Matters
- Information Expert — Assign to the Knowing Class
- Creator — Who Should Instantiate an Object?
- Controller — Route Use Cases to the Right Layer
- Low Coupling — Minimize Dependencies with Interfaces
- High Cohesion — Keep Classes Focused
- Polymorphism — Replace Conditionals with Types
- Pure Fabrication — Invent Non-Domain Classes Deliberately
- Indirection — Introduce an Intermediary
- Protected Variations — Shield Against Change
- Comprehensive Comparison Table
- Real-World Scenario: E-Commerce Checkout
- Conclusion & Checklist
1. What Is GRASP & Why It Matters
GRASP stands for General Responsibility Assignment Software Patterns. Craig Larman introduced it in Applying UML and Patterns (1997) as a set of nine principles for answering the most fundamental question in OO design: "Which object should be responsible for this behavior?"
Unlike GoF (Gang of Four) patterns that describe structural solutions to recurring problems, GRASP operates at a higher level — it guides the assignment of responsibilities to collaborating objects before you even reach the structural pattern stage. Think of GRASP as the reasoning framework that leads you to choose a Strategy, Factory, or Observer pattern in the first place.
In 2026, with microservices decomposition, domain-driven design, and AI-assisted coding proliferating, GRASP is more relevant than ever. AI code generators can produce syntactically correct code that violates every GRASP principle — producing anemic domain models, bloated service classes, and tightly coupled modules. Senior developers must recognize and correct these violations.
- SOLID — what properties classes and modules should have
- GoF Design Patterns — structural templates for recurring collaborations
- GRASP — how to assign responsibilities when designing those classes
The nine GRASP principles are: Information Expert, Creator, Controller, Low Coupling, High Cohesion, Polymorphism, Pure Fabrication, Indirection, and Protected Variations. Let's master each one with production Java code.
2. Information Expert — Assign to the Knowing Class
Principle: Assign a responsibility to the class that has the information necessary to fulfil it. Don't reach across object boundaries to compute something — let the object that owns the data do the work.
This is the most fundamental GRASP principle and the most commonly violated one in enterprise Java. The classic symptom is an anemic domain model: Order is a bag of fields, and all business logic lives in OrderService.
Anti-Pattern: Logic Outside the Expert
// ❌ Anti-pattern: OrderService computes total — but Order has all the data
@Service
public class OrderService {
public BigDecimal calculateTotal(Order order) {
BigDecimal total = BigDecimal.ZERO;
for (OrderLine line : order.getLines()) {
total = total.add(line.getUnitPrice()
.multiply(BigDecimal.valueOf(line.getQuantity())));
}
return total;
}
}
Applying Information Expert
// ✅ Information Expert: Order knows its own lines — it calculates its total
public class Order {
private List<OrderLine> lines = new ArrayList<>();
private OrderStatus status;
private String customerId;
public BigDecimal calculateTotal() {
return lines.stream()
.map(OrderLine::getLineTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public boolean isEligibleForDiscount() {
return calculateTotal().compareTo(new BigDecimal("100.00")) >= 0;
}
public void addLine(OrderLine line) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot modify a non-draft order");
}
lines.add(line);
}
}
// OrderLine knows its own price and quantity — it computes its own line total
public class OrderLine {
private final Product product;
private final int quantity;
private final BigDecimal unitPrice;
public BigDecimal getLineTotal() {
return unitPrice.multiply(BigDecimal.valueOf(quantity));
}
}
// OrderService now delegates to the domain object
@Service
public class OrderService {
public BigDecimal getOrderTotal(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
return order.calculateTotal(); // Expert computes it
}
}
When you apply Information Expert consistently, your domain objects become rich domain models — they encapsulate both data and the operations that naturally belong to them. This improves testability (you can unit test Order.calculateTotal() in isolation) and reduces duplication (the logic lives in exactly one place).
3. Creator — Who Should Instantiate an Object?
Principle: Assign class B the responsibility of creating instances of class A if one or more of the following are true: B contains or aggregates A; B closely uses A; B has the initializing data for A. In other words, the creator is the class that is most naturally associated with the created object.
Creator in Action: Order Creates OrderItems
// ✅ Order is the creator of OrderLine — it aggregates them
public class Order {
private final List<OrderLine> lines = new ArrayList<>();
// Creator responsibility: Order builds its own lines
public void addProduct(Product product, int quantity) {
if (quantity <= 0) throw new IllegalArgumentException("Quantity must be positive");
// Order creates the OrderLine — it has all the data needed
OrderLine line = new OrderLine(product, quantity, product.getCurrentPrice());
lines.add(line);
}
}
// Invoice is created by Order (Order contains the data needed to create an invoice)
public class Order {
public Invoice generateInvoice(String billingAddress) {
// Order creates Invoice — it has items, totals, customer data
return new Invoice(
UUID.randomUUID().toString(),
this.customerId,
billingAddress,
Collections.unmodifiableList(lines),
calculateTotal(),
LocalDateTime.now()
);
}
}
Creator with Spring Boot: Factory vs Direct Instantiation
// When the created object requires infrastructure (persistence ID, timestamps),
// use a Spring-managed factory — but keep the Creator principle: the factory
// is created because it closely uses the new object and has its init data.
@Component
public class OrderFactory {
private final ProductRepository productRepository;
public OrderFactory(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Order createOrder(String customerId, List<OrderItemRequest> items) {
Order order = new Order(customerId, OrderStatus.DRAFT, LocalDateTime.now());
for (OrderItemRequest req : items) {
Product product = productRepository.findById(req.productId())
.orElseThrow(() -> new ProductNotFoundException(req.productId()));
order.addProduct(product, req.quantity());
}
return order;
}
}
// OrderService delegates creation to the factory — not the other way around
@Service
@Transactional
public class OrderService {
private final OrderFactory orderFactory;
private final OrderRepository orderRepository;
public Order placeOrder(String customerId, List<OrderItemRequest> items) {
Order order = orderFactory.createOrder(customerId, items);
return orderRepository.save(order);
}
}
The Creator principle naturally leads you to aggregates in DDD — the aggregate root is the creator of its child entities. It prevents orphaned objects (an OrderLine without an Order) and ensures invariants are enforced at construction time.
4. Controller — Route Use Cases to the Right Layer
Principle: Assign the responsibility for handling system events (UI events, API requests, messages) to a non-UI class that represents the overall system, a root object, or a use-case handler. The controller is the first object beyond the UI that receives and coordinates a system operation — it should not contain business logic itself.
In Spring Boot, the GRASP Controller maps directly to the MVC @RestController layer — but with a critical constraint: the controller only routes; business logic belongs in the service layer.
Anti-Pattern: Fat Controller
// ❌ Fat Controller: business logic inside the REST layer
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired private OrderRepository orderRepository;
@Autowired private PaymentRepository paymentRepository;
@Autowired private EmailService emailService;
@PostMapping
public ResponseEntity<Order> placeOrder(@RequestBody OrderRequest req) {
// ❌ Direct DB access in controller
Order order = new Order(req.getCustomerId());
req.getItems().forEach(item -> order.addProduct(item.getProductId(), item.getQty()));
orderRepository.save(order);
// ❌ Payment logic in controller
Payment payment = new Payment(order.getId(), req.getPaymentToken());
paymentRepository.save(payment);
// ❌ Email logic in controller
emailService.sendOrderConfirmation(order);
return ResponseEntity.ok(order);
}
}
Applying the Controller Principle
// ✅ Thin Controller: delegates to use-case service
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final PlaceOrderUseCase placeOrderUseCase;
private final GetOrderUseCase getOrderUseCase;
private final CancelOrderUseCase cancelOrderUseCase;
@PostMapping
public ResponseEntity<OrderResponse> placeOrder(
@Valid @RequestBody PlaceOrderRequest request,
@AuthenticationPrincipal UserDetails user) {
OrderResponse response = placeOrderUseCase.execute(request, user.getUsername());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@GetMapping("/{orderId}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable Long orderId) {
return ResponseEntity.ok(getOrderUseCase.execute(orderId));
}
@DeleteMapping("/{orderId}")
public ResponseEntity<Void> cancelOrder(@PathVariable Long orderId) {
cancelOrderUseCase.execute(orderId);
return ResponseEntity.noContent().build();
}
}
// Use case class: handles exactly one system operation
@Service
@Transactional
@RequiredArgsConstructor
public class PlaceOrderUseCase {
private final OrderFactory orderFactory;
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final NotificationService notificationService;
public OrderResponse execute(PlaceOrderRequest request, String customerId) {
Order order = orderFactory.createOrder(customerId, request.getItems());
order = orderRepository.save(order);
paymentService.processPayment(order, request.getPaymentToken());
notificationService.sendOrderConfirmation(order);
return OrderResponse.from(order);
}
}
The Controller principle is why hexagonal architecture (ports & adapters) works so well with Spring Boot: the REST adapter is your GRASP Controller, delegating to application services (use cases) that coordinate domain logic without coupling to HTTP.
5. Low Coupling — Minimize Dependencies with Interfaces
Principle: Assign responsibilities so that coupling remains low. Coupling is a measure of how strongly one element is connected to, has knowledge of, or depends on other elements. Low coupling reduces the cost of change: modifying one class has minimal ripple effects.
In Java, coupling manifests as: direct class references (not interface references), method calls on external concrete classes, using static methods of third-party classes, and field access on other objects. The primary tool for achieving low coupling is dependency inversion via interfaces.
Anti-Pattern: High Coupling to Concrete Classes
// ❌ OrderService directly depends on StripePaymentGateway (concrete class)
@Service
public class OrderService {
private final StripePaymentGateway stripeGateway; // tight coupling!
public void processPayment(Order order, String token) {
StripeChargeRequest req = new StripeChargeRequest(
token, order.calculateTotal(), "usd");
StripeChargeResponse res = stripeGateway.charge(req);
if (!res.isSuccessful()) throw new PaymentFailedException(res.getErrorMessage());
}
}
Applying Low Coupling with Interface Abstraction
// ✅ PaymentGateway interface — OrderService depends on abstraction
public interface PaymentGateway {
PaymentResult charge(PaymentRequest request);
void refund(String transactionId, BigDecimal amount);
}
// Stripe implementation is isolated behind the interface
@Component
@ConditionalOnProperty(name = "payment.provider", havingValue = "stripe")
public class StripePaymentGateway implements PaymentGateway {
private final StripeClient stripeClient;
@Override
public PaymentResult charge(PaymentRequest request) {
// Stripe-specific implementation detail here
StripeChargeRequest stripeReq = StripeChargeRequest.builder()
.amount(request.amount().multiply(new BigDecimal("100")).longValue())
.currency(request.currency())
.source(request.token())
.build();
StripeChargeResponse resp = stripeClient.charges().create(stripeReq);
return new PaymentResult(resp.getId(), resp.getStatus().equals("succeeded"));
}
@Override
public void refund(String transactionId, BigDecimal amount) {
stripeClient.refunds().create(transactionId, amount.longValue());
}
}
// PayPal implementation — drop-in replacement, zero changes to OrderService
@Component
@ConditionalOnProperty(name = "payment.provider", havingValue = "paypal")
public class PayPalPaymentGateway implements PaymentGateway {
@Override
public PaymentResult charge(PaymentRequest request) { /* PayPal SDK calls */ return null; }
@Override
public void refund(String transactionId, BigDecimal amount) { /* PayPal SDK calls */ }
}
// OrderService now depends on the interface only
@Service
@RequiredArgsConstructor
public class OrderService {
private final PaymentGateway paymentGateway; // injected by Spring
public void processPayment(Order order, String token) {
PaymentRequest req = new PaymentRequest(token, order.calculateTotal(), "USD");
PaymentResult result = paymentGateway.charge(req);
if (!result.isSuccessful()) throw new PaymentFailedException("Payment rejected");
order.markAsPaid(result.transactionId());
}
}
Low Coupling is the structural reason behind the Dependency Inversion Principle (the D in SOLID). Spring's IoC container is designed precisely to enforce low coupling — but you still have to design the interfaces correctly. Coupling to JpaRepository directly instead of a custom repository interface is a common Spring Boot coupling trap.
6. High Cohesion — Keep Classes Focused
Principle: Assign responsibilities so that cohesion remains high. Cohesion is a measure of how strongly related and focused the responsibilities of a class are. A class with high cohesion does a small set of related things well. Low cohesion ("God classes") are hard to maintain, test, and understand.
High Cohesion in GRASP is the OO design expression of the Single Responsibility Principle. They differ in focus: SRP asks "does this class have one reason to change?" while GRASP High Cohesion asks "are all the responsibilities of this class closely related?"
Anti-Pattern: Low Cohesion God Service
// ❌ OrderService doing everything — low cohesion
@Service
public class OrderService {
// Order management
public Order createOrder(PlaceOrderRequest req) { /* ... */ return null; }
public void cancelOrder(Long orderId) { /* ... */ }
// Payment responsibilities (unrelated to order management)
public void processPayment(Long orderId, String token) { /* ... */ }
public void processRefund(Long orderId) { /* ... */ }
// Notification responsibilities (unrelated)
public void sendOrderConfirmationEmail(Order order) { /* ... */ }
public void sendShippingUpdateSms(Order order) { /* ... */ }
// Inventory responsibilities (unrelated)
public void reserveStock(Order order) { /* ... */ }
public void releaseStock(Long orderId) { /* ... */ }
}
Applying High Cohesion: Focused Services
// ✅ OrderService: only manages order lifecycle
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final OrderFactory orderFactory;
public Order createOrder(PlaceOrderRequest req, String customerId) {
return orderRepository.save(orderFactory.createOrder(customerId, req.getItems()));
}
public void cancelOrder(Long orderId) {
Order order = findById(orderId);
order.cancel();
orderRepository.save(order);
}
public Order findById(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
}
// ✅ PaymentService: only handles payment concerns
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentGateway paymentGateway;
private final PaymentRepository paymentRepository;
public Payment processPayment(Order order, String paymentToken) {
PaymentResult result = paymentGateway.charge(
new PaymentRequest(paymentToken, order.calculateTotal(), "USD"));
Payment payment = new Payment(order.getId(), result.transactionId(), PaymentStatus.COMPLETED);
return paymentRepository.save(payment);
}
public void refund(Long orderId) {
Payment payment = paymentRepository.findByOrderId(orderId)
.orElseThrow(() -> new PaymentNotFoundException(orderId));
paymentGateway.refund(payment.getTransactionId(), payment.getAmount());
payment.markAsRefunded();
paymentRepository.save(payment);
}
}
// ✅ NotificationService: only handles notifications
@Service
@RequiredArgsConstructor
public class NotificationService {
private final NotificationSender notificationSender;
public void sendOrderConfirmation(Order order) {
notificationSender.send(NotificationMessage.orderConfirmation(order));
}
public void sendShippingUpdate(Order order, String trackingNumber) {
notificationSender.send(NotificationMessage.shippingUpdate(order, trackingNumber));
}
}
// ✅ InventoryService: only manages stock
@Service
@RequiredArgsConstructor
public class InventoryService {
private final InventoryRepository inventoryRepository;
public void reserveStock(Order order) {
order.getLines().forEach(line ->
inventoryRepository.reserve(line.getProduct().getId(), line.getQuantity()));
}
public void releaseStock(Long orderId) {
inventoryRepository.releaseByOrderId(orderId);
}
}
The test for High Cohesion is simple: if you can describe a class's purpose without using "and" or "or", it's probably cohesive. "Manages order lifecycle" ✅. "Manages orders, payments, notifications, and inventory" ❌. High cohesion classes are smaller, have fewer dependencies, and are dramatically easier to unit test in isolation.
7. Polymorphism — Replace Conditionals with Types
Principle: When related alternatives or behaviors vary by type, assign responsibility for the behavior to the types for which the behavior varies, using polymorphic operations. Don't use instanceof checks or type-switching conditionals — let the type system do the work.
This is the GRASP justification for the Strategy, Command, and State GoF patterns. Every time you write if (type.equals("EMAIL")) { ... } else if (type.equals("SMS")) { ... }, you're violating the Polymorphism principle.
Anti-Pattern: Type-Switch Conditionals
// ❌ Type-switching: every new channel requires modifying this class (OCP violation)
@Service
public class NotificationService {
public void send(Order order, String channel) {
if ("EMAIL".equals(channel)) {
emailClient.send(order.getCustomerEmail(), buildEmailBody(order));
} else if ("SMS".equals(channel)) {
smsClient.sendSms(order.getCustomerPhone(), buildSmsText(order));
} else if ("PUSH".equals(channel)) {
pushClient.push(order.getCustomerDeviceToken(), buildPushPayload(order));
} else {
throw new UnsupportedOperationException("Unknown channel: " + channel);
}
}
}
Applying Polymorphism: Strategy Pattern
// ✅ NotificationSender interface — behavior varies by type
public interface NotificationSender {
void send(NotificationMessage message);
NotificationChannel channel();
}
// Each type encapsulates its own sending behavior
@Component
public class EmailNotificationSender implements NotificationSender {
private final JavaMailSender mailSender;
@Override
public void send(NotificationMessage message) {
SimpleMailMessage mail = new SimpleMailMessage();
mail.setTo(message.getRecipientEmail());
mail.setSubject(message.getSubject());
mail.setText(message.getBody());
mailSender.send(mail);
}
@Override
public NotificationChannel channel() { return NotificationChannel.EMAIL; }
}
@Component
public class SmsNotificationSender implements NotificationSender {
private final TwilioClient twilioClient;
@Override
public void send(NotificationMessage message) {
twilioClient.messages().create(
message.getRecipientPhone(),
TwilioFrom.of("+15551234567"),
message.getBody());
}
@Override
public NotificationChannel channel() { return NotificationChannel.SMS; }
}
@Component
public class PushNotificationSender implements NotificationSender {
private final FirebaseMessaging firebase;
@Override
public void send(NotificationMessage message) {
Message pushMsg = Message.builder()
.setToken(message.getDeviceToken())
.setNotification(Notification.builder()
.setTitle(message.getSubject())
.setBody(message.getBody())
.build())
.build();
firebase.send(pushMsg);
}
@Override
public NotificationChannel channel() { return NotificationChannel.PUSH; }
}
// Service uses the abstraction — adding a new channel requires no changes here
@Service
@RequiredArgsConstructor
public class NotificationService {
private final List<NotificationSender> senders; // Spring injects all implementations
private NotificationSender getSender(NotificationChannel channel) {
return senders.stream()
.filter(s -> s.channel() == channel)
.findFirst()
.orElseThrow(() -> new UnsupportedChannelException(channel));
}
public void sendOrderConfirmation(Order order) {
NotificationMessage msg = NotificationMessage.orderConfirmation(order);
order.getCustomer().getPreferredChannels().forEach(channel ->
getSender(channel).send(msg));
}
}
Adding a new notification channel (e.g., WhatsApp) now requires zero changes to existing code — just add a new @Component implementing NotificationSender. This is the Open/Closed Principle (OCP) in action, justified by the GRASP Polymorphism principle.
8. Pure Fabrication — Invent Non-Domain Classes Deliberately
Principle: When no domain class is a natural fit for a responsibility, fabricate a class that does not represent a problem-domain concept — chosen to achieve high cohesion, low coupling, and reuse. Repository, Service, Gateway, Mapper, and Formatter classes are all Pure Fabrications.
Pure Fabrication solves the tension between Information Expert (assign to the class with the data) and High Cohesion (don't overload domain classes). When assigning a responsibility to a domain class would lower its cohesion, fabricate a new class instead.
Examples of Pure Fabrications in Spring Boot
// Pure Fabrication #1: OrderRepository — doesn't represent a domain concept;
// exists purely to manage persistence concerns with high cohesion
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByCustomerIdAndStatus(String customerId, OrderStatus status);
Optional<Order> findByIdAndCustomerId(Long id, String customerId);
@Query("SELECT o FROM Order o WHERE o.createdAt BETWEEN :start AND :end")
List<Order> findOrdersInDateRange(
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
}
// Pure Fabrication #2: OrderMapper — no domain concept, pure transformation logic
@Component
public class OrderMapper {
public OrderResponse toResponse(Order order) {
return new OrderResponse(
order.getId(),
order.getCustomerId(),
order.getStatus().name(),
order.getLines().stream().map(this::toLineResponse).toList(),
order.calculateTotal(),
order.getCreatedAt()
);
}
private OrderLineResponse toLineResponse(OrderLine line) {
return new OrderLineResponse(
line.getProduct().getId(),
line.getProduct().getName(),
line.getQuantity(),
line.getUnitPrice(),
line.getLineTotal()
);
}
}
// Pure Fabrication #3: EmailTemplateRenderer — no domain equivalent
@Component
public class EmailTemplateRenderer {
private final TemplateEngine templateEngine; // Thymeleaf
public String renderOrderConfirmation(Order order, Customer customer) {
Context ctx = new Context();
ctx.setVariable("order", order);
ctx.setVariable("customer", customer);
ctx.setVariable("total", order.calculateTotal());
return templateEngine.process("email/order-confirmation", ctx);
}
}
// Pure Fabrication #4: AuditLogger — cross-cutting concern, not a domain entity
@Component
@Slf4j
public class AuditLogger {
private final AuditEventRepository auditEventRepository;
public void logOrderEvent(String eventType, Long orderId, String userId) {
AuditEvent event = new AuditEvent(
eventType, "Order", orderId.toString(), userId, Instant.now());
auditEventRepository.save(event);
log.info("AUDIT: {} on Order#{} by {}", eventType, orderId, userId);
}
}
Pure Fabrications are the backbone of the service layer in any layered architecture. The key insight: they are intentional design decisions, not accidental additions. When you fabricate a class, you should be able to justify exactly why no domain class could hold this responsibility without lowering cohesion.
9. Indirection — Introduce an Intermediary
Principle: Assign responsibility to an intermediate object to mediate between two components or services, to avoid direct coupling between them. The intermediary decouples components so they can evolve independently.
Indirection is the GRASP justification for message brokers, event buses, API gateways, and service meshes. It is also the principle behind Façade, Mediator, and Adapter GoF patterns. The famous David Wheeler quip applies: "All problems in computer science can be solved by another level of indirection."
Applying Indirection: Event Publisher as Intermediary
// Without Indirection: OrderService directly calls 4 downstream services
@Service
public class OrderService {
private final PaymentService paymentService; // direct dependency
private final InventoryService inventoryService; // direct dependency
private final NotificationService notificationService; // direct dependency
private final LoyaltyService loyaltyService; // direct dependency
public void placeOrder(Order order) {
orderRepository.save(order);
paymentService.process(order); // direct call
inventoryService.reserve(order); // direct call
notificationService.notify(order); // direct call
loyaltyService.awardPoints(order); // direct call
}
}
// ✅ With Indirection: OrderService publishes an event; intermediary routes it
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher; // Spring's event bus (intermediary)
public Order placeOrder(PlaceOrderRequest req, String customerId) {
Order order = orderFactory.createOrder(customerId, req.getItems());
order = orderRepository.save(order);
// Publish event — OrderService knows nothing about who handles it
eventPublisher.publishEvent(new OrderPlacedEvent(order.getId(), customerId));
return order;
}
}
// Each downstream service is decoupled — listens independently
@Component
@RequiredArgsConstructor
public class PaymentEventHandler {
private final PaymentService paymentService;
@EventListener
@Async
public void onOrderPlaced(OrderPlacedEvent event) {
paymentService.initiatePaymentFor(event.orderId());
}
}
@Component
@RequiredArgsConstructor
public class InventoryEventHandler {
private final InventoryService inventoryService;
@EventListener
@Async
public void onOrderPlaced(OrderPlacedEvent event) {
inventoryService.reserveStockFor(event.orderId());
}
}
@Component
@RequiredArgsConstructor
public class NotificationEventHandler {
private final NotificationService notificationService;
@EventListener
@Async
public void onOrderPlaced(OrderPlacedEvent event) {
notificationService.sendOrderConfirmation(event.orderId());
}
}
Indirection with Kafka for Cross-Service Decoupling
// MessageBroker as intermediary between microservices
@Component
@RequiredArgsConstructor
public class OrderEventPublisher {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
public void publishOrderPlaced(Order order) {
OrderPlacedEvent event = new OrderPlacedEvent(
order.getId(), order.getCustomerId(),
order.getLines().stream().map(OrderLineEvent::from).toList(),
order.calculateTotal(), Instant.now());
kafkaTemplate.send("order.placed", order.getId().toString(), event);
}
}
// Payment microservice consumes — completely decoupled from Order service
@KafkaListener(topics = "order.placed", groupId = "payment-service")
public void handleOrderPlaced(OrderPlacedEvent event) {
paymentService.initiatePayment(event.orderId(), event.total());
}
The Indirection principle is the reason event-driven microservices architectures are so resilient — the message broker is the intermediary that allows producer and consumer services to evolve, scale, and fail independently. Indirection does add complexity (eventual consistency, dead letter queues, idempotency) — only apply it when the decoupling benefit outweighs the operational cost.
10. Protected Variations — Shield Against Change
Principle: Identify points of predicted variation or instability. Assign responsibilities to create a stable interface around them. This is arguably the most powerful GRASP principle — it directly addresses the root cause of software fragility: change ripple-through.
Protected Variations is the GRASP articulation of the Open/Closed Principle. It says: wrap everything that is likely to change in a stable abstraction so that the rest of the system is shielded from that change. The three most common variation points in enterprise Java are: third-party integrations, data storage mechanisms, and external APIs.
Protected Variations: Payment Gateway Isolation
// Variation point: payment provider (Stripe today, PayPal tomorrow, Adyen next year)
// Protected by a stable interface — the application core never changes provider-specific code
// ✅ Stable interface shields the core from provider variation
public interface PaymentGateway {
PaymentResult charge(PaymentRequest request);
PaymentResult authorize(PaymentRequest request);
void capture(String authorizationId);
void refund(String transactionId, BigDecimal amount);
PaymentStatus getStatus(String transactionId);
}
// Provider-specific adapters are isolated in their own packages
// Switching from Stripe to Adyen: create AdyenPaymentGateway, update config — done
@Component
@ConditionalOnProperty(name = "payment.provider", havingValue = "stripe", matchIfMissing = true)
public class StripePaymentGateway implements PaymentGateway {
private static final String STRIPE_API_VERSION = "2024-04-10";
private final StripeClient stripeClient;
@Override
public PaymentResult charge(PaymentRequest request) {
try {
PaymentIntentCreateParams params = PaymentIntentCreateParams.builder()
.setAmount(request.amountInCents())
.setCurrency(request.currency().toLowerCase())
.setPaymentMethod(request.token())
.setConfirm(true)
.build();
PaymentIntent intent = stripeClient.paymentIntents().create(params);
return PaymentResult.success(intent.getId());
} catch (StripeException e) {
return PaymentResult.failure(e.getMessage());
}
}
// other methods...
}
// ✅ Protected Variations for data storage: repository abstraction
// Variation point: storage engine (PostgreSQL today, MongoDB if needed, in-memory for tests)
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(Long id);
List<Order> findByCustomerId(String customerId);
void delete(Long id);
}
@Repository
public class JpaOrderRepository implements OrderRepository {
private final SpringDataOrderRepository springRepo;
@Override
public Order save(Order order) { return springRepo.save(order); }
@Override
public Optional<Order> findById(Long id) { return springRepo.findById(id); }
@Override
public List<Order> findByCustomerId(String customerId) {
return springRepo.findByCustomerId(customerId);
}
@Override
public void delete(Long id) { springRepo.deleteById(id); }
}
// In-memory implementation for tests — no DB needed
public class InMemoryOrderRepository implements OrderRepository {
private final Map<Long, Order> store = new ConcurrentHashMap<>();
private final AtomicLong idGen = new AtomicLong(1);
@Override
public Order save(Order order) {
if (order.getId() == null) {
order.setId(idGen.getAndIncrement());
}
store.put(order.getId(), order);
return order;
}
@Override
public Optional<Order> findById(Long id) { return Optional.ofNullable(store.get(id)); }
@Override
public List<Order> findByCustomerId(String customerId) {
return store.values().stream()
.filter(o -> o.getCustomerId().equals(customerId))
.toList();
}
@Override
public void delete(Long id) { store.remove(id); }
}
Protected Variations is the principle that makes hexagonal architecture (ports & adapters) so powerful: ports are the stable interfaces protecting the application core, and adapters are the variation points. The application core is completely shielded from changes in frameworks, databases, UI layers, and third-party services.
11. Comprehensive GRASP Comparison Table
The following table summarizes all 9 GRASP principles, their intent, the primary problem they solve, related GoF patterns, and how they manifest in a Spring Boot e-commerce system.
| Principle | Core Question | Solves | Related GoF | Spring Boot Example |
|---|---|---|---|---|
| Information Expert | Who has the data? | Anemic models, duplicated logic | — | Order.calculateTotal() |
| Creator | Who creates this object? | Orphaned objects, scattered creation | Factory Method, Abstract Factory | Order.addProduct(), OrderFactory |
| Controller | Who handles the system event? | Fat controllers, mixed concerns | Command, Facade | OrderController → PlaceOrderUseCase |
| Low Coupling | How to minimize dependencies? | Ripple-change, hard to test | Adapter, Bridge | PaymentGateway interface |
| High Cohesion | Are responsibilities related? | God classes, bloated services | — | Separate OrderService, PaymentService |
| Polymorphism | Who handles type-variant behavior? | If/else type conditionals, OCP violations | Strategy, State, Command | NotificationSender implementations |
| Pure Fabrication | No domain class fits — what now? | Domain class overloading, mixed concerns | Service, Repository patterns | OrderRepository, OrderMapper |
| Indirection | How to decouple collaborators? | Direct service coupling, fan-out complexity | Mediator, Facade, Observer | ApplicationEventPublisher, Kafka |
| Protected Variations | What will change? Shield it. | Fragility from third-party changes | Adapter, Proxy, Decorator | OrderRepository port, PaymentGateway port |
12. Real-World Scenario: E-Commerce Checkout
Let's trace a complete e-commerce checkout flow and see how every GRASP principle applies in concert. The flow: customer adds items to cart → reviews order → enters payment → order is confirmed → notifications sent → inventory reserved.
Checkout Flow — GRASP Mapping
- Information Expert:
Cart.calculateSubtotal(),Order.calculateTotal(),OrderLine.getLineTotal()— each computes from its own data - Creator:
CartcreatesCartItem;Orderis created byOrderFactory(which has all the initializing data) - Controller:
CheckoutControllerreceives the HTTP POST and delegates toCheckoutUseCase— no business logic in the controller - Low Coupling:
CheckoutUseCasedepends onPaymentGatewayandOrderRepositoryinterfaces, not concrete classes - High Cohesion:
OrderServicemanages orders,PaymentServicehandles payments,InventoryServicemanages stock — each is focused - Polymorphism:
NotificationSenderwith email, SMS, and push implementations — customer preferences determine which runs - Pure Fabrication:
OrderRepository,OrderMapper,AuditLogger— non-domain classes invented for persistence, mapping, and auditing - Indirection:
ApplicationEventPublisherroutesOrderPlacedEventtoPaymentEventHandler,InventoryEventHandler,NotificationEventHandler - Protected Variations:
PaymentGatewayinterface isolates Stripe;OrderRepositoryinterface isolates JPA — swapping either requires zero changes to business logic
// Complete checkout orchestration applying all GRASP principles
@Service
@Transactional
@RequiredArgsConstructor
public class CheckoutUseCase {
// Low Coupling: depends on interfaces, not concretions
private final CartRepository cartRepository; // Pure Fabrication + Protected Variations
private final OrderFactory orderFactory; // Creator
private final OrderRepository orderRepository; // Pure Fabrication + Protected Variations
private final PaymentGateway paymentGateway; // Low Coupling + Protected Variations
private final ApplicationEventPublisher eventPublisher; // Indirection
public CheckoutResponse execute(CheckoutRequest request, String customerId) {
// Information Expert: Cart knows its own contents
Cart cart = cartRepository.findActiveCart(customerId)
.orElseThrow(() -> new EmptyCartException(customerId));
// Creator: OrderFactory creates the Order with all initializing data
Order order = orderFactory.createFromCart(cart, request.getShippingAddress());
// Information Expert: Order calculates its own total
BigDecimal total = order.calculateTotal();
// Protected Variations: PaymentGateway interface shields from provider changes
PaymentResult payment = paymentGateway.charge(
new PaymentRequest(request.getPaymentToken(), total, "USD"));
if (!payment.isSuccessful()) {
throw new PaymentFailedException(payment.errorMessage());
}
order.markAsPaid(payment.transactionId());
Order saved = orderRepository.save(order);
// Indirection: Event publisher routes to handlers — CheckoutUseCase
// knows nothing about inventory reservation, notifications, loyalty points, etc.
eventPublisher.publishEvent(new OrderPlacedEvent(saved.getId(), customerId, total));
cartRepository.clearCart(customerId);
// Pure Fabrication: OrderMapper translates domain to DTO
return CheckoutResponse.success(saved.getId(), total, payment.transactionId());
}
}
Notice how each GRASP principle plays a role: Information Expert keeps calculation in domain objects, Creator keeps instantiation coherent, Controller keeps the REST layer thin, Low Coupling + Protected Variations make the core independent of infrastructure, High Cohesion keeps each service focused, Polymorphism makes notification channels extensible, Pure Fabrication provides infrastructure seams, and Indirection decouples downstream reactions.
13. Conclusion & Application Checklist
GRASP is not a cookbook of structures like GoF — it's a set of reasoning heuristics you apply every time you ask "which class should own this?" Mastering GRASP transforms your design process from intuition-based to principle-based, producing systems that are consistently more testable, maintainable, and evolvable.
In practice, the GRASP principles work together and often reinforce each other: Information Expert and Creator guide initial responsibility assignment; Low Coupling and High Cohesion evaluate whether assignments are correct; Polymorphism and Protected Variations handle variation points; Pure Fabrication and Indirection solve the cases where domain classes are not the right home for a responsibility.
GRASP Design Review Checklist
- ☐ Information Expert: Does each class compute from its own data? Are any services computing things that belong in domain objects?
- ☐ Creator: Is each object created by a class that contains or closely uses it? Are there orphaned object creation calls scattered across the codebase?
- ☐ Controller: Does the controller layer route only? Is any business logic in
@RestControllerclasses? - ☐ Low Coupling: Do classes depend on interfaces rather than concrete classes? Can you mock every dependency in unit tests?
- ☐ High Cohesion: Can you describe each class's purpose without "and" or "or"? Are there any God classes with 15+ methods across unrelated concerns?
- ☐ Polymorphism: Are there
instanceofchecks or channel/typeif/elsechains that could be replaced with interface implementations? - ☐ Pure Fabrication: Have you deliberately invented service/repository/mapper classes rather than putting everything in domain objects?
- ☐ Indirection: Are direct service-to-service call chains creating coupling? Could an event bus or message broker decouple them?
- ☐ Protected Variations: Have you identified all third-party libraries/APIs that may change and wrapped them in stable interfaces?
Senior developers distinguish themselves by applying these principles consistently — not just in greenfield systems, but when refactoring legacy code, reviewing PRs, and guiding junior team members. Every code review is an opportunity to ask: "Is the responsibility assigned to the right class? Could this be more cohesive? Less coupled? More polymorphic?" GRASP gives you the vocabulary and framework to answer those questions with precision.