Loading content...
In the previous page, we learned to notate classes with their attributes and operations. But we glossed over a critical element: those small symbols (+, -, #, ~) that prefix every member. These aren't decorative—they are visibility markers, and they encode the fundamental concept of encapsulation.
Visibility determines who can access what. It's the mechanism by which classes protect their internals, expose only what's necessary, and create well-defined boundaries. Understanding visibility is not merely about UML syntax—it's about understanding how to design software that is maintainable, evolvable, and resistant to accidental coupling.
By the end of this page, you will master the four visibility levels in UML, understand when and why to use each, recognize common visibility anti-patterns, and be able to make informed decisions about encapsulation in your designs.
Before examining the specific markers, let's understand why visibility control exists at all.
The core problem:
In any non-trivial system, classes depend on each other. Class A calls methods on class B. Class C reads data from class D. These dependencies are inevitable—they're how software works.
But not all dependencies are equal:
Visibility markers distinguish between these two categories. They are the mechanism by which class authors say: "This is part of my contract; depend on it" vs. "This is an implementation detail; don't touch it."
Once you make something public, you're promising that it will continue to exist and work the same way. Changing public interfaces breaks clients. Private members can change freely because no external code should depend on them. This asymmetry drives all visibility decisions.
UML defines four standard visibility levels, each represented by a specific symbol:
┌─────────────────────────────────────────────────┐
│ BankAccount │
├─────────────────────────────────────────────────┤
│ + accountNumber: String // PUBLIC │
│ - balance: Decimal // PRIVATE │
│ # overdraftLimit: Decimal // PROTECTED │
│ ~ internalCode: String // PACKAGE │
├─────────────────────────────────────────────────┤
│ + deposit(amount: Decimal) // PUBLIC │
│ - validateAmount(amt): Bool // PRIVATE │
│ # applyInterest(): void // PROTECTED │
│ ~ reconcile(): void // PACKAGE │
└─────────────────────────────────────────────────┘
| Symbol | Name | Who Can Access | Language Equivalent |
|---|---|---|---|
+ | Public | Anyone (no restrictions) | public in Java/C#/TypeScript |
- | Private | Only the class itself | private in Java/C#/TypeScript |
# | Protected | Class and its subclasses | protected in Java/C#/TypeScript |
~ | Package | Classes in the same package/module | internal in C#, package-private in Java, internal in Kotlin |
Different programming languages implement visibility slightly differently. Java has package-private (default), C# has internal, Python uses naming conventions (_ and __). UML's four levels are a superset that maps to most OO languages, even if the exact semantics vary.
Public members (marked with +) are accessible to everyone. They form the class's external contract—the interface that other classes depend on.
When to use public:
MAX_CAPACITY).Example of well-designed public interface:
┌─────────────────────────────────────────────────┐
│ ShoppingCart │
├─────────────────────────────────────────────────┤
│ (attributes are all private) │
├─────────────────────────────────────────────────┤
│ + addItem(product: Product, qty: Integer) │
│ + removeItem(productId: UUID) │
│ + getItemCount(): Integer │
│ + calculateTotal(): Decimal │
│ + checkout(): Order │
│ + clear(): void │
└─────────────────────────────────────────────────┘
Notice: The public interface describes what you can do with a cart, not how the cart works internally. There's no getItems() returning the internal list, no setTotal() allowing direct manipulation, no access to implementation mechanics.
Public attributes bypass encapsulation—anyone can read or modify them directly. Prefer private attributes with public getters/setters (or better: meaningful methods). The only common exception is immutable value objects where direct access is safe.
+ items: List<CartItem>
— Exposes internal collection
— External code can modify directly
— Can't add validation/logging+ total: Decimal
— Can be set to anything
— No control over calculation
— Inconsistent state possible- items: List<CartItem>
+ getItems(): List<CartItem>
— Returns copy or read-only view
— Cart controls its own state+ calculateTotal(): Decimal
— Computed when needed
— Always consistent with items
— Can add caching laterPrivate members (marked with -) are accessible only within the class itself. They represent implementation details that are hidden from the outside world.
The principle: Start private, make public only when necessary.
This is the most conservative and safest default. Private members can be changed freely without breaking external code, because no external code can access them.
When to use private:
Example showing private implementation:
┌─────────────────────────────────────────────────────┐
│ PasswordHasher │
├─────────────────────────────────────────────────────┤
│ - algorithm: String │
│ - iterations: Integer │
│ - saltLength: Integer │
│ - _cache: Map<String, String> │
├─────────────────────────────────────────────────────┤
│ + hash(password: String): String │
│ + verify(password: String, hash: String): Boolean │
│ - generateSalt(): Bytes │
│ - applyAlgorithm(input: Bytes, salt: Bytes): Bytes │
│ - validatePasswordStrength(password: String): Bool │
│ - getCachedHash(password: String): String? │
└─────────────────────────────────────────────────────┘
External code sees only hash() and verify(). The entire hashing mechanism—algorithm choice, salting, caching—is private. This allows the implementation to change (e.g., switch from bcrypt to Argon2) without affecting callers.
Every private member is a refactoring opportunity. You can rename them, change their types, merge them, split them—with confidence that nothing external will break. The more private members you have, the more freedom you have to improve internals.
Protected members (marked with #) occupy a middle ground: they're hidden from the general public but accessible to subclasses. Protected members form what we might call the class's inheritance interface—the contract it offers to classes that extend it.
The role of protected:
Protected members are intended for a specific audience: developers creating subclasses. They provide hooks for customization, access to internal state that subclasses need, and machinery that the public shouldn't see but inheritance requires.
When to use protected:
Example showing protected in a class hierarchy:
┌─────────────────────────────────────────────────────┐
│ «abstract» Document │
├─────────────────────────────────────────────────────┤
│ - id: UUID │
│ # content: String │
│ # metadata: Map<String, String> │
│ - createdAt: DateTime │
├─────────────────────────────────────────────────────┤
│ + save(): void │
│ + render(): String │
│ # formatContent(): String {abstract} │
│ # validateContent(): Boolean {abstract} │
│ # getDefaultMetadata(): Map<String, String> │
│ - generateId(): UUID │
└─────────────────────────────────────────────────────┘
△
│
│
┌─────────────────────────────────────────────────────┐
│ PdfDocument │
├─────────────────────────────────────────────────────┤
│ - pageCount: Integer │
│ # pdfVersion: String │
├─────────────────────────────────────────────────────┤
│ + render(): String │
│ # formatContent(): String │
│ # validateContent(): Boolean │
│ - generatePdfStructure(): Bytes │
└─────────────────────────────────────────────────────┘
The dynamics at play:
id and createdAt are private—even subclasses shouldn't manipulate these directly.content and metadata are protected—subclasses need access to work with the document's core data.formatContent() and validateContent() are protected abstract—subclasses must implement these hooks.getDefaultMetadata() is protected—a utility that subclasses can use or override.generateId() is private—purely internal implementation that subclasses shouldn't touch.Protected members, while not fully public, are still promises to subclass authors. Changing protected members can break subclasses just as changing public members breaks external code. Use protected judiciously—it expands your maintenance burden.
Package visibility (marked with ~) restricts access to classes within the same package, module, or namespace. This is less commonly used in diagrams but important for understanding larger system organization.
The role of package visibility:
Package visibility enables internal APIs—interfaces that classes within a package use to collaborate, but that shouldn't be exposed outside the package. This is particularly useful for:
Example: Payment processing package:
package payment {
┌─────────────────────────────────────────────────────┐
│ PaymentProcessor │
├─────────────────────────────────────────────────────┤
│ - gateway: PaymentGateway │
│ ~ transactionLog: TransactionLog │
├─────────────────────────────────────────────────────┤
│ + processPayment(order: Order): PaymentResult │
│ + refund(transactionId: String): RefundResult │
│ ~ retryFailedTransaction(txId: String): Boolean │
│ ~ getTransactionMetrics(): Metrics │
│ - connectToGateway(): void │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ TransactionLog │
├─────────────────────────────────────────────────────┤
│ ~ entries: List<LogEntry> │
├─────────────────────────────────────────────────────┤
│ ~ log(entry: LogEntry): void │
│ ~ findByTransactionId(id: String): LogEntry? │
│ ~ getRecentFailures(): List<LogEntry> │
└─────────────────────────────────────────────────────┘
}
TransactionLog is entirely package-visible—it's an internal collaboration mechanism. PaymentProcessor exposes public methods to the outside world but uses package-visible methods for internal coordination that other payment classes might need.
| Language | Package Visibility Mechanism | Notes |
|---|---|---|
| Java | Default (no modifier) | Classes in same package can access |
| C# | internal keyword | Assembly-level visibility |
| Kotlin | internal keyword | Module-level visibility |
| TypeScript | No direct equivalent | Use modules/barrels for similar effect |
| Python | Convention (_ prefix) | No enforcement, convention only |
| Go | lowercase name | Package-level visibility by naming |
Package visibility is often omitted from high-level design diagrams because it's an implementation detail. Include it when designing package/module structure specifically, or when the internal collaboration patterns are important to communicate.
When designing a class, how do you decide what visibility to assign? Here's a systematic decision framework:
Every increase in visibility increases maintenance burden and reduces flexibility. When in doubt, choose the more restrictive option. You can always increase visibility later, but decreasing it breaks clients.
Visibility mistakes are among the most common design flaws. Let's examine patterns to avoid:
+ public.#) so subclasses can "easily" access them.increaseBalance() not setBalance().Each visibility increase seems harmless in isolation. But cumulatively, they erode encapsulation until the class has no meaningful boundaries. Be vigilant about each decision.
Visibility markers are small symbols with large implications. Let's consolidate the key principles:
+), Private (-), Protected (#), and Package (~) form a hierarchy from most to least accessible.What's next:
Now that you understand visibility, we'll explore static and abstract notation. Static members belong to classes rather than instances, while abstract members define contracts without implementations. Both are essential for modeling real OO systems.
You now understand UML visibility markers and their role in encapsulation. In the next page, we'll cover static and abstract notation—the mechanisms for class-level members and inheritance contracts.