Skip to main content
CleanCodeMastery

Encapsulate Field: Let the Object Guard Its Own Data

Encapsulate Field explained simply — why public fields let any code corrupt an object's data, and how private fields with getters and setters put the object back in charge.

25 min read Updated June 11, 2026beginner
refactoringencapsulate fieldencapsulationgetters and setterspropertiesdata classtypescriptcsharp

🏪 The Shop Where Customers Serve Themselves

Imagine a small kirana shop in Jaipur. Joshi ji, the shopkeeper, stands behind the counter. Behind him are shelves of biscuits, soap, oil, and a locked drawer of cash. His nephew Montu helps on weekends and dreams of "modernising" everything.

One Sunday, Montu has a brainwave. "Chacha ji, why do you stand here all day? Let customers serve themselves! No counter, no waiting. Maximum efficiency!" Joshi ji is tired, so he agrees to try it for one day.

By ten in the morning, the experiment is already going wrong. There is no counter, so customers walk straight to the shelves and reach for whatever they want. Golu, a six-year-old from the next lane, reaches the toffee jar and takes ten — he leaves one rupee, which covers exactly two. Mrs. Iyer wants change for a hundred, so she opens the cash drawer herself and "borrows" — she is honest, but she counts wrong. Someone puts a soap packet back on the biscuit shelf. A customer takes oil, fully intending to pay later, and forgets by the time he reaches home.

By evening, the drawer has the wrong amount in it, the stock register matches nothing on the shelves, and Joshi ji cannot say what went wrong, when, or who did it — because everything happened everywhere, all day, with no single point where the shop's rules could be applied. He owns the shop, but he controlled nothing in it.

Notice the most important thing about this disaster: nobody was a thief. Golu cannot count. Mrs. Iyer miscounted. The oil customer forgot. The shop was not robbed; it was corrupted by honest people making small mistakes — because the design of the shop gave their mistakes direct access to the shelves and the drawer.

The real shop works differently. There is a counter, and everything passes over it. You ask Joshi ji for a kilo of sugar. He weighs it, checks the price, takes the money, gives change, and notes it in his ledger. If Golu asks for fifty toffees on credit, Joshi ji can smile and say no. The shopkeeper applies the shop's rules to every single transaction, because every transaction goes through him.

Figure 1: One day at the shop — Montu's no-counter experiment versus the normal counter

In code, a public field is the shop with no counter. Any line of code anywhere in the program can reach in and change the value — no rules, no record, no chance to refuse nonsense. The refactoring that builds the counter is called Encapsulate Field: make the field private, and let all outside access pass through methods that the object controls.

🤔 What is Encapsulate Field?

Encapsulate Field means taking a field that is declared public and (1) making it private, then (2) providing a getter method to read it and, if writing is allowed at all, a setter method to change it. From then on, outside code cannot touch the raw data; it can only ask the object, the way customers ask the shopkeeper.

Why does this matter so much? One of the deepest ideas of object-oriented programming is that an object should own its data together with the operations on that data. A public field breaks this ownership in half: the data is exposed naked, while the rules that should protect it have nowhere to live. With the field public, the class can never promise anything about its own state. It cannot say "temperature is always between 5 and 35" or "balance never goes negative", because any stranger can falsify the promise with one assignment.

Once access goes through methods, the class gets a single checkpoint where it can:

  • validate ("reject a thermostat target of 95°C"),
  • react ("when the target changes, signal the heater"),
  • log or audit ("note who changed the price and when"),
  • change its internal storage later ("store temperature in Kelvin internally") without breaking a single caller.
Figure 2: A public field accepts anything from anywhere; a guarded field refuses nonsense at one checkpoint
💡

A simple way to remember the rule: data is like the cash drawer; methods are like the shopkeeper. Customers never open the drawer — they hand money to the shopkeeper, who follows the shop's rules. If you ever see code writing directly into another object's field, picture a stranger's hand inside Joshi ji's cash drawer.

A cousin of this refactoring is Self-Encapsulate Field, where even the class's own methods use the accessors instead of the raw field. Martin Fowler writes on his bliki that he reaches for self-encapsulation only when there is a concrete need, such as lazy initialisation of the value. For today, our focus is the more common and more urgent case: stopping outsiders from grabbing the field.

