| Lesson 10 |
Packages |
| Objective |
Define the purpose and function of packages. |
Packages: Organizational Tool for Managing Complexity
Packages serve fundamentally different purpose than other UML constructs—where diagrams model system structure or behavior, packages organize modeling artifacts themselves into manageable hierarchies. This meta-modeling capability addresses complexity management: large systems generate hundreds of classes, dozens of use cases, numerous components requiring organizational structure preventing cognitive overload. Understanding packages' purpose and function requires recognizing their dual role: logical grouping mechanism during modeling (organizing related elements together) and physical mapping to implementation structures (corresponding to directory hierarchies, namespaces, modules). Contemporary software development evolved package concepts beyond UML's original scope—Java modules (JPMS), C++20 modules, Python packages, npm packages, Go modules all provide language-level packaging with explicit dependencies, encapsulation boundaries, and versioning—demonstrating that package organization principles UML formalized became first-class programming language features. Modern practice uses package diagrams selectively where architectural layering visualization serves needs—documenting bounded contexts in Domain-Driven Design, showing module dependencies in monorepo projects, demonstrating architectural conformance to layered patterns—while relying on build tools and dependency analyzers for authoritative package structure rather than manually maintained UML diagrams.
Purpose of Packages
The fundamental purpose of packages addresses the organizational complexity problem: how do we manage thousands of modeling elements without overwhelming teams? A banking system might contain 500+ classes, 100+ use cases, dozens of components. Viewing all elements simultaneously proves impossible—human cognitive capacity limits comprehension. Packages partition large models into comprehensible subsystems. Customer Management package groups customer-related classes (Customer, Account, Address), use cases (CreateCustomer, UpdateProfile), and components (customer-service). Transaction Processing package contains transaction-related elements. Reporting package handles analytics. This hierarchical organization enables developers to focus on relevant subsystem without mental burden of entire system simultaneously present.
Packages facilitate team organization and ownership. Large development teams divide work by assigning package ownership—Customer Management team owns everything in customer package, Payment team owns payment package, Reporting team owns analytics package. Package boundaries establish clear ownership, reducing coordination overhead compared to free-for-all where any developer modifies any class. Changes within package don't require cross-team coordination; changes to package interfaces trigger collaboration. This organizational alignment—package structure mirrors team structure—follows Conway's Law observation that system architecture reflects organizational communication structure. Microservices architecture essentially implements package organization at deployment level, making package boundaries service boundaries.
Packages provide encapsulation and information hiding at architectural level. Package visibility controls specify which elements external packages can access. Public elements (exported from package) define package interface—the contract external packages depend on. Private elements (package-internal) implement package functionality without external visibility. This encapsulation enables internal refactoring without affecting clients—changing private class implementation doesn't impact external packages accessing only public interfaces. Language-level module systems (Java JPMS, C++20 modules) formalize this concept, requiring explicit `exports` declarations making package encapsulation enforceable by compilers rather than merely documentation convention.
Function: What Package Diagrams Model
Package diagrams represent hierarchical organization through nesting. Top-level packages represent major subsystems: `com.company.ecommerce.customer`, `com.company.ecommerce.order`, `com.company.ecommerce.payment`. Nested packages provide finer granularity: `com.company.ecommerce.customer.profile`, `com.company.ecommerce.customer.authentication`. This hierarchical naming reflects organizational structure: broad categories subdivide into specialized concerns. The nesting depth balances comprehensibility (too flat creates overwhelming number of packages) with navigation complexity (too deep requires traversing many levels). Most systems settle on 2-4 nesting levels providing adequate organization without excessive hierarchy.
Package diagrams' critical function models package dependencies—which packages depend on which others. Dashed arrows show dependency direction: if Package A uses classes from Package B, arrow points from A to B. These dependencies reveal architectural structure. Layered architecture shows dependencies flowing unidirectionally: Presentation → Business → Data Access, never reverse. Hexagonal architecture shows application core with no outbound dependencies, adapters depending inward. Circular dependencies (A depends on B, B depends on A, directly or transitively) indicate architectural problems—packages cannot compile independently, preventing modular development. Package diagrams make these architectural patterns and violations visible, enabling architectural conformance discussions.
// Package Diagram: Layered Architecture
┌─────────────────────────────────────┐
│ Presentation Layer │
│ - Controllers │
│ - View Models │
│ - API Endpoints │
└────────────┬────────────────────────┘
│ depends on
▼
┌─────────────────────────────────────┐
│ Business Layer │
│ - Domain Entities │
│ - Business Rules │
│ - Use Case Handlers │
└────────────┬────────────────────────┘
│ depends on
▼
┌─────────────────────────────────────┐
│ Data Access Layer │
│ - Repositories │
│ - Database Entities │
│ - Query Builders │
└─────────────────────────────────────┘
// Package dependencies enforce:
// - Unidirectional dependency flow (top → bottom)
// - Presentation cannot access Data Access directly
// - Business layer has no UI dependencies
// - Circular dependencies prohibited
Import relationships specify which elements one package uses from another. UML distinguishes «import» (accessing specific elements) from «access» (using elements without importing into namespace). Modern languages implement imports differently: Java `import` statements, Python `import`/`from`, C++ `#include`, JavaScript `import`, Go `import`. Package diagrams showing import relationships document intended dependencies, though actual implementation dependencies extracted from source code provide authoritative truth. Automated dependency analysis tools (jdepend, dependency-cruiser, go mod graph) generate package dependency graphs from code, ensuring accuracy unattainable with manually maintained diagrams.
Packages vs Components vs Modules
The terms "package," "component," and "module" create persistent confusion due to overlapping usage across contexts. UML packages organize modeling elements logically. UML components represent deployable software units. Language modules (Java JPMS modules, C++20 modules, Python modules, npm packages) provide language-level encapsulation with dependency management. These concepts relate but aren't equivalent. A Java package (`com.company.customer`) provides namespace organization. A Java module (JPMS `module-info.java`) declares dependencies and exports. A deployable component (JAR file) might contain multiple packages within single module. Understanding these distinctions prevents confusion when package diagrams show logical organization but deployment requires component perspective.
Java's evolution illustrates package concept maturation. Java 1.0 (1996) introduced packages for namespace organization—logical grouping only. JARs provided physical packaging but without explicit dependency management (classpath hell). Maven/Gradle added dependency declaration via `pom.xml`/`build.gradle`. Java 9 (2017) introduced JPMS modules with `module-info.java` declaring dependencies and exports, finally providing language-level package encapsulation UML package diagrams conceptually modeled. This evolution demonstrates package concepts' value—organizing elements hierarchically, controlling visibility, declaring dependencies—leading to language-level features formalizing what UML documented informally.
Package Mapping to Physical Structures
Packages typically map to directory hierarchies in source control. Java enforces this correspondence: `com.company.ecommerce.customer` package must reside in `com/company/ecommerce/customer/` directory structure. Python packages map to directories containing `__init__.py`. C++ namespaces don't mandate directory structure but convention aligns namespace organization with directory layout. This physical correspondence makes packages navigation aid—finding `OrderService` class means navigating to order package directory. Build systems leverage this structure, compiling packages in dependency order, enabling parallel compilation of independent packages.
Monorepo projects (single repository containing multiple projects/services) rely heavily on package organization. Google's monorepo contains thousands of packages representing services, libraries, and tools. Package dependencies dictate build order and change impact. Modifying low-level utility package requires rebuilding all dependent packages. Package boundaries enable selective testing—changes within package trigger package tests; interface changes trigger dependent package tests. Tools like Bazel and Nx leverage package structure for incremental builds and affected tests, making package organization not merely documentation but build system foundation.
Domain-Driven Design and Bounded Contexts
Domain-Driven Design provides strategic package organization guidance through bounded contexts. Each bounded context—autonomous domain model with ubiquitous language—maps naturally to package. E-commerce system might have Catalog context (product browsing), Cart context (shopping cart), Order context (order processing), Payment context (payment handling). These contexts become top-level packages: `com.company.ecommerce.catalog`, `com.company.ecommerce.cart`, `com.company.ecommerce.order`, `com.company.ecommerce.payment`. Each package contains complete domain model for its context—entities, value objects, repositories, domain services. Context mapping patterns (Customer-Supplier, Conformist, Anti-Corruption Layer) translate into package dependencies with documented integration patterns.
Package diagrams visualizing bounded contexts serve strategic architecture communication. Showing Order context depends on Catalog context (needs product information), Cart context depends on Catalog (displays products), Order context depends on Payment context (processes payments) reveals system architecture at business domain level rather than technical infrastructure level. This domain-aligned package structure enables domain expert conversations—"Order processing needs product catalog integration" translates directly to Order package depending on Catalog package. Technical teams implement strategic design through package dependencies architecture enforces.
// Package Organization: Domain-Driven Design Bounded Contexts
com.ecommerce
├── catalog // Bounded Context: Product Catalog
│ ├── domain
│ │ ├── Product.java
│ │ ├── Category.java
│ │ └── ProductRepository.java
│ └── api
│ └── CatalogService.java
│
├── cart // Bounded Context: Shopping Cart
│ ├── domain
│ │ ├── Cart.java
│ │ ├── CartItem.java
│ │ └── CartRepository.java
│ └── api
│ └── CartService.java
│
├── order // Bounded Context: Order Processing
│ ├── domain
│ │ ├── Order.java
│ │ ├── OrderItem.java
│ │ └── OrderRepository.java
│ └── api
│ └── OrderService.java
│
└── payment // Bounded Context: Payment Processing
├── domain
│ ├── Payment.java
│ ├── PaymentMethod.java
│ └── PaymentGateway.java
└── api
└── PaymentService.java
// Package dependencies show context relationships:
// - cart → catalog (needs product information)
// - order → catalog (order contains products)
// - order → payment (order triggers payment)
// - No circular dependencies between contexts
Modern Language Module Systems
Java Platform Module System (JPMS, Java 9+) formalizes package encapsulation through modules. `module-info.java` declares module name, required modules, and exported packages. Only exported packages visible outside module; other packages remain module-internal. This language-enforced encapsulation realizes UML package visibility concepts—public elements export via `exports` clause, private elements remain inaccessible externally. JPMS modules enable strong encapsulation preventing reflection hacks accessing internal APIs, supporting reliable modular evolution where internal changes don't break clients.
// Java Module: Formal Package Encapsulation
// module-info.java (Module Declaration)
module com.ecommerce.order {
requires com.ecommerce.catalog; // Dependency declaration
requires com.ecommerce.payment;
exports com.ecommerce.order.api; // Public package
// com.ecommerce.order.domain is package-private
// External modules cannot access domain package
}
// JPMS advantages:
// - Compiler enforces dependency declarations
// - Private packages truly inaccessible (no reflection hacks)
// - Circular dependencies detected at compile time
// - Reliable encapsulation enables modular evolution
C++20 modules provide similar encapsulation replacing header-based `#include` with `import` statements. Modules export interfaces explicitly, keeping implementation private. This eliminates include-order dependencies, speeds compilation through precompiled module interfaces, and provides clean package boundaries. Python's package system with `__init__.py` and `__all__` controls package exports. Go modules manage dependencies with `go.mod`. These language features demonstrate UML package concepts becoming programming language primitives—logical organization, dependency declaration, visibility control all first-class language concerns.
What Modern Practice Replaced
Automated dependency analysis tools generate package dependency graphs from source code rather than manual UML diagrams. jdepend analyzes Java packages extracting dependencies, metrics (abstractness, instability), and violations. dependency-cruiser validates JavaScript/TypeScript module dependencies against rules. Madge visualizes Node.js module dependencies. go mod graph shows Go module relationships. These tools provide authoritative package structure guaranteed accurate because extracted from actual code. Generated graphs update automatically as code evolves, preventing documentation drift plaguing manually maintained package diagrams.
Architecture fitness functions (automated tests validating architectural characteristics) enforce package dependencies programmatically. ArchUnit (Java) writes tests asserting package rules: "classes in presentation package should not depend on data access package," "domain package should have no external dependencies," "circular dependencies prohibited." These executable architecture specifications fail builds when violated, preventing architectural erosion. This shift from descriptive diagrams toward prescriptive tests represents package governance evolution—moving from documentation artifacts toward enforced constraints.
// Architecture Fitness Function: Enforcing Package Rules
// ArchUnit (Java): Package Dependency Rules as Tests
@Test
public void layeredArchitecture_shouldBeRespected() {
layeredArchitecture()
.layer("Presentation").definedBy("..presentation..")
.layer("Business").definedBy("..business..")
.layer("DataAccess").definedBy("..dataaccess..")
.whereLayer("Presentation").mayNotBeAccessedByAnyLayer()
.whereLayer("Business").mayOnlyBeAccessedByLayers("Presentation")
.whereLayer("DataAccess").mayOnlyBeAccessedByLayers("Business")
.check(classes);
}
@Test
public void noCyclesInPackageDependencies() {
slices()
.matching("com.ecommerce.(*)..")
.should().beFreeOfCycles()
.check(classes);
}
// Tests fail if:
// - Presentation accesses DataAccess directly
// - Circular dependencies exist between packages
// - Layering violations introduced
Where Package Diagrams Remain Valuable
Package diagrams excel at high-level architecture communication during design reviews and stakeholder presentations. Showing major subsystems, dependencies, and layering provides overview automated graphs' detail obscures. Whiteboard sketches during architecture discussions, PowerPoint presentations to executives, onboarding documentation for new developers—all benefit from simplified package views showing forest, not trees. These communication diagrams abstract details, showing strategic structure automated tools' comprehensiveness can obscure.
Architectural conformance validation uses package diagrams as references specifying intended architecture. Intended package structure documented in diagram, actual package dependencies extracted from code, deviations identified through comparison. This intended-vs-actual analysis reveals architectural drift—unintended dependencies creeping in, layering violations accumulating, circular dependencies emerging. Package diagrams serve as architectural north star—vision teams work toward—while automated analysis measures conformance progress or degradation.
Strategic DDD context mapping visualizes bounded context relationships through package diagrams. Showing which contexts depend on others, integration patterns at context boundaries (Customer-Supplier, Conformist, Anti-Corruption Layer), and upstream/downstream relationships communicates strategic domain architecture. These strategic package maps guide tactical implementation—teams understand domain structure informing microservice boundaries, integration approaches, and team coordination needs. The strategic perspective package diagrams provide complements tactical code-level dependency graphs.
Modern Tooling
PlantUML supports package diagrams enabling version-controlled architecture documentation. Package hierarchies, dependencies, and nested structures expressible in text syntax compile to images. This diagrams-as-code approach integrates package documentation into development workflow—architecture diagrams version controlled with code, updated through pull requests, generated in CI/CD pipelines.
// PlantUML: Package Diagram as Code
@startuml
package "Presentation Layer" {
[Controllers]
[ViewModels]
}
package "Business Layer" {
[Domain Entities]
[Use Cases]
[Business Rules]
}
package "Data Access Layer" {
[Repositories]
[Database Entities]
}
[Controllers] --> [Use Cases]
[Use Cases] --> [Domain Entities]
[Domain Entities] --> [Repositories]
[Repositories] --> [Database Entities]
@enduml
// Benefits:
// - Text format (version control friendly)
// - Generate images automatically
// - UML-compliant package notation
C4 Model System Context diagrams serve similar purpose to package diagrams at system level, showing major system boundaries and external dependencies. While not identical to package diagrams, C4's hierarchical decomposition (Context → Containers → Components) parallels package nesting conceptually. Teams using C4 for system architecture often find package diagrams unnecessary—C4 provides sufficient organizational structure without additional UML package notation.
Conclusion
Packages serve distinct purpose among UML constructs—organizing modeling artifacts hierarchically enabling complexity management, team coordination, and architectural encapsulation rather than modeling system structure directly. Their function—providing logical grouping, controlling visibility, declaring dependencies—proved sufficiently valuable that programming languages formalized package concepts through module systems (Java JPMS, C++20 modules, Python packages, Go modules) with compiler-enforced encapsulation and dependency management. Contemporary practice uses package diagrams selectively for high-level architecture communication, bounded context visualization in Domain-Driven Design, and conformance checking against intended architecture, while relying on automated dependency analysis, architecture fitness functions, and language module systems for authoritative package structure. Understanding package organization principles—hierarchical decomposition, dependency management, encapsulation boundaries—enables informed architectural decisions regardless whether documentation takes form of UML package diagrams, language module declarations, automated dependency graphs, or architecture decision records. The fundamental insight—complex systems require hierarchical organization with managed dependencies and controlled visibility—persists across all approaches, from UML package diagrams through modern language module systems and automated architecture governance, demonstrating enduring value even as specific techniques evolved from manual documentation toward language-enforced modular architecture and programmatic conformance validation.
The next lesson concludes this module by discussing standardization.

