← Open in the full interactive course (progress tracking, search & more)

Encapsulation is the art of hiding what's internal and exposing only what's necessary.

Imagine you're using a vending machine. You put in money, press a button, and a drink comes out. You don't need to know how the machine works inside — where the cans are stored, how the motors turn, or how the change is counted. All you see is a simple interface: coin slot, buttons, and a dispenser.

That's encapsulation in a nutshell: hiding the complex internal details and exposing a clean, simple way to interact with an object. In object-oriented programming, encapsulation is one of the four pillars (alongside abstraction, inheritance, and polymorphism). It's what makes your code modular, maintainable, and safe.

In this lesson, you'll learn what encapsulation is, why it matters, how to implement it in C# using access modifiers and properties, and how to design classes that are easy to use and hard to misuse.

What Is It?

The Simple Explanation

Encapsulation is the bundling of data (fields) and behaviour (methods) into a single unit (a class), and controlling access to that data from outside. It's like putting your data in a capsule with a clear label and a button — you interact through the label and button, not by opening the capsule.

Encapsulation = Data Hiding + Controlled Access

Encapsulation has two main aspects:

The class acts as a protective wrapper around its data.

The Technical Definition

In C#, encapsulation is achieved through access modifiers (private, protected, internal, public, etc.) and by exposing data through properties and methods rather than public fields. It ensures that an object's internal state can only be altered in controlled ways, preserving invariants and reducing coupling.

Encapsulation Level Mechanism Example
Hiding fields private fields private int _age;
Controlled read/write Properties with get/set public int Age { get; private set; }
Behaviour exposure Public methods public void SetAge(int age) { ... }
Validation Logic in setters/methods if (value < 0) throw ...

Why Does It Exist?

The Problem

Without encapsulation, an object's internal state is completely exposed. Any code can change it arbitrarily, leading to:

The Solution

Encapsulation solves these by:

Big Picture

Encapsulation is the foundation of object-oriented design. Here's how it looks:

ENCAPSULATION — THE CONTRACT
WITHOUT ENCAPSULATION
Object
public int Balance;
public string Name;
Any code can change directly
No validation
Tight coupling
WITH ENCAPSULATION
Object
private decimal _balance;
public decimal Balance => _balance;
public void Deposit(decimal amount) { ... }
Only methods can change state
Validation enforced
Loose coupling
The class is a capsule: private internals, public interface.

How It Works

Encapsulation is implemented using a combination of techniques:

ENCAPSULATION — IMPLEMENTATION STEPS
Step 1 — Make fields private
public class BankAccount { private decimal _balance; // hidden from outside private string _accountHolder; }

Private fields are the first line of defense. They can only be accessed within the class itself.

Step 2 — Provide public properties or methods
public class BankAccount { private decimal _balance; // Read-only property (no setter) public decimal Balance => _balance; // Public method to change state public void Deposit(decimal amount) { if (amount <= 0) throw new ArgumentException("Amount must be positive."); _balance += amount; } public void Withdraw(decimal amount) { if (amount <= 0) throw new ArgumentException("Amount must be positive."); if (amount > _balance) throw new InvalidOperationException("Insufficient funds."); _balance -= amount; } }

The class exposes a controlled interface — callers can read the balance and perform deposits/withdrawals, but cannot directly alter the balance.

Step 3 — Add validation and logic
public void SetAge(int age) { if (age < 0 || age > 150) throw new ArgumentOutOfRangeException(nameof(age), "Age must be between 0 and 150."); _age = age; // Optionally raise an event or update derived fields }

The class enforces its own invariants. It can also log changes, raise events, or trigger other side effects.

Step 4 — Use properties with private setters or init
public class Person { public string Name { get; init; } // set only during construction public int Age { get; private set; } // set only inside the class public Person(string name, int age) { Name = name; Age = age; } public void HaveBirthday() => Age++; // allowed (private setter) }

Properties with init or private set provide fine-grained control over mutability.

Simple Example

Let's create a Temperature class that encapsulates a temperature value in Celsius, with validation and conversion methods.

public class Temperature { // ─── Private field ─── private double _celsius; // ─── Public property with validation ─── public double Celsius { get => _celsius; set { if (value < -273.15) // absolute zero throw new ArgumentOutOfRangeException(nameof(value), "Temperature cannot be below absolute zero."); _celsius = value; } } // ─── Computed properties ─── public double Fahrenheit => _celsius * 9 / 5 + 32; public double Kelvin => _celsius + 273.15; // ─── Constructor ─── public Temperature(double celsius) { Celsius = celsius; // uses validation in setter } // ─── Factory method ─── public static Temperature FromFahrenheit(double fahrenheit) { var celsius = (fahrenheit - 32) * 5 / 9; return new Temperature(celsius); } // ─── Override ToString ─── public override string ToString() => $"{_celsius:F1}°C / {Fahrenheit:F1}°F / {Kelvin:F1}K"; } // ─── Usage ─── var temp = new Temperature(25); Console.WriteLine(temp); // 25.0°C / 77.0°F / 298.2K temp.Celsius = 30; Console.WriteLine(temp); // 30.0°C / 86.0°F / 303.2K try { temp.Celsius = -300; // throws exception } catch (ArgumentOutOfRangeException ex) { Console.WriteLine($"Error: {ex.Message}"); } var temp2 = Temperature.FromFahrenheit(98.6); Console.WriteLine(temp2); // 37.0°C / 98.6°F / 310.2K

This example shows encapsulation in action:

Real-World Example

In a real application, encapsulation is used everywhere. Consider a ShoppingCart class that manages a list of items, totals, and discounts.

public class ShoppingCart { // ─── Private fields ─── private readonly List<CartItem> _items = new(); private decimal _discountPercentage = 0; // ─── Public read-only view ─── public IReadOnlyList<CartItem> Items => _items; // ─── Public properties with logic ─── public decimal Subtotal => _items.Sum(i => i.Price * i.Quantity); public decimal DiscountPercentage { get => _discountPercentage; set { if (value < 0 || value > 100) throw new ArgumentOutOfRangeException(nameof(value), "Discount must be between 0 and 100."); _discountPercentage = value; } } public decimal Total => Subtotal * (1 - _discountPercentage / 100); // ─── Public methods to modify state ─── public void AddItem(CartItem item) { if (item == null) throw new ArgumentNullException(nameof(item)); if (item.Quantity <= 0) throw new ArgumentException("Quantity must be positive."); // Check if item already exists; if so, update quantity var existing = _items.FirstOrDefault(i => i.ProductId == item.ProductId); if (existing != null) { existing.Quantity += item.Quantity; } else { _items.Add(item); } } public void RemoveItem(int productId) { var item = _items.FirstOrDefault(i => i.ProductId == productId); if (item != null) _items.Remove(item); } public void Clear() => _items.Clear(); public void ApplyDiscount(decimal percentage) => DiscountPercentage = percentage; } // ─── CartItem is a simple record (immutable by design) ─── public record CartItem(int ProductId, string Name, decimal Price, int Quantity); // ─── Usage ─── var cart = new ShoppingCart(); cart.AddItem(new CartItem(1, "Book", 19.99m, 2)); cart.AddItem(new CartItem(2, "Pen", 2.50m, 3)); Console.WriteLine($"Subtotal: {cart.Subtotal:C}"); // $47.48 cart.ApplyDiscount(10); Console.WriteLine($"Total after 10%: {cart.Total:C}"); // $42.73 // cart._items = ... // Error: private // cart.Subtotal = 100; // Error: no setter

This demonstrates:

Analogy

Encapsulation = ATM Machine

Think of a bank ATM. It encapsulates all the complex banking logic inside a secure box.

Encapsulation protects both the user (from mistakes) and the system (from misuse).

Under the Hood

How does the .NET runtime enforce encapsulation?

ENCAPSULATION — INTERNAL ENFORCEMENT
1. Compile-time checking
2. Runtime enforcement
3. Properties are syntactic sugar
4. Benefits for performance and security

Common Confusion

1. Encapsulation vs Abstraction

Encapsulation is about hiding implementation details (data hiding and controlled access). Abstraction is about providing a simplified interface (what the object does, not how). They often go together: you use encapsulation to achieve abstraction.

2. Properties vs Public Fields

Many beginners think properties are just syntactic sugar for fields. But properties allow you to add logic (validation, events, etc.) without changing the public API. Always use properties instead of public fields.

3. Encapsulation is not just about hiding everything

It's about exposing a useful and safe interface. Some members need to be public (the API), others need to be internal or protected for extensibility, and others should be private (implementation). Balance is key.

Common Mistakes

Mistake 1 — Exposing public fields

Wrong:

public class Person { public string Name; // public field — breaks encapsulation }

Correct:

public class Person { public string Name { get; set; } // property }

Mistake 2 — Exposing mutable collections directly

Wrong:

public class Order { public List<OrderItem> Items { get; set; } // callers can modify the list }

Correct:

public class Order { private readonly List<OrderItem> _items = new(); public IReadOnlyList<OrderItem> Items => _items; // read-only view public void AddItem(OrderItem item) => _items.Add(item); }

Mistake 3 — Putting validation outside the class

Wrong:

var age = int.Parse(input); if (age >= 0 && age <= 150) person.Age = age; // validation outside

Correct:

person.SetAge(int.Parse(input)); // validation inside the class

Mistake 4 — Over-encapsulation (getter/setter for every field without reason)

If a field doesn't need validation or logic, a simple auto-property is fine. Don't add full properties with backing fields unless you actually need the control.

When Should I Use It?

Always!
Encapsulation is a fundamental principle of OOP. Apply it to every class you design.
Data with invariants
If your data must satisfy certain rules, encapsulate it with validation.
Complex state
When changing one field affects others, encapsulate the logic in methods.
Changing implementation
Encapsulation lets you change internals without breaking callers.

Mental Model

Encapsulation = protect your data, expose only behaviour
Private fields = the internal organs of the class
Public properties/methods = the hands that interact with the outside
Validation = the security guard at the door
Computed properties = derived information without storing it
Invariants = rules that must always hold true

Remember:
· Keep fields private (or protected for inheritance).
· Expose data via properties or methods.
· Validate inputs in setters/methods.
· Don't expose mutable collections directly — use IReadOnlyList or provide methods.
· Encapsulation is not just about hiding — it's about controlling how state changes.

Key Takeaway


Check Your Understanding

You've seen how encapsulation protects data and provides controlled access. Let's test your understanding.

1. Which of the following best describes encapsulation?

Show answer

Correct: B

Why B is correct: Encapsulation is the bundling of data (fields) and behaviour (methods) into a single unit (a class), and controlling access to that data from outside. It's about hiding implementation details and exposing a controlled interface.

Why A is incorrect: Making fields public breaks encapsulation because it exposes internal state directly.

Why C is incorrect: Inheritance is a separate OOP concept (the "is-a" relationship).

Why D is incorrect: Method overloading is a way to provide multiple methods with the same name but different parameters; it's not encapsulation.

Reinforcement: Encapsulation = data hiding + controlled access.

2. Why is it important to use properties instead of public fields?

Show answer

Correct: B

Why B is correct: Properties are a controlled access point. You can add validation, raise events, log changes, or compute values in the getter/setter. If you use a public field and later need to add such logic, you break all calling code. With a property, you can change the implementation without affecting callers.

Why A is incorrect: Properties have a slight method-call overhead, though the JIT often inlines them, making them almost as fast as fields.

Why C is incorrect: Properties don't automatically provide thread safety; you'd need locks or other synchronization.

Why D is incorrect: Properties are supported in both classes and structs.

Reinforcement: Properties provide encapsulation by adding a layer of control over data access.

3. Given the following class, what is the proper way to encapsulate the _balance field so that it cannot be set to a negative value?

public class BankAccount { private decimal _balance; // add code here }
Show answer

Correct: C

Why C is correct: This is a full property with a getter that returns the private field and a setter that validates the new value. If the value is negative, it throws an exception, preventing an invalid state.

Why A is incorrect: This is an auto-property; it doesn't use the existing _balance field and doesn't include validation.

Why B is incorrect: This uses a method, not a property, which is less idiomatic for simple get/set. It also doesn't follow the property pattern.

Why D is incorrect: A private setter prevents external modification, but it doesn't validate the value when set from inside the class.

Reinforcement: Use a full property with validation in the setter to enforce invariants.

4. What is the benefit of exposing a collection as IReadOnlyList<T> instead of List<T>?

Show answer

Correct: B

Why B is correct: IReadOnlyList exposes only read operations (Count, indexer, enumeration). The caller cannot add, remove, or modify items, preserving the object's control over its internal collection. This is a key encapsulation practice.

Why A is incorrect: It's the opposite — the caller cannot modify it.

Why C is incorrect: Performance is usually similar; the benefit is about control, not speed.

Why D is incorrect: It doesn't add thread-safety; it only prevents modifications.

Reinforcement: Expose collections as read-only to maintain encapsulation.

5. Which of the following is a sign that encapsulation might be violated?

Show answer

Correct: C

Why C is correct: A public field allows external code to change the internal state without any control or validation. This breaks encapsulation because the class no longer controls its own state.

Why A is incorrect: Having many private methods is a sign of good encapsulation — internal logic is hidden.

Why B is incorrect: A property that returns a private field value is a controlled way to expose data; it's part of encapsulation.

Why D is incorrect: Constructor injection is a common pattern for dependency injection, which often enhances encapsulation by making dependencies explicit.

Reinforcement: Public fields are an encapsulation antipattern. Always use properties or methods.

You now have a solid understanding of encapsulation — the cornerstone of object-oriented design that keeps your code safe, maintainable, and easy to reason about!


dotnetmadeeasy.com — Learn C# and .NET, the right way.