College corner: the formal name for a promise like "balance never goes negative" is a class invariant — a condition that must hold before and after every public operation on the object. Encapsulation is what makes invariants enforceable: if every mutation passes through methods the class controls, the class can re-establish the invariant on every exit. With a public field, the set of operations that can mutate state is effectively "every line in the program", and no invariant can survive that. This is also why debugging corrupted state in encapsulated code is tractable: the suspect list shrinks from the whole codebase to a handful of methods.

🚨 When do we need it?

Look for these warning signs:

  1. A public, mutable field on a class with rules. If the field has any rule at all about valid values — a percentage between 0 and 100, a non-negative stock count, a six-digit PIN — then a public field means that rule is unenforceable.
  2. Scattered validation. You find the same check (if (price < 0) throw ...) copy-pasted in five different callers before they write to the field. The rule is begging to live in one place: a setter.
  3. Mysterious corrupted state. A bug report says "balance became negative, no idea where". With a public field, every line in the program is a suspect. With a setter, you put one breakpoint in it and catch the culprit on the next run.
  4. The Data Class smell. A class that is nothing but public fields — no behaviour, no protection — is a Data Class. Encapsulating its fields is the first step in giving it a spine.
  5. You need read-only data. A bare public field cannot say "readable by all, writable by none". A getter without a setter expresses exactly that.

When is it not needed? For a pure immutable data carrier — a record, a DTO, a configuration struct with no rules — get/set ceremony adds noise without benefit. A readonly field or a record type says it better. The shopkeeper is needed where there are rules and money; nobody posts a guard at the free newspaper stand.

When you are unsure whether a particular field is urgent, place it on this map. Two questions: how widely is it read, and is it mutated from outside?

Figure 3: Triage map for fields — mutation from outside is the axis that creates emergencies

The vertical axis matters more than the horizontal one. A field read everywhere but written nowhere is mostly a documentation problem; a field written from many places is Montu's shop at ten in the morning.

👀 Before and after at a glance

Before — the shop with no counter:

// BEFORE: anyone can reach over and change anything
class TiffinAccount {
  public balance = 0;          // wide open
}
 
const account = new TiffinAccount();
 
// Scattered across the codebase, far from the class:
account.balance = account.balance + 500;     // a deposit... probably?
account.balance = -2000;                     // nonsense, silently accepted
account.balance = account.balance - 700;     // overdraw? nobody checks

After — every transaction passes over the counter:

// AFTER: the object guards its own data
class TiffinAccount {
  private _balance = 0;
 
  get balance(): number {                    // read: allowed for everyone
    return this._balance;
  }
 
  deposit(amount: number): void {
    if (amount <= 0) throw new Error("Deposit must be positive");
    this._balance += amount;
  }
 
  withdraw(amount: number): void {
    if (amount <= 0) throw new Error("Withdrawal must be positive");
    if (amount > this._balance) throw new Error("Insufficient balance");
    this._balance -= amount;
  }
}
 
const account = new TiffinAccount();
account.deposit(500);        // ok — shopkeeper accepts
account.withdraw(700);       // refused — Error: Insufficient balance
// account.balance = -2000;  // compile error: no setter, read-only outside

Notice that we did not even write a plain setter. We wrote intention-revealing methodsdeposit and withdraw — because that is what the domain actually does. A raw set balance would still let callers compute the new balance outside the object. The best version of Encapsulate Field often skips the generic setter entirely.

Figure 4: Every request crosses the counter — the object checks rules before touching its own data

Here is the whole shop comparison in one table:

SituationOpen shop (public field)Counter shop (encapsulated)
Who can change the value?Any line of code, anywhereOnly the object's own methods
Where do the rules live?Nowhere enforceableIn one checkpoint, run on every write
A bad value arrivesAccepted silently, found weeks laterRefused immediately, with a clear error
Debugging a corruptionEvery caller is a suspectOne breakpoint in the setter catches the culprit
Changing internal storage laterBreaks every callerInvisible to callers

🪜 Step-by-step, the safe way

Here are the mechanics, in the careful one-step-at-a-time style Fowler teaches. At no point does the program stop compiling.

Step 1: Add a getter and setter beside the public field. Do not touch the field's visibility yet — old direct access and new method access work side by side.

class Thermostat {
  public targetCelsius = 21;                 // still public, for now
 
