Java Omnibus
OOAD & Architecture / Design principles / Package Principles: Cohesion and Coupling
Design Principles Java Order Management
Design Principles

Good package design turns class-level cleanups into real architectural boundaries.

Many systems improve individual classes while leaving the package structure muddled. Package-level design matters because it shapes ownership, deployment, dependency direction, and how people mentally navigate the codebase. Class quality alone cannot rescue weak package boundaries.

Why This Topic Matters

Why it matters in real Java systems

In an order platform, package boundaries often reveal whether the system is organized around domain concepts, technical layers, or a mix of both. Poor package cohesion leads to change scattering. Poor package coupling leads to ripple effects across modules and teams.

Core Explanation

The design idea behind this topic

Package principles scale cohesion and coupling up one level. A class can be clean while the package still mixes domain, infrastructure, API, and persistence details in ways that make the system hard to evolve.

In Java, package structure is part of the design language. It signals ownership and dependency direction. OOAD becomes more durable when package boundaries reflect domain responsibilities and architectural intent.

The package test is similar to the class test: what kinds of changes should need to happen here together, and which changes should not?

Concept Breakdown

Key ideas to hold onto

Concept

Package cohesion

Elements in a package should belong together from a change and responsibility standpoint.

Concept

Package coupling

Dependencies between packages should be limited, stable, and easy to reason about.

Concept

Architectural consequence

Package design often determines whether modules are teachable, testable, and refactorable.

Java Example

Java example in the Order Management domain

Java example: package-oriented separation by responsibility
package org.javaomnibus.ecommerce.ordering;

public final class PlaceOrderUseCase {
    private final PaymentPort paymentPort;
    private final InventoryPort inventoryPort;

    public PlaceOrderUseCase(PaymentPort paymentPort, InventoryPort inventoryPort) {
        this.paymentPort = paymentPort;
        this.inventoryPort = inventoryPort;
    }

    public void handle(Order order) {
        inventoryPort.reserve(order);
        paymentPort.authorize(order);
    }
}

interface PaymentPort { void authorize(Order order); }
interface InventoryPort { void reserve(Order order); }
record Order(String orderId) {}
Code Walkthrough

Step by step

  1. The example is simple, but the important point is that ordering owns the use case and the ports it depends on.
  2. Infrastructure adapters can live in different packages without owning the ordering policy.
  3. That reduces package-level coupling and makes dependency direction clearer.
  4. Package structure now supports the same responsibility logic seen at the class level.
Real-World Usage

Where this shows up in practice

  • Modular monoliths where package boundaries signal domain ownership
  • Spring applications moving toward ports-and-adapters or clearer service boundaries
  • Large teams that need lower coordination cost between domains
Trade-Offs

When not to over-apply it

Over-optimizing package structure too early can create ceremony and churn. Good boundaries matter, but they should reflect real ownership and change patterns, not just an idealized diagram.

Common Mistakes

Frequent mistakes to watch for

  • Mixing controllers, repositories, domain objects, and integration clients in one package
  • Creating layers so broad that every change crosses package boundaries anyway
  • Using package names that describe technology but not responsibility
  • Ignoring dependency direction at the package level while only reviewing classes
Related Pages

Related concepts


What To Read Next