Skip to main content
CleanCodeMastery

Extract Class: Give an Overworked Class a Helping Partner

Learn the Extract Class refactoring with a fun school office story. Split one overloaded class into two focused classes, each with a single clear job to do.

23 min read Updated June 11, 2026beginner
refactoringsmoving-featuresextract-classlarge-classsingle-responsibilitytypescriptcsharp

🏫 The school office that did everything

Sunrise Public School has one office room, and inside it sits Mrs. D'Souza — the most overworked person in the whole building.

In the morning, parents queue at her window for admissions: forms, birth certificates, interview dates. Behind them queue other parents waving fee challans: tuition fees, bus fees, late fines, refund requests. The phone rings — it is the bank asking about a cheque. A teacher walks in needing a salary slip. The peon, Raghu, brings the electricity bill. All of it lands on one desk, in front of one person, in one room.

The result? Everything is slow, and everything is mixed up. An admission form gets stapled to a fee receipt. A parent asking a two-minute admission question waits behind a thirty-minute fee dispute. Mrs. D'Souza switches between the admission register and the fee ledger forty times a day, and every switch costs her a minute of "now where was I?". When the auditor comes to check the accounts, the whole office shuts down — admissions and all — because the money papers and the admission papers live in the same cupboards. One room, two completely different jobs, total confusion.

Watch her actual day with a stopwatch, the way the school management finally did:

Figure 1: A stopwatch on Mrs. D'Souza's day — one desk, several unrelated jobs fighting for it

That last slice is the silent killer. Eight percent of a working day lost not to work, but to switching between kinds of work — the human version of a class whose methods keep tripping over each other's fields.

The school management finally does the wise thing. They open a brand-new Accounts Office down the corridor, Room 12, and hire a dedicated accounts clerk, Mr. Fernandes. All money matters move there: the fee registers, the challan books, the cash box. Mrs. D'Souza's office keeps admissions, records, and general enquiries. A new signboard goes up: "For fees, please visit the Accounts Office, Room 12."

Within a week, both offices run smoothly. Fee parents go straight to Room 12. Admission parents finish in minutes. The auditor checks Mr. Fernandes's books without stopping admissions for even an hour. Each office now has one clear job — and each can improve, reorganise, even hire help, without disturbing the other.

Figure 2: The split as everyone lived it — chaos in one room, calm in two

In code, we constantly meet classes like the old school office: one class quietly doing two jobs. The fix is the management's fix — open a new office and shift one job there completely. That refactoring is called Extract Class.

🤔 What is Extract Class?

Extract Class is a refactoring where we split one class that carries two (or more) responsibilities into two classes, each carrying one. It is described in Martin Fowler's Refactoring catalog, and it is the standard answer to a class that has grown too big and "does too many things".

The recipe in one breath:

  1. Find the cluster — the group of fields and methods inside the big class that together describe a separate concept.
  2. Create a new, empty class named after that concept.
  3. Carry the cluster over, piece by piece, using Move Field and Move Method.
  4. Let the old class hold a reference to the new one.

As with every refactoring, behaviour does not change. The program gives the same answers; only the org chart of the code is reorganised. The school still admits the same students and collects the same fees — it just stopped doing both at one desk.

Why is one-class-one-job worth this effort? It is the famous Single Responsibility Principle: a class should have only one reason to change. The old school office had two reasons to change — admission rules and money rules — so a change in either disturbed both. After the split, fee rule changes touch only the Accounts Office. In code terms: smaller classes are easier to read top to bottom, easier to test in isolation, and safer to modify, because the blast radius of any change is one focused class.

There is a quiet, beautiful side effect too. Extract Class often discovers a concept that was hiding in plain sight. Three fields named phoneAreaCode, phoneNumber, phoneExtension were never really three fields — they were one PhoneNumber object waiting for a birth certificate. Extraction gives the hidden idea a name, and named ideas can be reused everywhere.

Figure 3: Extract Class at a glance — the signs that demand it, the steps that perform it, and its mirror twin
💡