  getTargetCelsius(): number {
    return this.targetCelsius;
  }
 
  setTargetCelsius(value: number): void {
    this.targetCelsius = value;              // no rules yet — pure pass-through
  }
}

Step 2: Find every reference to the field. Use your IDE's Find Usages. Make a checklist. There may be more callers than you expect — a public field attracts users the way an open sweet box attracts ants.

Step 3: Replace reads with the getter and writes with the setter, one call site at a time. Compile and run the tests after each change. Slow is smooth; smooth is fast.

// before:  display.show(thermostat.targetCelsius);
// after:   display.show(thermostat.getTargetCelsius());
 
// before:  thermostat.targetCelsius = 24;
// after:   thermostat.setTargetCelsius(24);

Step 4: Make the field private. Once Find Usages shows zero external references, flip the modifier:

class Thermostat {
  private targetCelsius = 21;
  // getter and setter as before
}

Compile. If anything breaks, some caller escaped your checklist — the compiler has just found it for you. Fix it and move on.

Step 5: Now add the rules — the actual payoff.

setTargetCelsius(value: number): void {
  if (value < 5 || value > 35) {
    throw new Error("Target must be between 5°C and 35°C");
  }
  this.targetCelsius = value;
}

Step 6: Reassess the setter's name. Would increaseBy(degrees) or enableEcoMode() say what callers really mean? Push towards methods that express intent, not raw assignment.

Figure 5: The field's life from wide open to safe — each transition is one small, testable commit
⚠️

Adding validation (step 5) is the one step that can change behaviour. If some existing code was setting 50°C and "working" (wrongly, but quietly), your new check will now throw. That is usually a hidden bug being exposed, not a new bug being created — but do it consciously. Check what values flow through today, decide whether to throw, clamp, or just log a warning, and keep the mechanical refactoring (steps 1–4) in a separate commit from the behaviour change (step 5). If a bug appears, you will know exactly which commit to blame.

College corner: steps 1–4 and step 5 differ in a way professionals take very seriously. Steps 1–4 are behaviour-preserving transformations — the program does exactly what it did before, which is the strict definition of refactoring. Step 5 is a behaviour change dressed in the same clothes. Mixing them in one commit is how teams end up with the dreaded review comment "this refactoring broke production" — when actually the refactoring was fine and the smuggled-in rule change broke it. Separate commits give you separate blame, separate reverts, and a reviewer who can verify each piece in seconds.

📚 A bigger real-life example

A library in Kochi manages memberships in software. The original class is all public fields — a shop with no counter:

// BEFORE: every field is a self-service shelf
class LibraryMember {
  public name = "";
  public finePaise = 0;          // fine owed, in paise
  public booksIssued = 0;
  public isBlocked = false;
}
 
// Scattered through the codebase:
member.booksIssued = member.booksIssued + 1;   // issue a book... limit? what limit?
member.finePaise = -500;                       // negative fine: the library owes HIM?
member.isBlocked = false;                      // anyone can unblock anyone
member.booksIssued = 99;                       // the rule "max 4 books" lives nowhere

The library has clear rules — at most 4 books at a time, a member with a fine above ₹100 is blocked, fines never go negative — but not one of these rules can be enforced, because the data is wide open. The librarian, Mr. Thomas, files the same bug report every month: "member shows blocked but has no fine", "member has minus five rupees fine", "member somehow has 99 books". The developers close each ticket by fixing the data by hand, because they can never find which code wrote the bad value. Sound familiar? It is Joshi ji at closing time, staring at a drawer that does not match the ledger.

After Encapsulate Field:

// AFTER: rules live where the data lives
class LibraryMember {
  private static readonly MAX_BOOKS = 4;
  private static readonly BLOCK_LIMIT_PAISE = 100 * 100;   // ₹100
 
  private _finePaise = 0;
  private _booksIssued = 0;
 
  constructor(public readonly name: string) {}
 
  get finePaise(): number { return this._finePaise; }
  get booksIssued(): number { return this._booksIssued; }
 
  // derived, always correct, cannot be set from outside
  get isBlocked(): boolean {
    return this._finePaise > LibraryMember.BLOCK_LIMIT_PAISE;
  }
 
