Encapsulation is the bundling of data and the methods that operate on that data, while hiding the internal representation and controlling access to maintain object invariants.

Core Concept

Encapsulation serves two primary purposes:

  1. Information Hiding: Hide internal implementation details from external code
  2. Invariant Protection: Ensure objects can only be in valid states

Why Encapsulation Matters

Protecting Invariants

Without encapsulation, external code can bypass validation and create invalid object states:

// BAD: No encapsulation, invariant can be violated
public class BankAccount
{
    public decimal Balance; // Public field - danger!
}
 
var account = new BankAccount { Balance = 1000 };
account.Balance = -500; // VIOLATION: Negative balance allowed!
// GOOD: Encapsulation protects invariant
public class BankAccount
{
    private decimal _balance; // Hidden representation
    // INVARIANT: _balance >= 0
 
    public BankAccount(decimal initialDeposit)
    {
        if (initialDeposit < 0)
            throw new ArgumentException("Initial deposit must be non-negative");
        _balance = initialDeposit;
    }
 
    public void Withdraw(decimal amount)
    {
        if (_balance - amount < 0)
            throw new InvalidOperationException("Insufficient funds");
        _balance -= amount; // Invariant maintained
    }
 
    public decimal GetBalance() => _balance; // Read-only access
}

Enabling Evolution

Encapsulation allows internal representation changes without breaking clients:

// Version 1: Store temperature in Celsius
public class Temperature
{
    private double _celsius;
 
    public double Fahrenheit => (_celsius * 9/5) + 32;
    public double Celsius => _celsius;
}
 
// Version 2: Changed to store in Kelvin (clients unaffected!)
public class Temperature
{
    private double _kelvin; // Internal representation changed
 
    public double Fahrenheit => ((_kelvin - 273.15) * 9/5) + 32;
    public double Celsius => _kelvin - 273.15;
}

Mechanisms of Encapsulation

1. Access Modifiers

Control visibility of class members:

public class Example
{
    private int _internal;      // Only accessible within this class
    protected int _inherited;   // Accessible in this class and subclasses
    public int External;        // Accessible everywhere
 
    public int GetInternal() => _internal; // Controlled access
}

2. Properties and Methods

Provide controlled access to internal state:

public class Person
{
    private string _name;
    private int _age;
 
    // Property with validation
    public string Name
    {
        get => _name;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Name cannot be empty");
            _name = value;
        }
    }
 
    // Read-only property (computed)
    public bool IsAdult => _age >= 18;
}

3. Immutability

Make objects unchangeable after construction:

public class Money
{
    public decimal Amount { get; }
    public string Currency { get; }
 
    public Money(decimal amount, string currency)
    {
        if (amount < 0)
            throw new ArgumentException("Amount cannot be negative");
        Amount = amount;
        Currency = currency ?? throw new ArgumentNullException(nameof(currency));
    }
 
    // Operations return new instances
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("Currency mismatch");
        return new Money(Amount + other.Amount, Currency);
    }
}

Common Violations

Getters and Setters Without Purpose

Don’t create getters/setters for every field:

// BAD: Pointless encapsulation
public class Person
{
    private string _name;
 
    public string GetName() => _name;
    public void SetName(string value) => _name = value;
}
// This is just a public field with extra steps!

Better approaches:

// GOOD: Just use a public field if no invariant
public class PersonData
{
    public string Name { get; set; }
}
 
// GOOD: Real encapsulation with invariant
public class ValidatedPerson
{
    private string _name;
    // INVARIANT: Name is never null or empty
 
    public ValidatedPerson(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Name required");
        _name = name;
    }
 
    public string Name => _name; // Read-only access
}

Exposing Collections

Don’t expose mutable collections directly:

// BAD: Internal list can be modified
public class Team
{
    public List<Player> Players { get; set; }
}
 
var team = new Team();
team.Players = null; // Can break invariants!
team.Players.Add(invalidPlayer); // No validation!
// GOOD: Controlled access to collection
public class Team
{
    private readonly List<Player> _players = new();
 
    public IReadOnlyCollection<Player> Players => _players.AsReadOnly();
 
    public void AddPlayer(Player player)
    {
        if (player == null)
            throw new ArgumentNullException(nameof(player));
        if (_players.Count >= 11)
            throw new InvalidOperationException("Team is full");
        _players.Add(player);
    }
}

Exposing Internal Objects

Don’t return mutable internal objects:

// BAD: Exposes internal state
public class Document
{
    private Metadata _metadata = new();
 
    public Metadata GetMetadata() => _metadata; // Danger!
}
 
var doc = new Document();
doc.GetMetadata().Author = null; // Violated encapsulation!
// GOOD: Return defensive copy or immutable view
public class Document
{
    private Metadata _metadata = new();
 
    public Metadata GetMetadata() => _metadata.Clone();
    // Or: Return immutable interface
    public IReadOnlyMetadata GetMetadata() => _metadata;
}

Relationship to Other Concepts

Tell, Don’t Ask

Tell, Don’t Ask is the behavioral principle that supports encapsulation:

// BAD: Asking for data to make decisions
if (account.GetBalance() >= amount)
    account.SetBalance(account.GetBalance() - amount);
 
// GOOD: Tell the object what to do
account.Withdraw(amount); // Object maintains its own invariant

Law of Demeter

Law of Demeter prevents breaking encapsulation through chaining:

// BAD: Reaching through multiple objects
customer.GetAddress().GetZipCode();
 
// GOOD: Ask the object directly
customer.GetZipCode();

Single Responsibility

Encapsulation naturally leads to Single Responsibility:

  • Each class protects its own invariant
  • This invariant defines the class’s single reason to change

When to Relax Encapsulation

Data Transfer Objects (DTOs)

Simple data containers with no invariants:

public class UserDTO
{
    public string Username { get; set; }
    public string Email { get; set; }
    public DateTime CreatedAt { get; set; }
}

Value Objects

Immutable objects with simple validation:

public record Email(string Address)
{
    public Email(string address) : this(Validate(address)) { }
 
    private static string Validate(string email)
    {
        if (!email.Contains("@"))
            throw new ArgumentException("Invalid email");
        return email;
    }
}

Resources

See Also