UML Package Diagrams

concepts and notation, illustrated with TextFinder

1.  What a Package Diagram Shows

A UML package diagram answers one question: which named group of code depends on which other group? It deliberately omits internal detail - no methods, no fields, no inheritance chains. That omission is the point. When you need to communicate deployment boundaries, layering rules, or allowed import directions across a team, a package diagram lets a reader absorb the constraint in seconds without wading through class-level noise. "Package" maps naturally to a language namespace, a module, a crate, a project, or an assembly - whatever unit of code your build system treats as independently compilable. The diagram notation does not care which; it cares only about the dependency arrows between them.

2.  Notation

The diagram uses three elements:
ElementMeaning
Rectangle with tab A package. The tab at the top-left carries the package name. The body may list contained classifiers or be left empty when only boundaries matter.
Dashed arrow  → A dependency. The arrow points from the package that depends to the package it depends on. If A imports B, the arrow goes A → B.
<<stereotype>> A label that qualifies the kind of relationship. Common ones: <<use>> (calls into), <<import>> (brings names into scope), <<merge>> (extends another package's contents).
A rule worth enforcing as a team: dependency arrows must never form a cycle. A cycle means two packages are mutually entangled - they cannot be compiled, tested, or deployed independently. Package diagrams make cycles immediately visible because you see the arrow turn back on itself.

3.  TextFinder as a Worked Example

TextFinder walks a directory tree and reports every file whose content matches a user-supplied regular expression. Five implementations - Rust (baseline and optimized), C++, C#, and Python - share the same architecture. That shared structure makes it a useful worked example: the same package diagram describes all five. The architecture rule for TextFinder is strict: no library package may depend on another library package. All coordination flows through EntryPoint using callbacks. The package diagram captures that rule in one picture.
graph TD
    subgraph EP["<<package>> EntryPoint"]
        ep["Wires the three libraries together.\nHolds no domain logic of its own."]
    end
    subgraph CL["<<package>> CommandLine"]
        cl["Parses /Key [Value] tokens from argv.\nExposes typed option values to callers."]
    end
    subgraph DN["<<package>> DirNav"]
        dn["Depth-first directory walk.\nFires a callback on each directory and file."]
    end
    subgraph OUT["<<package>> Output"]
        out["Applies the regex to file content.\nWrites matching paths to the console."]
    end
    EP -->|"<<use>>"| CL
    EP -->|"<<use>>"| DN
    EP -->|"<<use>>"| OUT
      

Figure 1. TextFinder package diagram.

Three arrows radiate outward from EntryPoint; none connect the library packages to each other. That absence of cross-library arrows is the design constraint made visible. Read the diagram from the arrows, not the boxes. Three <<use>> dependencies leave EntryPoint; none exist between CommandLine, DirNav, and Output. That single observation tells a new contributor everything they need to know about which files they are allowed to import from which other files.

4.  Drilling In: What EntryPoint Actually Does

The package diagram defers all internal detail. Once a reader accepts the boundary rule, the natural next question is: how does EntryPoint coordinate three packages that cannot see each other? The answer is callbacks - EntryPoint supplies a function (or object) to DirNav that DirNav calls for each directory and file it visits. That function, also in EntryPoint, delegates to Output. In Rust the callback contract is a trait. EntryPoint defines a concrete type TfAppl that implements it:
impl dir_nav_lib::DirEvent for TfAppl {
    fn do_dir(&mut self, d: &str) {
        self.curr_dir = d.to_string();
    }
    fn do_file(&mut self, f: &str) {
        let fqf = format!("{}/{}", self.curr_dir, f);
        if self.tf.find(&fqf) {
            print!("\n      {:?}", f);
        }
    }
}
In C# and Python the same contract is a delegate or callable object. In C++ it is a function pointer or std::function. The package diagram does not show this distinction - it only shows that the dependency arrow from EntryPoint to DirNav exists, and that no arrow goes the other direction.

5.  Complementing the Package Diagram with a Sequence Diagram

Package diagrams show structure. They say nothing about time - which component acts first, which waits, which returns a value. A sequence diagram covers that gap. The two diagrams are complementary: read the package diagram to understand boundaries, read the sequence diagram to understand the runtime flow. For TextFinder the runtime flow is straightforward. EntryPoint parses options, configures Output, then hands control to DirNav. From that point DirNav drives everything by firing callbacks.
sequenceDiagram
    participant EP as EntryPoint
    participant CL as CommandLine
    participant DN as DirNav
    participant OUT as Output

    EP->>CL: parse(argv)
    CL-->>EP: options (path, regex, ext, flags)
    EP->>OUT: set_regex(pattern)
    EP->>DN: add_pattern(ext)
    EP->>DN: search(root_path)
    loop each directory
        DN->>EP: do_dir(dir_name)
        loop each file
            DN->>EP: do_file(file_name)
            EP->>OUT: find(fully_qualified_path)
            OUT-->>EP: matched?
        end
    end
      

Figure 2. Runtime sequence.

DirNav calls back into EntryPoint for every directory and file; EntryPoint delegates the match decision to Output. Control never flows directly between the library packages. The sequence diagram confirms what the package diagram asserts: the only runtime calls that cross a package boundary arrive at or leave from EntryPoint. DirNav and Output never exchange messages directly.

6.  The Same Diagram Across Five Implementations

One practical benefit of the package diagram is that it survives translation. The TextFinder architecture was specified once and then implemented independently in Rust, C++, C#, and Python. Each implementation uses different file names, different module systems, and different callback mechanisms, yet all five satisfy the same package diagram. That is the diagram doing its job: it expresses a constraint at a level of abstraction that is language-independent.
Package Rust C++ C# Python
CommandLine cmd_line_lib.rs CmdLine.ixx CmdLine.cs cmd_line.py
DirNav dir_nav_lib.rs DirNav.ixx DirNav.cs dir_nav.py
Output text_finder.rs Output.ixx Output.cs output.py
EntryPoint text_finder.rs / main main.cpp Program.cs PyTextFinder.py
The callback mechanism is the one element that varies. Rust uses a trait; C# uses Action delegates; Python uses callable objects; C++ uses std::function. None of that variation changes the package diagram.

7.  When to Draw One

Draw a package diagram when the question you need to answer - or communicate - is about boundaries and allowed dependencies, not about types or behavior. Specific triggers: Package diagrams are not useful when the interesting question is behavioral. For that, reach for sequence or activity diagrams instead.

8.  References

ResourceDescription
Project Story: TextFinder Architecture, CLI reference, performance benchmarks, and code metrics for all five implementations.
SWDev Projects Overview of TextFinder, CodeAnalyzer, and CallGraph as cross-language design exercises.
Mermaid diagram syntax Reference for the Mermaid notation used to render the diagrams on this page.
UML Package Diagrams Overview Formal UML notation reference covering package, import, merge, and access relationships.