  issueBook(): void {
    if (this.isBlocked) throw new Error(`${this.name} is blocked: pay fine first`);
    if (this._booksIssued >= LibraryMember.MAX_BOOKS) {
      throw new Error("Book limit reached (4)");
    }
    this._booksIssued++;
  }
 
  returnBook(daysLate: number): void {
    if (this._booksIssued === 0) throw new Error("No books to return");
    this._booksIssued--;
    if (daysLate > 0) this._finePaise += daysLate * 200;   // ₹2 per late day
  }
 
  payFine(paise: number): void {
    if (paise <= 0) throw new Error("Payment must be positive");
    this._finePaise = Math.max(0, this._finePaise - paise);  // never negative
  }
}

Walk through the improvements:

  • isBlocked is no longer a stored field anyone can flip — it is derived from the fine. It can never disagree with reality, and "anyone can unblock anyone" is now impossible.
  • issueBook() enforces the 4-book limit in exactly one place. Before, the limit's only hope was every caller remembering it.
  • payFine() clamps at zero, so a negative fine is unrepresentable.
  • Reads (member.booksIssued) still look clean, thanks to getters — callers lost nothing they legitimately needed.
Figure 6: The library member after the refactoring — state is private, behaviour is the only door

When Mr. Thomas's team finally investigated the old bug tickets, they tried to answer one simple question for each corrupted member record: which part of the code wrote the bad value? The results were humbling:

Figure 7: Source of bad writes into the open fields — the biggest slice is the scariest one

A quarter of the corruptions could never be traced at all. That is the signature of a public field: not just bugs, but unattributable bugs. After encapsulation, the team put one breakpoint inside payFine and one inside issueBook and caught the next bad caller in a single afternoon. Their bug tracker told the rest of the story:

Figure 8: Corrupted-state tickets per month at the Kochi library, before and after encapsulation

The one remaining ticket? A rule that was wrong, not a rule that was bypassed — the fine rate had been entered as ₹2 per day when the board had approved ₹1. The fix was one line, in one place, because the rule lived in one place. That is the quiet, long-term payoff of this refactoring: even your future mistakes become cheap.

🐍 A quick look in Python

Python has no private keyword, but it has something culturally equivalent: a naming convention plus the @property decorator. The beautiful part is that callers keep plain attribute syntax while your checks run underneath:

class Thermostat:
    def __init__(self) -> None:
        self._target_celsius = 21.0      # leading underscore: internal, please knock
 
    @property
    def target_celsius(self) -> float:
        return self._target_celsius
 
    @target_celsius.setter
    def target_celsius(self, value: float) -> None:
        if not 5.0 <= value <= 35.0:
            raise ValueError("Target must be between 5 and 35 degrees")
        self._target_celsius = value
 
t = Thermostat()
t.target_celsius = 24      # looks like a field write — setter runs the check
t.target_celsius = 95      # ValueError: Target must be between 5 and 35 degrees

This is the same trick C# properties perform: the syntax of a field with the power of a method. A Python class can even start life with a plain attribute and grow a @property later without changing a single caller — which means in Python, you can delay this refactoring safely and apply it the day the first rule appears.

🟦 The same refactoring in C#

C# is the most comfortable language in the world for this refactoring, because properties are built into the language. A property looks like a field to callers but runs code underneath — getter and setter logic with field-access syntax.

Before:

// BEFORE
public class Thermostat
{
    public double TargetCelsius;   // public field: no rules possible
}
 
// anywhere:
thermostat.TargetCelsius = 95.0;   // accepted, sadly

After, using a property with a private backing field:

// AFTER
public class Thermostat
{
    private double _targetCelsius = 21.0;
 
    public double TargetCelsius
    {
        get => _targetCelsius;
        set
        {
            if (value < 5.0 || value > 35.0)
                throw new ArgumentOutOfRangeException(
                    nameof(value), "Target must be between 5°C and 35°C");
            _targetCelsius = value;
        }
    }
}
 
// callers keep the SAME syntax as before:
thermostat.TargetCelsius = 24.0;   // ok — setter ran the check
thermostat.TargetCelsius = 95.0;   // throws ArgumentOutOfRangeException

This is a beautiful C# advantage: because property syntax matches field syntax, call sites do not change at all when you convert a field to a property. (One careful note: it is still a binary breaking change — code compiled against the old field must be recompiled — and you cannot pass a property as a ref argument the way you could a field.)