The easiest way to find the cluster: play the "who talks to whom" game. For each method in the big class, write down which fields it touches. You will usually see two camps — methods that touch fields a, b, c and methods that touch fields x, y, z — with very little crossing between camps. That dividing line is your extraction seam. A shared name prefix (phone-, address-, fee-) is the same clue in word form.

College corner: the "who talks to whom" game has a formal name — it is exactly what the LCOM family of metrics computes. LCOM4, the most practical variant, builds a graph whose nodes are the class's methods and fields, with an edge wherever a method uses a field or calls a sibling method, and then counts the connected components. One component means one cohesive class. Two components means two classes wearing one coat — and the components themselves tell you precisely which members belong to which new class. Tools like NDepend, SonarQube, and JDeodorant compute this for you; JDeodorant even proposes complete Extract Class refactorings by clustering the dependency graph. When your professor says "a class should have high cohesion", LCOM4 equal to one is what that sentence looks like as a number.

📋 When do we need it?

Watch for these signals:

1. Large Class — the classic customer. A class with dozens of fields and methods is almost never one idea; it is several ideas tangled together. Large Class is the smell, Extract Class is the surgery. The school office did not need a faster clerk — it needed to become two offices.

2. Divergent Change. You edit the same class this week for fee rules, next week for admission rules, the week after for report formatting. One class, many unrelated reasons to change, means several responsibilities are sharing one body. Splitting them gives each reason its own home.

3. Data Clumps. The same trio of fields keeps appearing together — in this class, in that method's parameters, in another class again. A group of data that always travels together is begging to become its own class. Extract it once, and every place that used the clump can hold one neat object instead.

4. Half a class is envious. Sometimes you run Move Method to cure Feature Envy and discover that five methods all want to move to the same place — but that place does not exist yet. When many members share a destination that has no name, the destination should be a new class. Extract Class creates it.

5. You cannot test one part without the other. If testing the fee calculation forces you to build admission data too, the two jobs are tangled. After extraction, each class gets its own small, fast tests.

Here is the "who talks to whom" game played on the school office class you will meet below, as a table:

MethodadmissionsfeesPaidfeePerTermlateFinePerDay
admitStudent
isAdmitted
collectFee
feeDue
lateFine

Two camps, zero crossing. The top camp is the admissions office; the bottom camp is Room 12 waiting to be opened.

When should you not extract?

  • No real second responsibility. If you cannot name the new class with a crisp noun, you may be slicing one idea in half. A forced extraction creates a Lazy Class — and the cure for that is the exact inverse refactoring, Inline Class. We will return to this seesaw in the benefits section.
  • The two halves must constantly share state. If the new class and the old one need to pass data back and forth in every operation, you probably cut along the wrong seam.
  • The cluster is tiny. Two fields and one method may not justify a new file and a new name yet. Wait until the concept proves itself.

The decision in picture form — the two questions are how separable the cluster is, and how heavy it is:

Figure 4: Extract or wait — a heavy, cleanly separable cluster is a new office begging to open

👀 Before and after at a glance

A small TypeScript example first. The Teacher class has quietly absorbed a whole telephone-number concern:

// BEFORE — one class, two jobs (teaching info + phone formatting)
class Teacher {
  constructor(
    public name: string,
    public subject: string,
    public phoneStdCode: string,
    public phoneNumber: string,
  ) {}
 
  formattedPhone(): string {
    return `(${this.phoneStdCode}) ${this.phoneNumber}`;
  }
 
  isMumbaiNumber(): boolean {
    return this.phoneStdCode === "022";
  }
}

phoneStdCode, phoneNumber, formattedPhone, isMumbaiNumber — that cluster is not about teachers at all. It is about phone numbers. Give it its own class:

// AFTER — each class has one clear job
class PhoneNumber {
  constructor(
    public stdCode: string,
    public number: string,
  ) {}
 
  formatted(): string {
    return `(${this.stdCode}) ${this.number}`;
  }
 
  isMumbai(): boolean {
    return this.stdCode === "022";
  }
}
 
