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:
| Element | Meaning |
| 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:
-
Onboarding a new contributor: one picture answers "what can import what" faster
than any amount of prose.
-
Architecture review: compare the intended diagram against an actual dependency
graph generated by your build tool. Discrepancies show where the design has drifted.
-
Specifying a multi-language project: the diagram commits all implementations to
the same structure before any code is written.
-
Detecting cycles: draw the current state of the codebase. Any arrow that creates
a loop is a violation to fix.
Package diagrams are not useful when the interesting question is behavioral. For
that, reach for sequence or activity diagrams instead.
8. References
| Resource | Description |
|
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. |