C# also gives you graded levels of protection, so you can use exactly as much as needed:

public class LibraryMember
{
    // 1. Read anywhere, write only inside this class:
    public int BooksIssued { get; private set; }
 
    // 2. Set once during construction, never again (init-only):
    public string Name { get; init; }
 
    // 3. Derived property — computed, not stored:
    public bool IsBlocked => FinePaise > 10_000;
 
    // 4. Full control with a backing field when validation is needed:
    private int _finePaise;
    public int FinePaise
    {
        get => _finePaise;
        private set => _finePaise = Math.Max(0, value);
    }
 
    public void IssueBook()
    {
        if (IsBlocked) throw new InvalidOperationException("Member is blocked");
        if (BooksIssued >= 4) throw new InvalidOperationException("Limit reached");
        BooksIssued++;
    }
}

{ get; private set; } alone solves most everyday cases: the world may read, only the object may write. That single line is Encapsulate Field distilled into six tokens.

Use this ladder when choosing how much protection a C# member needs:

NeedC# toolOne-line example
Read everywhere, never changes after constructioninit-only propertypublic string Name { get; init; }
Read everywhere, changed only by the object itselfprivate setterpublic int BooksIssued { get; private set; }
Value computed from other stateexpression-bodied propertypublic bool IsBlocked => FinePaise > 10_000;
Every write must be validatedbacking field + full propertyproperty with checks in set
Outsiders express intent, not assignmentsmethod instead of settermember.IssueBook();

🧰 IDE support

This refactoring is so common that IDEs automate it completely:

  • Visual Studio: place the caret on a field and press Ctrl+R, Ctrl+E (or Ctrl+. for Quick Actions) and choose Encapsulate field. Microsoft Learn documents two flavours: encapsulate and update usages (callers switch to the property) or encapsulate but keep direct usages inside the class.
  • JetBrains Rider / ReSharper: caret on the field, Ctrl+Shift+R (Refactor This) or Alt+Enter, then Encapsulate Field. Rider can generate an auto-property or a property with a backing field, updates all usages, and registers everything as one operation — a single Ctrl+Z undoes the whole refactoring.
  • IntelliJ IDEA (Java): menu Refactor → Encapsulate Fields opens a dialog where you tick fields, choose getter/setter names, and decide whether even in-class access should use the accessors (Self-Encapsulate Field as a checkbox!).
  • VS Code (TypeScript): there is no single built-in "encapsulate field" action, but Rename Symbol (F2) plus the compiler make the manual steps quick: rename balance to _balance, mark it private, and the error list becomes your checklist of call sites to migrate.

Automated encapsulation does the mechanical part flawlessly. The thoughtful part — what validation belongs in the setter, whether a setter should exist at all, whether deposit() is the truer name — remains your job, and it is the part that matters.

⚖️ Benefits and risks

BenefitsRisks / Costs
Validation and invariants run on every write — bad values are rejected at the doorFor immutable, rule-free data carriers, accessors are pure ceremony
One breakpoint in the setter finds every write — debugging corrupted state becomes easyHollow get/set pairs can give a false feeling of safety while behaviour still lives outside (Data Class in disguise)
Reads and writes become extension points: lazy loading, change events, audit logsAdding validation to a previously open field may surface (good!) but disrupt (annoying!) existing wrong callers
Internal representation can change without touching callersIn C#, field-to-property is source-compatible but binary-breaking; ref access to the field is lost
Read-only exposure becomes possible (getter without setter)Tiny call overhead in languages without inlining — almost never measurable

🧪 Which smells does it cure?

SmellHow Encapsulate Field helps
Data ClassFirst step of the standard cure: hide the fields, then pull behaviour into the class through the new methods
Inappropriate IntimacyOutsiders can no longer poke around in another class's private parts — the interface becomes the only door
Duplicate CodeValidation copy-pasted before every write collapses into the one setter
Shotgun SurgeryA rule change (new valid range) edits one setter instead of every caller

This smell cluster is closely related to Primitive Obsession too: a public primitive field is doubly exposed — wrong shape and no guard. Often the full cure is Encapsulate Field followed by Replace Data Value with Object.

Here is the whole lesson as one map for revision:

Figure 9: Encapsulate Field on one page

📦 Quick revision box