class Teacher {
  constructor(
    public name: string,
    public subject: string,
    public phone: PhoneNumber,
  ) {}
}

Now PhoneNumber is a real citizen. A Parent class can hold one. A Vendor class can hold one. It can be unit-tested with three tiny tests and reused everywhere — none of which was possible while it hid inside Teacher.

Figure 5: Before — Teacher is two concepts wearing one coat; the phone cluster has no name of its own
Figure 6: After — the hidden concept gets its own class, and Teacher simply holds one

🪜 Step-by-step, the safe way

Extract Class is a composite refactoring — under the hood it is a series of Move Field and Move Method steps. Never do it as one big cut-and-paste. The school did not shut for a week and reopen rearranged; Room 12 opened empty, the ledgers moved one cupboard at a time, and both offices stayed open throughout. Here is the safe sequence.

Figure 7: The extraction pipeline — every stage compiles, passes tests, and keeps both offices open

Step 1 — Name the concept. Decide what the cluster is and give it a crisp noun: PhoneNumber, AccountsOffice, DiscountPolicy. If you cannot find a noun, stop — there may be no real second class here.

Step 2 — Create an empty new class, and link it. Add the new class with nothing inside, and give the old class a field holding an instance:

// INTERMEDIATE STATE 1 — empty new class, linked but unused
class PhoneNumber {}
 
class Teacher {
  private _phone = new PhoneNumber(); // link in place, nothing moved yet
  // ...all old fields and methods still here...
}

Compile and test. Nothing changed behaviourally — we only laid a foundation stone. Room 12 exists; it is just empty.

Step 3 — Move the fields, one at a time. Use the Move Field procedure for each field in the cluster. After moving phoneStdCode, the intermediate state looks like this:

// INTERMEDIATE STATE 2 — one field moved, old accessor delegates
class PhoneNumber {
  stdCode = "";
}
 
class Teacher {
  private _phone = new PhoneNumber();
 
  get phoneStdCode(): string {
    return this._phone.stdCode; // temporary delegation
  }
  set phoneStdCode(v: string) {
    this._phone.stdCode = v;
  }
  // phoneNumber, formattedPhone(), isMumbaiNumber() still here
}

Compile and test after each field. Then move the next one.

Step 4 — Move the methods. Now use Move Method for each method in the cluster, starting with the ones that have the fewest dependencies. formattedPhone becomes PhoneNumber.formatted(), and the old name briefly survives as a delegator.

Step 5 — Clean both interfaces. Review what each class now exposes. Delete the temporary delegating accessors on Teacher if callers can be updated, or keep a thin façade if hundreds of callers depend on the old names. Make the new class's internals as private as possible.

Step 6 — Decide exposure and ownership. Will callers see the new object (teacher.phone.formatted()) or only the old class (teacher.formattedPhone() forwarding inside)? Also decide: is the PhoneNumber owned by one teacher (a value, copy it freely) or shared between objects (a reference, be careful)? Value semantics avoid nasty surprises where changing one teacher's phone silently changes another's.

The whole split, as the states the big class passes through:

Figure 8: The lifecycle of a splitting class — from overloaded to settled, one safe state at a time
⚠️

Run the full test suite after every move — every field, every method. Extract Class touches many lines across two classes, and the most common mistake is a half-moved cluster: the field moved but one method still reads a stale copy through an old path. Tests catch this immediately when you move in small steps, and almost never when you move everything in one heroic commit.

🏢 A bigger real-life example

Now the full school office story in TypeScript. Here is the overworked office, doing admissions and accounts in one class:

// BEFORE — one office, every queue
class SchoolOffice {
  private admissions: string[] = [];
  private feesPaid = new Map<string, number>();
  private feePerTerm = 5000;
  private lateFinePerDay = 20;
 
  admitStudent(name: string): string {
    this.admissions.push(name);
    return `${name} admitted. Welcome to Sunrise Public School!`;
  }
 
  isAdmitted(name: string): boolean {
    return this.admissions.includes(name);
  }
 
