Responsibilities drift
Logic accumulates wherever a developer had access first, rather than where it belongs conceptually.
In many Java codebases, the problem is not that classes exist. The problem is that responsibilities are misplaced. Controllers become coordinators, services become God objects, entities become empty shells, repositories absorb business rules, and architecture ends up compensating for unclear object design rather than building on top of it.
OOAD exists because business systems keep changing. Discount rules evolve. Payment methods expand. Shipment logic becomes asynchronous. Notifications fan out across channels. If responsibilities were never shaped clearly, every change leaks into five places. That is where design becomes a business issue, not just a code issue.
Logic accumulates wherever a developer had access first, rather than where it belongs conceptually.
A small business rule update touches controllers, services, persistence code, and integration code at once.
Objects stop expressing business concepts and become passive data containers with procedural logic scattered elsewhere.
Teams add layers, patterns, or frameworks without solving the underlying modelling problem.
Consider a typical e-commerce checkout path. A customer places an order, payment is authorized, stock is reserved, shipment is created, and a confirmation is sent. Without OOAD, this often collapses into a single application service that knows too much.
public class OrderService {
private final PaymentGateway paymentGateway;
private final InventoryClient inventoryClient;
private final OrderRepository orderRepository;
private final ShipmentService shipmentService;
private final NotificationService notificationService;
public OrderConfirmation placeOrder(CheckoutRequest request) {
BigDecimal total = request.items().stream()
.map(item -> item.unitPrice().multiply(BigDecimal.valueOf(item.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (total.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Order total must be positive");
}
if (!inventoryClient.reserve(request.items())) {
throw new IllegalStateException("Inventory not available");
}
PaymentResult payment = paymentGateway.authorize(request.paymentMethod(), total);
if (!payment.approved()) {
inventoryClient.release(request.items());
throw new IllegalStateException("Payment failed");
}
OrderEntity order = new OrderEntity();
order.setCustomerId(request.customerId());
order.setTotal(total);
order.setStatus("PAID");
orderRepository.save(order);
shipmentService.createShipment(order.getId(), request.shippingAddress());
notificationService.sendConfirmation(order.getId(), request.customerEmail());
return new OrderConfirmation(order.getId(), "PAID");
}
}
This code may work, but it mixes pricing, validation, inventory coordination, payment handling, persistence,
shipment orchestration, and notification concerns in one place. The domain concept of Order is
barely visible.
public final class Order {
private final CustomerId customerId;
private final List<OrderLine> lines;
private OrderStatus status;
public Order(CustomerId customerId, List<OrderLine> lines) {
this.customerId = customerId;
this.lines = List.copyOf(lines);
this.status = OrderStatus.DRAFT;
}
public Money total() {
return lines.stream()
.map(OrderLine::lineTotal)
.reduce(Money.zero("USD"), Money::add);
}
public void markPaid() {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Only draft orders can be marked paid");
}
status = OrderStatus.PAID;
}
}
public final class PlaceOrderUseCase {
private final InventoryReservationPolicy inventoryReservationPolicy;
private final PaymentAuthorizer paymentAuthorizer;
private final OrderRepository orderRepository;
private final ShipmentCoordinator shipmentCoordinator;
private final NotificationPublisher notificationPublisher;
public OrderId handle(PlaceOrderCommand command) {
Order order = OrderFactory.from(command);
inventoryReservationPolicy.reserve(order);
paymentAuthorizer.authorize(order);
order.markPaid();
orderRepository.save(order);
shipmentCoordinator.startFor(order);
notificationPublisher.publishOrderConfirmed(order);
return order.id();
}
}
The improvement is not “more classes” by itself. The improvement is more meaningful responsibility boundaries. The domain object owns domain rules. The use case orchestrates a business flow. Policies and collaborators each have one clear role.
Order type now communicates business meaning directly instead of acting as a passive record.OOAD does not mean every CRUD screen needs a rich domain model. If a part of the system is simple, stable, and mostly data transport, a lighter design can be enough. The goal is proportional design, not ceremony.