+================================================================+
|              ENCAPSULATE FIELD — REVISION CARD                 |
+================================================================+
| SMELL SIGN : public mutable field on a class that has rules    |
| PICTURE    : shop with no counter -> strangers in cash drawer  |
+----------------------------------------------------------------+
| THE MOVE   : 1. Add getter + setter (field still public)       |
|              2. Find Usages -> checklist of call sites         |
|              3. Migrate reads/writes one by one (test each)    |
|              4. Make the field PRIVATE                         |
|              5. Add validation / rules in the setter           |
|              6. Rename towards intent: deposit(), issueBook()  |
+----------------------------------------------------------------+
| C# GOLD    : public int X { get; private set; }                |
|              derived value -> computed property, not a field   |
| REMEMBER   : mechanical steps and behaviour change =           |
|              SEPARATE commits                                  |
+================================================================+

✍️ Practice exercise

A cinema booking system in Indore has this class:

class Show {
  public movieName = "";
  public totalSeats = 100;
  public seatsBooked = 0;
  public ticketPriceRupees = 150;
  public isHouseFull = false;
}
 
// Found around the codebase:
show.seatsBooked = show.seatsBooked + 4;        // booking 4 tickets
show.seatsBooked = 130;                          // more than total seats!
show.ticketPriceRupees = -50;                    // negative price
show.isHouseFull = true;                         // set by hand, often forgotten

Your tasks:

  1. Apply Encapsulate Field step by step: accessors first, migrate callers, then make every field private. Keep the code compiling at each step.
  2. Replace the raw seat-setter with an intention-revealing method bookSeats(count: number) that refuses to book more seats than remain.
  3. Turn isHouseFull into a derived getter computed from seatsBooked and totalSeats, so it can never be stale or forgotten.
  4. Add a rule: ticket price must be between ₹50 and ₹500. Where exactly does this rule now live?
  5. Bonus (C#): rewrite the finished class using C# properties — use { get; private set; } for SeatsBooked, an expression-bodied IsHouseFull, and a validating setter for the price.
  6. Reflection question: should there be a public setter for totalSeats at all? What real-world event would change a hall's seat count, and which method name would describe it honestly?

One closing image. At the end of the no-counter day, Montu asked Joshi ji why the shop had failed when every customer was honest. Joshi ji tapped the counter and said, "Beta, this is not here because I distrust people. It is here so that one careless moment — theirs or mine — has one place to get caught." That is encapsulation in a sentence. You are not building walls against villains; you are building one good counter where ordinary mistakes, including your own, can be stopped politely, every single time.

Frequently asked questions

If my getter and setter just read and write the field with no checks, what was the point?
Two points. First, you have bought insurance: tomorrow you can add validation, logging, or a changed internal representation without touching a single caller, because they already go through the methods. Second, even an empty setter is a searchable, breakpoint-able place — you can find every write in one click. But do also ask whether real rules belong there; hollow accessors forever can hide a Data Class smell.
Are getters and setters not considered bad by some people?
What is criticised is the habit of adding a get/set pair for every field automatically, which makes the object a glass box with handles. The deeper goal is 'tell, don't ask' — instead of getting a value, changing it outside, and setting it back, give the object a method like deposit(500) that does the work itself. Encapsulate Field is the first step; intention-revealing methods are the destination.
Do I need this in TypeScript, where I can just mark a field readonly or private?
TypeScript's private and readonly keywords are compile-time guards and are often enough. Encapsulate Field matters when the value must remain changeable but only under rules — then a private field plus a setter (or a method) with validation is exactly the tool. TypeScript also has get and set accessor syntax, so callers can keep property-style syntax while your checks run underneath.
What is the difference between Encapsulate Field and Self-Encapsulate Field?
Encapsulate Field controls access from outside the class: make the field private, force outsiders through accessors. Self-Encapsulate Field goes one step further — even the class's own methods stop touching the field directly and use the accessors instead. Fowler himself says he reaches for self-encapsulation only when there is a concrete reason, such as lazy loading or a subclass needing to override access.
Will all these method calls slow my program down?
Almost never measurably. Modern compilers and runtimes (the JIT in .NET and the JVM, V8 for JavaScript) inline simple accessors, making them as fast as direct field access. Only in extreme inner loops in lower-level languages might it matter — and you would measure first, not guess.

Further reading

Related Lessons