Large Class: The School Bag That Carries Everything
Understand the Large Class code smell — why god classes grow, how to spot low cohesion, and how Extract Class splits them into small, focused classes.
🎯 The School Bag That Weighed More Than the Student
Meet Anaya, a class 7 student at Green Valley School, Nagpur. Her school bag is famous in the whole colony — because it carries everything.
All fourteen textbooks, even on days with only four periods. Three notebooks per subject. Her tiffin box, a water bottle, and an extra packet of biscuits. Her skates for Tuesday's sports class — kept inside all week, "just in case." Her painting kit. Her old test papers from last year. Even the spare umbrella her grandmother insisted on.
Every morning, Anaya groans lifting the bag. When the teacher says, "take out your geometry box," Anaya digs for five whole minutes. Once, her leaking water bottle soaked the maths homework. When her cousin borrowed "just the science book," he had to empty half the bag to find it.
Her mother, Sunita, finally fixed it the sensible way: a small daily bag packed by the timetable, a separate tiffin bag, and a sports kit that goes only on Tuesday. Now each bag is light, each thing is findable, and a leak in one bag cannot ruin another.
Here the story takes a turn. Anaya's elder sister Diya is a second-year CSE student. For a college hackathon, Diya and her teammate Sameer build a "Smart School" app — and Diya, smiling at the family joke, names her main class SchoolBag. Six weeks later, the joke has become real. The SchoolBag class in the app has grown exactly like Anaya's bag: books, tiffin, sports, library fines — everything stuffed into one class. Sameer cannot add a feature without stepping on Diya's code. Their professor, Dr. Rao, takes one look and writes two words on the review sheet: "Large Class."
In code, a Large Class is Anaya's old school bag. One class stuffed with every field and method that ever seemed "related" to it. It works — the bag does carry things — but every search is slow, every change risks soaking something unrelated, and nobody can lift it with confidence. This lesson follows Diya and Sameer as they learn to spot the smell, measure it, and split the bag.
💡 What is this smell?
First, a quick reminder of the golden rule: a code smell is not a bug. The program runs and gives right answers. A smell is a structural warning — like a creaking shelf that still holds the books today. Large Class is one of the "Bloater" smells from Martin Fowler's book Refactoring: code that has swollen so much it is hard to handle.
A Large Class is a class that has collected too many fields, too many methods, and — most importantly — too many responsibilities. It is no longer one "thing." It has become a whole department squeezed into a single name.
The extreme version even has a nickname in the industry: the god object — a class that knows too much and does too much, and which everything else in the program depends on. Legacy systems are famous for them: a Manager or Context class with thousands of lines that every developer fears and every feature touches.
The true measure of this smell is not line count but cohesion — do the parts of the class actually belong together? A 700-line class where everything serves one purpose can be healthier than a 200-line class hiding three unrelated jobs.
This smell is the big sibling of Long Method. A long method does many jobs in one function; a large class does many jobs in one class — and it is usually full of long methods too. Bloaters travel in packs. When Dr. Rao opened Diya's SchoolBag, he found three long methods inside it within a minute.
College corner: The formal name for "one class, one job" is the Single Responsibility Principle (SRP) — Robert C. Martin defines it as "a class should have only one reason to change." Cohesion is measured by metrics like LCOM (Lack of Cohesion of Methods): roughly, count the method pairs that share no fields. A high LCOM says the class is a hostel of strangers, mathematically.
👃 How to spot it
Dr. Rao gives Diya and Sameer his checklist. Open any class in your project and run it:
- The file is hundreds or thousands of lines long; you navigate by Ctrl+F, not by reading.
- The class has many fields, and each method touches only a small, different subset of them.
- The class name is a vague absorber:
Manager,Service,Helper,Util,Engine,Context,Processor. - Field names carry prefixes that mark territories:
tiffinBox,tiffinFresh,sportsShoes,sportsDay— those prefixes are class names trying to escape. - Unrelated reasons make the file change: a UI tweak, a pricing rule, and a logging fix all land in this one class.
- The class's test file is enormous, with painful setup, because creating the object drags in every concern at once.
- Merge conflicts happen here constantly, because everyone on the team edits this same file.
| Symptom | What it tells you |
|---|---|
| Methods use disjoint groups of fields | Several classes are hiding inside one — low cohesion |
Name like Manager or Helper | The class has no single idea, so it got a bucket name |
Prefixed field names (billingX, shippingY) | The author already grouped the concepts informally — make the groups real classes |
| Every feature touches this file | The class has become a god object and a team bottleneck |
| Giant test setup | You cannot create the object without building all its unrelated machinery |
| Same logic appears twice inside the class | The class is so big that even its own author forgot what it contains |
Diya runs the checklist and ticks five boxes. Sameer ticks the sixth — he had quietly re-written a date-formatting helper that already existed 300 lines above, because he never scrolled that far.
⚠️ Why it is a problem
Why should we care, if the bag still carries the books?
- Cognitive overload. Nobody can hold a 2,000-line class in their head. So changes are made with partial understanding, and partial understanding breeds side effects.
- Low cohesion, high coupling. Methods that work on unrelated fields share one namespace and can interfere. Meanwhile, every consumer of the class depends on far more than it actually uses — like carrying skates to a maths exam.
- Team traffic jam. One hot file that everyone edits means endless merge conflicts. The class becomes a bottleneck for parallel work, just like one school counter handling all admissions. Diya and Sameer hit this in week three: every pull request conflicted with the other's.
- Hidden duplication. With so much surface area, the same helper logic gets re-written in two corners of the same class, and nobody notices. Sameer's date helper is the living proof.
- Untestable units. You cannot test the tiffin-freshness logic without also constructing the sports kit and the library records. Tests become heavy, slow, and rare — and rare tests mean hidden bugs.
And like all bloaters, it grows silently. Classes obey a kind of gravity: the more a class already knows, the easier it is to add the next feature there, because all the data is conveniently in scope. Researchers and practitioners call this a rich-get-richer dynamic — well-connected classes attract still more responsibility.
When Dr. Rao asks Diya to count her fields by territory, the result embarrasses her. The class called SchoolBag is barely about bags at all:
Then he shows them the team cost. As the class grows, merge conflicts on the file grow much faster than the line count, because more people need the same file more often:
College corner: This is coupling theory in action. A god object has high afferent coupling (many classes depend on it) and high efferent coupling (it depends on many classes), so it sits in the blast radius of every change in both directions. SRP fixes the cohesion side; dependency inversion and small interfaces fix the coupling side. The two problems almost always arrive together.
🧪 A week in the life of the god class
To make the cost concrete, watch Sameer try to add one small feature — "remind students to return library books" — to the swollen SchoolBag:
One feature, four territories disturbed, one merge conflict, one broken test in an unrelated area. Now look at the same week as an emotional journey:
"Notice," Dr. Rao says, "the actual feature took twenty minutes. The bag cost you two days."
📊 Which classes should be split first?
You cannot split every big class this sprint, and you should not. Dr. Rao teaches the same two-question trick we used for long methods: how incohesive is the class, and how often does it change? Plot your classes and the priorities reveal themselves:
SchoolBag sits deep in "Refactor now": it mixes four jobs and both teammates edit it weekly. The old report generator is just as messy but frozen — it can wait its turn.
🧪 A real-life code example
Let us look at Diya's actual class. The app began with a simple SchoolBag. Two terms (well, six hackathon weeks) later, it looks like this:
class SchoolBag {
// books territory
books: string[] = [];
notebooks: string[] = [];
// tiffin territory
tiffinDish: string = "";
tiffinPackedAt: Date = new Date();
waterBottleFull: boolean = true;
// sports territory
sportsShoes: boolean = false;
skates: boolean = false;
sportsDay: string = "Tuesday";
// library territory
borrowedBook: string = "";
dueDate: Date = new Date();
packForDay(timetable: string[]): void {
this.books = timetable.map((subject) => subject + " textbook");
this.notebooks = timetable.map((subject) => subject + " notebook");
}
isTiffinFresh(now: Date): boolean {
const hours = (now.getTime() - this.tiffinPackedAt.getTime()) / 3600000;
return hours < 6;
}
refillWater(): void {
this.waterBottleFull = true;
}
needsSportsKit(day: string): boolean {
return day === this.sportsDay;
}
packSportsKit(day: string): void {
if (this.needsSportsKit(day)) {
this.sportsShoes = true;
this.skates = true;
}
}
isLibraryBookOverdue(today: Date): boolean {
return this.borrowedBook !== "" && today > this.dueDate;
}
lateFine(today: Date): number {
if (!this.isLibraryBookOverdue(today)) return 0;
const daysLate = Math.ceil(
(today.getTime() - this.dueDate.getTime()) / 86400000,
);
return daysLate * 2; // Rs. 2 per day
}
totalWeightKg(): number {
let weight = this.books.length * 0.5 + this.notebooks.length * 0.2;
if (this.sportsShoes) weight += 0.7;
if (this.skates) weight += 1.5;
if (this.borrowedBook !== "") weight += 0.4;
return weight;
}
}Pause and inspect, the way Dr. Rao made Diya inspect. Do you see the four territories? Books, tiffin, sports, library. The comments confess it, the field prefixes confess it, and the methods confess it too: isTiffinFresh touches only tiffin fields; lateFine touches only library fields. The clusters never talk to each other — they merely share a roof.
Why does this hurt in practice?
- A bug in
lateFineforces you to open and re-understand a file full of tiffin and skates code. - To unit test tiffin freshness, you must create a whole
SchoolBag, dragging sports and library baggage along. - When the library module of the school app wants due-date logic, it cannot reuse it without importing the entire bag.
- Two teammates editing tiffin and library features collide in this one file — exactly Diya and Sameer's week three.
When methods inside a class form camps that never share fields, the class is not one thing — it is a hostel of strangers. Each camp deserves its own room.
🛠️ Cleaning it up, step by step
The cure is Extract Class: pick one cohesive cluster of fields + methods, move them into a new class, and let the original hold a reference. Diya and Sameer split the work — and for the first time in weeks, they can work in parallel without conflicts.
Step 1: Extract the most independent cluster first — the library record.
class LibraryLoan {
constructor(
readonly bookTitle: string,
readonly dueDate: Date,
) {}
isOverdue(today: Date): boolean {
return today > this.dueDate;
}
lateFine(today: Date): number {
if (!this.isOverdue(today)) return 0;
const daysLate = Math.ceil(
(today.getTime() - this.dueDate.getTime()) / 86400000,
);
return daysLate * 2;
}
weightKg(): number {
return 0.4;
}
}The fine rule now lives where the due date lives. The library module can reuse LibraryLoan directly — no school bag required. Notice the bonus: in the old code, isLibraryBookOverdue had to check borrowedBook !== "" because the bag might not have a book. In the new design, a LibraryLoan object only exists when a book is borrowed. A whole category of "empty string" bugs disappears.
Step 2: Extract the tiffin and sports kit clusters.
class Tiffin {
constructor(
readonly dish: string,
readonly packedAt: Date,
) {}
isFresh(now: Date): boolean {
const hours = (now.getTime() - this.packedAt.getTime()) / 3600000;
return hours < 6;
}
}
class SportsKit {
constructor(
readonly items: { name: string; weightKg: number }[],
readonly sportsDay: string,
) {}
isNeededOn(day: string): boolean {
return day === this.sportsDay;
}
weightKg(): number {
return this.items.reduce((sum, item) => sum + item.weightKg, 0);
}
}Step 3: The bag becomes a light coordinator.
class SchoolBag {
books: string[] = [];
notebooks: string[] = [];
tiffin?: Tiffin;
sportsKit?: SportsKit;
loan?: LibraryLoan;
packForDay(timetable: string[], day: string): void {
this.books = timetable.map((s) => s + " textbook");
this.notebooks = timetable.map((s) => s + " notebook");
if (this.sportsKit && !this.sportsKit.isNeededOn(day)) {
this.sportsKit = undefined; // leave it at home!
}
}
totalWeightKg(): number {
return (
this.books.length * 0.5 +
this.notebooks.length * 0.2 +
(this.sportsKit?.weightKg() ?? 0) +
(this.loan?.weightKg() ?? 0)
);
}
}Look at what the team gained. SchoolBag now does only bag things: deciding what to pack and weighing the result. Tiffin freshness, sports schedules, and library fines each live in a small, named, independently testable class. And notice the design improvement that came free: the bag can now actually leave the skates at home — modelling that was awkward when everything was boolean fields in one giant class.
Here is the new design as a class diagram — the picture Diya finally pinned on the team's wall:
And the before-and-after at a glance:
Two more tools belong in this kit. If a class is large because some features apply only to some objects (say, a Vehicle class with truck-only fields), use Extract Subclass. If clients depend on a giant class but each client uses only a slice of it, use Extract Interface to give each client a small, honest contract.
College corner: Extract Interface is really the Interface Segregation Principle at work — no client should be forced to depend on methods it does not use. In a layered system, you often apply both: Extract Class to fix the implementation's cohesion, then Extract Interface so each consumer sees only its own slice. Together they convert one giant dependency into several narrow, replaceable ones — which is also what makes mocking in unit tests possible.
🔄 The life cycle of this smell
Like Anaya's bag, a class is not born heavy. It moves through states, and team habits decide the direction:
The cheapest arrow is Growing to Clean — split at the first visible seam, when extraction takes an afternoon. The GodObject state is sticky: by then, so many callers depend on the class that every extraction needs coordination across the whole team.
🧰 The same smell in C#
A quick second example, from the sample code Dr. Rao shows in class. This hostel warden class started as "the person who manages the hostel" and absorbed every hostel-related job:
public class HostelWarden
{
public List<string> Rooms = new();
public Dictionary<string, string> RoomAllotments = new();
public List<string> MessMenu = new();
public decimal MessBudget;
public List<string> Complaints = new();
public void AllotRoom(string student, string room) =>
RoomAllotments[student] = room;
public bool IsRoomFree(string room) =>
!RoomAllotments.ContainsValue(room);
public void PlanWeeklyMenu(List<string> dishes) => MessMenu = dishes;
public decimal CostPerStudent(int students) => MessBudget / students;
public void FileComplaint(string text) => Complaints.Add(text);
public int PendingComplaints() => Complaints.Count;
}Three strangers in one room: room allotment, mess planning, complaints. After Extract Class:
public class RoomRegister
{
private readonly Dictionary<string, string> _allotments = new();
public void Allot(string student, string room) => _allotments[student] = room;
public bool IsFree(string room) => !_allotments.ContainsValue(room);
}
public class MessPlan
{
public List<string> Menu { get; private set; } = new();
public decimal Budget { get; set; }
public void PlanWeek(List<string> dishes) => Menu = dishes;
public decimal CostPerStudent(int students) => Budget / students;
}
public class ComplaintBook
{
private readonly List<string> _complaints = new();
public void File(string text) => _complaints.Add(text);
public int PendingCount => _complaints.Count;
}
public class HostelWarden
{
public RoomRegister Rooms { get; } = new();
public MessPlan Mess { get; } = new();
public ComplaintBook Complaints { get; } = new();
}The warden still exists — but now as a coordinator of three focused helpers, exactly like Sunita with her three small bags. Each helper can be tested with three-line tests, and the mess team can change MessPlan without ever opening room-allotment code.
🔍 Where this smell hides in real projects
Large Class has well-known nesting grounds in real software:
- Legacy god classes. Old enterprise systems often contain an
ApplicationManager,SystemContext, orMainFormof several thousand lines that every feature passes through. The Wikipedia entry on god object describes exactly this anti-pattern: an object that references many unrelated types and knows too much. - Entities that absorbed services. A
CustomerorUserclass that began as data and slowly gained email sending, password hashing, invoice formatting, and report generation — each addition "related to customers," none of them being a customer. - Utility dumping grounds.
Utils,Helpers, andCommonclasses where homeless functions accumulate. The name itself is a confession that no single concept lives there. - Fat controllers and fat models in MVC apps. Web frameworks make the controller (or the model) the easiest place to put logic, so checkout controllers and
Usermodels swell. The community even coined slogans against it ("skinny controllers"). - God constants files. A single class holding hundreds of unrelated constants — discussed by Maximiliano Contieri as its own smell — is the same disease in miniature.
Many static analysers flag this smell. SonarQube counts class lines and complexity; metrics like LCOM (Lack of Cohesion of Methods) directly measure whether methods share fields. A high LCOM score is the mathematical version of "strangers sharing one room."
🤔 When it is okay to ignore
| Situation | Ignore the smell? | Why |
|---|---|---|
| Big class, but every method serves one purpose on the same data | ✅ Yes | Size without low cohesion is not the disease; splitting would scatter one concept |
| Generated code (ORM models, API clients) | ✅ Yes | Machines maintain it; humans rarely read it |
| Framework base classes designed to be broad | ✅ Usually | Some frameworks intentionally offer wide surface areas |
| Class with clear field clusters and method camps | ❌ No | The seams are visible — extract along them |
| God class that every team member edits weekly | ❌ No | Highest change traffic means highest payoff for splitting |
| Splitting would create classes with no clear names | ❌ Wait | If you cannot name the pieces, you have not found the real seam yet — do not chop along arbitrary lines |
The honest rule: split only along a real seam — a subset of fields used together by a subset of methods. Cutting a healthy class into random slices produces several anemic classes plus new plumbing, which is worse than one big bag. Sunita did not give Anaya fourteen bags, one per book — she made three, along the real seams of her daughter's day.
💊 Which refactorings cure it
| Refactoring | When to use it |
|---|---|
| Extract Class | The main cure — move a cohesive cluster of fields and methods into a new collaborating class |
| Extract Subclass | Some features are used only by certain instances — push them down into a subclass |
| Extract Interface | Clients need only a slice of the class — give each client a small contract |
| Extract Method | Large classes are full of long methods; shrinking them first often reveals the class seams |
| Replace Data Value with Object | Loose primitive fields in the giant class become proper value objects you can move out |
🧠 The whole smell on one page
Diya's final revision sketch, straight from her hackathon notebook:
📦 Quick revision box
+----------------------------------------------------------------+
| LARGE CLASS - CHEAT SHEET |
+----------------------------------------------------------------+
| What : One class stuffed with many jobs (the school bag) |
| Family : Bloaters (big sibling of Long Method) |
| Nickname : God object, when extreme |
| Spot it : Many fields, method camps, name prefixes, |
| bucket names like Manager/Helper/Util |
| Costs : Overload, coupling, merge conflicts, heavy tests |
| Main fix : Extract Class along real seams |
| Helpers : Extract Subclass, Extract Interface |
| Ignore : Big but cohesive classes; generated code |
| Mantra : "Low cohesion is the disease; size is only |
| the symptom." |
+----------------------------------------------------------------+✍️ Practice exercise
Dr. Rao's homework for Diya and Sameer — and for you. Here is a small but smelly class from a cricket club app. It works fine — but at least three different "bags" are stuffed inside it. Find the seams and extract classes.
class CricketClub {
// members
memberNames: string[] = [];
memberFeesPaid: boolean[] = [];
// ground booking
groundBookings: { date: string; team: string }[] = [];
// kit inventory
bats: number = 10;
balls: number = 24;
addMember(name: string): void {
this.memberNames.push(name);
this.memberFeesPaid.push(false);
}
payFee(name: string): void {
const i = this.memberNames.indexOf(name);
if (i >= 0) this.memberFeesPaid[i] = true;
}
bookGround(date: string, team: string): boolean {
if (this.groundBookings.some((b) => b.date === date)) return false;
this.groundBookings.push({ date, team });
return true;
}
lendKit(batCount: number, ballCount: number): boolean {
if (batCount > this.bats || ballCount > this.balls) return false;
this.bats -= batCount;
this.balls -= ballCount;
return true;
}
}Your tasks:
- Identify the three clusters of fields and methods. Which fields does
bookGroundtouch? Which doespayFeetouch? Do they overlap? - Extract a
MemberRegister, aGroundSchedule, and aKitStoreclass. LetCricketClubhold one of each. - Draw your own version of Figure 7 — a class diagram of your refactored design — before you write any code. Diagrams first, code second.
- Bonus: the parallel arrays
memberNamesandmemberFeesPaidare fragile (what if they go out of sync?). Replace them with a singleMemberobject holding a name and a fee status — a small taste of our next lesson, Primitive Obsession.
When each class in your solution can be explained in one sentence, you have beaten the school-bag smell — and like Diya and Sameer, you can finally work on the same project without stepping on each other.
Frequently asked questions
- Is every big class a Large Class smell?
- No. Size alone is not the disease — low cohesion is. A class can be big but healthy if all its methods work on the same data for one purpose. The smell appears when groups of fields and methods inside the class ignore each other, like strangers sharing one room.
- What is a 'god object'?
- A god object is the extreme form of Large Class — one class that knows too much and does too much, referencing many unrelated types and collecting uncategorised methods. It becomes the centre of the program that everything depends on, which makes it very hard to change safely.
- Won't splitting one class into five make my project confusing?
- Usually the opposite happens. Five small classes with honest names are easier to learn than one tangled giant, because you can understand each one alone. Confusion comes from entangled concepts, not from file count.
- How do I find the 'seams' where a class should be split?
- Look for a subset of fields that is used together by a subset of methods. If three fields and four methods always work together and rarely touch the rest, that cluster is a hidden class. Name prefixes like billingTotal and billingTax are also strong hints.
- Which refactoring fixes Large Class most often?
- Extract Class. You pick one cohesive cluster of fields and methods, move it into a new class, and let the old class hold a reference to it. Extract Subclass and Extract Interface help in special cases.
Further reading
Related Lessons
Long Method: When One Function Tries to Do Everything
Learn the Long Method code smell with simple stories, TypeScript and C# examples, and step-by-step refactoring using Extract Method. Beginner friendly guide.
Primitive Obsession: When Everything Is Just a String or a Number
Primitive Obsession explained simply — why plain strings and numbers hide bugs, and how value objects like Money and Address make code safe and clear.
Data Clumps: The Friends Who Always Travel Together
Data Clumps code smell for beginners — learn to spot groups of values that always travel together and bundle them into one class, like a student ID card.
Divergent Change: One Poor Clerk, Too Many Bosses
Learn the Divergent Change code smell with a school clerk story, simple definitions, TypeScript and C# examples, a clear comparison with Shotgun Surgery, and practice.