  collectFee(name: string, amount: number): void {
    const paid = this.feesPaid.get(name) ?? 0;
    this.feesPaid.set(name, paid + amount);
  }
 
  feeDue(name: string): number {
    return this.feePerTerm - (this.feesPaid.get(name) ?? 0);
  }
 
  lateFine(daysLate: number): number {
    return daysLate * this.lateFinePerDay;
  }
}

Play the "who talks to whom" game (or look back at the camp table in the earlier section). admitStudent and isAdmitted touch only admissions. collectFee, feeDue, and lateFine touch only the money fields. Two camps, zero crossing — a perfect seam. In LCOM4 terms, this class is already two connected components pretending to be one. Open the new office:

// AFTER — admissions stay; all money matters move to Room 12
class AccountsOffice {
  private feesPaid = new Map<string, number>();
  private feePerTerm = 5000;
  private lateFinePerDay = 20;
 
  collectFee(name: string, amount: number): void {
    const paid = this.feesPaid.get(name) ?? 0;
    this.feesPaid.set(name, paid + amount);
  }
 
  feeDue(name: string): number {
    return this.feePerTerm - (this.feesPaid.get(name) ?? 0);
  }
 
  lateFine(daysLate: number): number {
    return daysLate * this.lateFinePerDay;
  }
}
 
class SchoolOffice {
  private admissions: string[] = [];
  readonly accounts = new AccountsOffice(); // the signboard to Room 12
 
  admitStudent(name: string): string {
    this.admissions.push(name);
    return `${name} admitted. Welcome to Sunrise Public School!`;
  }
 
  isAdmitted(name: string): boolean {
    return this.admissions.includes(name);
  }
}
 
// Callers now go to the right window:
//   office.admitStudent("Meera");
//   office.accounts.collectFee("Meera", 3000);
//   office.accounts.feeDue("Meera");      // 2000

What did the school gain?

  • The auditor problem is solved. You can now test AccountsOffice completely on its own — no admission data needed. Five small money tests, fast and focused.
  • Each office changes for one reason. New late-fine rule? Only AccountsOffice changes. New admission form? Only SchoolOffice changes. Divergent Change is gone.
  • The concept is reusable. Next year, the school's summer camp can use its own AccountsOffice instance without dragging admissions along.

A parent paying a fee after the split still gets one smooth experience — they just walk through the right door, and the reference field is the signboard that points the way:

Figure 9: A fee payment after the split — the front office forwards money matters to Room 12 through its reference

And the measurement the management cared about — how many unrelated reasons to change each class carries:

Figure 10: Reasons to change per class — the overloaded office carried four; after the split, each office carries its own

College corner: Extract Class is the refactoring most directly tied to the Single Responsibility Principle, but the deeper measurement is the pairing of cohesion and coupling. A good extraction makes intra-class cohesion rise (each new class's LCOM4 drops to one) while inter-class coupling stays low (the two classes touch through one narrow reference, like the single signboard to Room 12). A bad extraction — cutting along the wrong seam — shows up immediately in the numbers: cohesion barely improves and coupling explodes, because the two halves now chatter constantly through getters. Researchers call the good outcome "high cohesion, low coupling", and it has been the north star of modular design since Parnas's 1972 paper on decomposition criteria: decompose around things that change together, not around steps of a flowchart.

💻 The same refactoring in C# and Python

The identical move in C#, using the employee-and-phone shape that Fowler made famous:

// BEFORE
class Employee
{
    public string Name { get; set; }
    public decimal Salary { get; set; }
 
    public string PhoneStdCode { get; set; }
    public string PhoneNumber { get; set; }
 
    public string FormattedPhone() => $"({PhoneStdCode}) {PhoneNumber}";
    public bool IsTollFree() => PhoneStdCode is "1800" or "1860";
}
// AFTER
class PhoneNumber
{
    public string StdCode { get; set; }
    public string Number { get; set; }
 
    public string Formatted() => $"({StdCode}) {Number}";
    public bool IsTollFree() => StdCode is "1800" or "1860";
}
 
class Employee
{
    public string Name { get; set; }
    public decimal Salary { get; set; }
    public PhoneNumber Phone { get; set; } = new();
}

Employee is back to being about employment. PhoneNumber can be held by Customer, Supplier, anyone — and it can grow proper validation in one place. In C# you might even make it a record to get value semantics for free, which answers the ownership question (copies, not shared references) neatly.

A Python flavour of the same split, in miniature — note how the extracted class immediately becomes testable on its own:

# AFTER the extraction, in Python
class PhoneNumber:
    def __init__(self, std_code, number):
        self.std_code = std_code
        self.number = number
 
    def formatted(self):
        return f"({self.std_code}) {self.number}"
 
    def is_mumbai(self):
        return self.std_code == "022"
 
class Teacher:
    def __init__(self, name, subject, phone):
        self.name = name
        self.subject = subject
        self.phone = phone  # holds one PhoneNumber
 
# a three-line unit test, impossible while the cluster hid inside Teacher:
assert PhoneNumber("022", "23456789").is_mumbai()

🧰 IDE support

Extract Class has strong tooling, especially in the JetBrains family:

ToolSupportHow
JetBrains Rider (C#)FullRefactor → Extract Class: tick fields and methods, Rider creates the class and the reference field
IntelliJ IDEA (Java/Kotlin)FullExtract Delegate moves selected members into a new class and wires up delegation
ReSharper in Visual StudioFullSame Extract Class dialog inside Visual Studio
Visual Studio (plain)PartialExtract Interface / Extract Base Class only; member extraction is the manual safe sequence
VS Code (TypeScript)ManualNo automated Extract Class; "Find All References" guides each Move Field / Move Method

Even with a one-click tool, review what the IDE produced: tools move members correctly, but only you can decide good names, visibility, and whether the new object should be exposed or hidden.

⚖️ Benefits and risks

Benefit ✅Risk / cost ⚠️
ResponsibilityEach class gets one job and one reason to change (SRP)Cutting along the wrong seam leaves two classes chattering constantly
TestingThe new class is unit-testable alone, with small fast testsTwo classes mean slightly more setup wiring in some tests
ReuseThe hidden concept gets a name and can be held by other classesA badly named class spreads confusion everywhere it is reused
ReadabilitySmaller files, clearer storiesOne more indirection hop for readers; one more file to open
EncapsulationThe new class hides its internals behind a focused interfaceOwnership questions appear: is the new object shared or owned? Decide deliberately

The seesaw: Extract Class ↔ Inline Class. These two refactorings are exact inverses, like a seesaw with "too much responsibility" on one side and "too little" on the other. A class doing two jobs? Push the seesaw down with Extract Class. An extracted class that never grew a real job — just a field and a forwarding method? It has become a Lazy Class; ride the seesaw back with Inline Class. This is not failure — it is normal design breathing. Requirements change, classes grow and shrink, and a healthy codebase rides the seesaw in both directions over the years. The skill is noticing which way the seesaw is tilted today. The school knows this too: if fee collection ever moves fully online and Room 12 shrinks to a cupboard, Mr. Fernandes's desk will quietly merge back into the front office — and that will be the right decision then, just as the split is the right decision now.

🧪 Which smells does it cure?

SmellHow Extract Class helps
Large ClassThe main cure — the oversized class hands a whole responsibility to a new class
Divergent ChangeEach reason to change now has its own class to change in
Data ClumpsThe travelling group of fields becomes one named object
Feature EnvyWhen many methods envy a destination that does not exist yet, extraction creates it
Primitive ObsessionLoose primitives (stdCode + number) combine into a real type like PhoneNumber

📦 Quick revision box

+--------------------------------------------------------------+
|                   EXTRACT CLASS — CHEAT CARD                 |
+--------------------------------------------------------------+
| Story    : one overworked school office -> open a new        |
|            Accounts Office for all money matters             |
| Problem  : one class doing TWO jobs (two reasons to change)  |
| Detect   : field/method clusters; shared name prefixes;      |
|            "who talks to whom" shows two camps (LCOM4 > 1)   |
| Fix      : name the concept -> empty class + reference ->    |
|            Move Field x N -> Move Method x N -> clean up     |
| Rule     : one class, one job, one reason to change (SRP)    |
| Test     : full suite after EVERY field/method move          |
| Inverse  : Inline Class (the other end of the seesaw)        |
| Don't    : extract with no crisp noun; create a Lazy Class   |
+--------------------------------------------------------------+

✏️ Practice exercise

Here is a class secretly doing two jobs. Your mission: separate them.

class LibraryDesk {
  private issuedBooks = new Map<string, string>(); // member -> book
  private memberPhoneStd = new Map<string, string>();
  private memberPhoneNumber = new Map<string, string>();
 
  issueBook(member: string, book: string): void {
    this.issuedBooks.set(member, book);
  }
 
  bookWith(member: string): string | undefined {
    return this.issuedBooks.get(member);
  }
 
  setPhone(member: string, std: string, num: string): void {
    this.memberPhoneStd.set(member, std);
    this.memberPhoneNumber.set(member, num);
  }
 
  formattedPhone(member: string): string {
    return `(${this.memberPhoneStd.get(member)}) ${this.memberPhoneNumber.get(member)}`;
  }
}

Your tasks:

  1. Play the "who talks to whom" game on paper. Which methods touch which fields? Draw the two camps as a table like the one in this article, or — college students — as an LCOM4 graph with its connected components.
  2. Name the hidden concept. (Hint: two maps with a shared prefix are really one map holding one object.)
  3. Perform the extraction in safe steps: create the new class empty, move the phone data, move setPhone and formattedPhone, testing after each move. Write a tiny test first: set phone ("011", "23456789") and expect "(011) 23456789".
  4. Decide: should callers see the new class directly, or should LibraryDesk keep delegating methods? Write one sentence defending your choice.
  5. Plot your extraction on the Figure 4 quadrant: how separable was the cluster, and how heavy? Did it land in "Extract now"?
  6. Bonus seesaw question: imagine that a year later, the extracted class still has only a constructor and one getter — nothing grew. Which refactoring undoes your work, and why is undoing it the right move rather than a defeat?

If you answered "Inline Class — because a class that carries no real responsibility costs more than it gives", you have understood both ends of the seesaw. Mrs. D'Souza and Mr. Fernandes would both nod: open the office when the queue demands it, close it when the queue disappears, and never keep a room just for sentiment.

Frequently asked questions

What is the Extract Class refactoring in simple words?
Extract Class means splitting one class that is doing two jobs into two classes, each doing one job. You spot a cluster of fields and methods inside the big class that really describe a separate concept, create a new class for that concept, and move the cluster there using Move Field and Move Method. The old class keeps a reference to the new one.
How do I know a class needs Extract Class?
Look for a cluster: a group of fields that are always used together, and methods that touch only that group. Another sign is a shared name prefix like phoneAreaCode, phoneNumber, phoneExtension — the prefix is the hidden class shouting its own name. Also ask: does this class have two different reasons to change? If yes, it is two classes living in one file.
Which code smells does Extract Class fix?
It is the main cure for Large Class — a class that has accumulated too many responsibilities. It also fixes Divergent Change (one class changing for many different reasons) and Data Clumps (the same group of fields appearing together again and again).
What is the relationship between Extract Class and Inline Class?
They are exact inverses — two ends of one seesaw. Extract Class splits a class that does too much; Inline Class merges away a class that does too little. If you extract too eagerly and the new class never grows a real job, you ride the seesaw back with Inline Class. Good design keeps each class carrying a worthwhile amount of responsibility.
Should the new extracted class be visible to outside callers?
You choose. You can expose it (callers write employee.phone.formatted()) or hide it behind delegating methods on the old class (callers still write employee.formattedPhone(), which forwards inside). Hiding keeps the old public interface stable, which is gentler on a big codebase; exposing is more direct. Decide based on how many callers you would otherwise break.

Further reading

Related Lessons