Encapsulation and Access Modifiers
Encapsulation is the object-oriented principle of bundling data with the logic that operates on it, and restricting direct access to that data from outside the type. In C#, access modifiers are the language mechanism that enforces this: they declare, member by member and type by type, who is allowed to see or use a given piece of code. Good encapsulation means a class's internal representation can change freely as long as its public contract stays stable — callers depend only on what is exposed, not on how it is implemented. C# gives you finer-grained control over visibility than many languages, with six distinct access levels rather than the typical three.
Cricket analogy: A team's dressing room keeps tactical discussions private from the opposition, exposing only the final batting order publicly — encapsulation lets a team change its game plan internally as long as the lineup announced stays consistent.
The access modifier spectrum
public members are visible to any code that can see the type at all. private (the default for class members) restricts visibility to the containing type only. protected extends visibility to the declaring type and any class that derives from it, anywhere. internal restricts visibility to the current assembly (project/DLL) — useful for implementation details shared across your own codebase but not meant for external consumers. protected internal is the union of the two: accessible either from a derived class anywhere, or from anywhere in the same assembly. private protected (C# 7.2+) is the intersection instead: accessible only from a derived class that is also within the same assembly, which is the most restrictive inheritance-aware modifier.
Cricket analogy: public is a shot any batsman on any team can play; private is a signature technique only that batsman uses; protected is a family coaching trick passed only to direct successors in that batting lineage; internal is a tactic known only within one franchise's squad; protected internal is known to that squad or any inheriting player elsewhere; private protected is known only to successors who also play for that same franchise.
public class BankAccount
{
// Only this class can touch the raw balance.
private decimal _balance;
// Public read access, but writes are internal to the class hierarchy logic below.
public decimal Balance => _balance;
// Visible to this assembly only — e.g. an internal reconciliation job.
internal void AdjustForReconciliation(decimal delta) => _balance += delta;
// Visible to this class and subclasses across any assembly.
protected void ApplyInterest(decimal rate) => _balance += _balance * rate;
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
_balance += amount;
}
public bool TryWithdraw(decimal amount)
{
if (amount <= 0 || amount > _balance)
return false;
_balance -= amount;
return true;
}
}Why encapsulate at all
Exposing _balance as a public field would let any caller set it to a negative number or bypass business rules entirely. By keeping the field private and exposing controlled operations (Deposit, TryWithdraw) plus a read-only computed property, the class guarantees its own invariants hold no matter who calls it. This is the core value of encapsulation: it moves validation and consistency logic into one place, rather than trusting every caller to do the right thing.
Cricket analogy: A scoreboard operator doesn't let spectators directly edit the run total; they must go through the official scorer who validates each run against what actually happened, preventing a fan from typing in a fake score.
C#'s default accessibility differs by context: class members default to private if unspecified, but top-level type declarations (classes, interfaces, structs) default to internal when no modifier is written. This trips up developers coming from Java, where the no-modifier default is package-private, a different and slightly broader concept than C#'s assembly-scoped internal.
A frequent design mistake is exposing a mutable collection through a public property, e.g. public List<Order> Orders { get; }. Even though the property itself has no setter, callers can still call account.Orders.Add(...) or .Clear() directly, completely bypassing any validation logic. Prefer exposing IReadOnlyList<T> or IReadOnlyCollection<T> and offering explicit AddOrder/RemoveOrder methods that enforce invariants.
File-scoped and other modern visibility tools
C# 11 introduced the file access modifier, which restricts a type's visibility to the single source file it is declared in — useful for source generators and internal helper types that should never leak even within the same assembly. Combined with nested classes (which can use any access modifier, including private), and partial classes for splitting large types across files, C# offers substantial flexibility for shaping exactly how much of a type's surface area is visible to each layer of a codebase.
Cricket analogy: A file-scoped tactic is like a play the team only discusses in the dressing room and never writes into the shared team manual, invisible even to other coaches on the same franchise staff; a nested specialist role like a designated 'super-sub' exists only within one squad's structure; splitting a long strategy document across multiple notebooks that combine into one plan mirrors partial classes.
- Encapsulation bundles data with the logic that maintains its invariants, hiding raw internal state from callers.
- C# has six access levels: public, private, protected, internal, protected internal, and private protected.
- Class members default to private; top-level type declarations default to internal when unspecified.
- protected internal is a union (derived-class-anywhere OR same-assembly); private protected is an intersection (derived-class AND same-assembly).
- Exposing a mutable collection via a public getter-only property still lets callers mutate it directly — prefer read-only interfaces.
- The
filemodifier (C# 11+) scopes a type's visibility to a single source file.
Practice what you learned
1. What is the default accessibility of a class member with no explicit access modifier?
2. How does `protected internal` differ from `private protected`?
3. Why is exposing `public List<T> Items { get; }` considered a weak encapsulation practice?
4. What is the default accessibility of a top-level class declared without an access modifier?
5. What does the `file` access modifier (C# 11+) restrict a type's visibility to?
Was this page helpful?
You May Also Like
Properties and Indexers
Explore how C# properties expose controlled field access through get/set accessors, and how indexers let custom types support array-like `obj[key]` syntax.
Static Members
Learn how static fields, methods, and classes attach behavior and state to a type itself rather than to individual instances, and when that design choice makes sense.
Classes and Objects
Understand how C# classes define blueprints for reference-type objects, covering fields, methods, object creation, and the distinction between a class and its instances.
Abstract Classes
Learn how abstract classes provide a partially-implemented base type that cannot be instantiated directly, mixing shared implementation with members derived classes must supply.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics