Java Omnibus
OOAD & Architecture / What problem does OOAD solve?
Problem First

OOAD solves the problem of software that works today but becomes harder to understand, change, and trust tomorrow.

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.

Why This Topic Matters

Bad design raises the cost of every future change

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.

In practical terms: OOAD helps you answer who should know what, who should do what, and where a rule belongs before a system grows too large to refactor casually.
Core Explanation

OOAD gives structure to four recurring design problems

Problem 1

Responsibilities drift

Logic accumulates wherever a developer had access first, rather than where it belongs conceptually.

Problem 2

Changes ripple unpredictably

A small business rule update touches controllers, services, persistence code, and integration code at once.

Problem 3

Domain meaning gets lost

Objects stop expressing business concepts and become passive data containers with procedural logic scattered elsewhere.

Problem 4

Architecture compensates for weak design

Teams add layers, patterns, or frameworks without solving the underlying modelling problem.

Order Management Example

Before OOAD: everything flows through one service

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.

Java example: responsibility overload
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.

A Better Direction

After OOAD: responsibilities move closer to the concepts they belong to

Java example: clearer boundaries
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.

Step-by-Step Walkthrough

Why the second design is easier to evolve

  1. The Order type now communicates business meaning directly instead of acting as a passive record.
  2. The use case class coordinates collaborators without becoming the only place that knows every rule.
  3. Policies such as reservation and payment can evolve independently when business rules change.
  4. Persisting the order is now one concern among several, not the center of the design.
  5. Future architecture choices such as domain events or sagas have clearer seams to build on.
Real-World Usage

Where OOAD helps in everyday Java work

  • Refactoring a Spring Boot service layer that has become procedural and bloated
  • Designing domain boundaries before introducing DDD or microservices
  • Clarifying package structure and ownership in a modular monolith
  • Improving testability by separating orchestration, policy, and domain state changes
  • Reducing accidental coupling between persistence models and business decisions
Trade-Offs

When not to over-apply OOAD

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.

Common Mistakes

Frequent misunderstandings

  • Confusing OOAD with class proliferation instead of responsibility clarity
  • Adding patterns before understanding the problem shape
  • Keeping domain models anemic while hoping architecture layers will compensate
  • Treating OOAD as a diagram exercise instead of a design decision process
Related Pages

Related concepts and what to read next


Next Recommended Pages