Temporary Field: The Cricket Kit That Lives in the School Bag
Learn the Temporary Field code smell with a school bag story, see null-filled fields in TypeScript and C#, and fix them with Extract Class step by step.
๐ The cricket kit in the school bag
Meet Ravi, a class 7 student in Chennai. Every single morning his mother packs his school bag: textbooks, notebooks, lunch box, water bottle. And then โ pads, gloves, a helmet, and a heavy cricket bat sticking out of the top.
"Ravi, why are you carrying the whole cricket kit every day?"
"Because Sports Day is coming, Amma. It is on the 15th of next month."
So for weeks, Ravi drags eight extra kilos to school and back. The bag will not close properly. His geometry box is buried somewhere under the gloves. When his friend Kavya borrows the bag to find a pencil, she has to dig past a helmet first, and she wonders, "Is today Sports Day? Why is this in here?" Every classmate who lifts the bag asks the same confused question. And on most days, the honest answer about the cricket kit is: "It is not being used today. Just ignore it."
It gets worse. One Tuesday the class teacher, Lakshmi ma'am, asks everyone to take out their geometry boxes for a surprise test. Ravi digs. He pulls out a glove. He pulls out a pad. The class giggles. By the time he finds the box, five minutes are gone. The kit does not just sit quietly โ it slows down every other job the bag has.
And here is the part nobody noticed until it mattered. Last year's Sports Day, Ravi had stuffed an old, torn glove deep into a side pocket and forgotten it. This year, on the actual Sports Day, he reached into the bag in a hurry, grabbed the old torn glove instead of the new one, and dropped two catches. Stale equipment, picked up by mistake, because old leftovers and current things share the same bag.
The sensible fix is obvious to everyone except Ravi. The cricket kit should live in a separate kit bag, packed on Sports Day only, carried to the ground only, and brought home when the match ends. The school bag should contain school things โ always, every day, no exceptions, no explanations needed.
Now look at your classes. Have you ever seen a class with a field that is null almost all the time? A field that gets a value only while one special method runs, and means nothing before or after? That is Ravi's cricket kit. The class is carrying baggage every day that is used on one "Sports Day" โ one method โ and every reader of the class has to dig past it and ask, "why is this here?"
That is the Temporary Field code smell. Keep Ravi, Amma, Kavya, and that torn glove in mind โ the whole post follows this one bag. Let us unpack it.
๐ What is this smell?
Temporary Field is one of the Object-Orientation Abuser smells. The abuse here is subtle: fields are one of OOP's most important promises. When you read a class, its fields tell you what an object of this class is. A Student has a name, a rollNumber, a className. You trust that every field is meaningful for the whole life of the object.
A temporary field breaks that promise. It is an instance variable that gets a real value only under special circumstances โ usually only while one particular method (or a small family of methods) is running. Outside that narrow window, it is null, undefined, zero, an empty array, or worse: stale, still holding leftovers from the last run. The torn glove in the side pocket.
How does this happen? Almost always through one innocent shortcut. A programmer writes a big algorithm. It grows too long, so they split it into helper methods. Now all the helpers need the same five intermediate values. Passing five parameters into every helper feels ugly โ so the programmer promotes those values to fields of the class. The parameter lists become short and pretty. But the class has quietly swallowed a cricket kit: five fields that mean something only during that one algorithm.
One-line summary: a temporary field is scratch paper for one method that got glued into the object's permanent shape โ the object now lies about what it contains.
Notice the trade that happened: the programmer avoided the Long Parameter List smell by creating the Temporary Field smell. Smells often work like this โ squeeze one, and another pops up โ unless you fix the real cause, which here is that the algorithm and its working data want to be their own object.
College corner: the formal idea being broken here is the class invariant โ a condition that is true for every object of the class, from the moment the constructor finishes until the object dies. "Every field holds a meaningful value" is the most basic invariant of all. A temporary field replaces that invariant with a weaker, time-dependent one: "fields are meaningful only while method X is on the call stack." Time-dependent invariants are exactly what reviewers, type checkers, and future maintainers are worst at verifying. The cure you will see below โ moving the fields into a class whose constructor establishes the invariant completely โ restores the strong form. In C++ circles, the same instinct appears as RAII: acquire everything in the constructor so there is no half-ready zombie state to reason about.
The whole smell fits on one map:
๐ต๏ธ How to spot it
Run through this checklist when reading a class:
- Some fields are initialized to
null/undefinedin the constructor and only filled inside one method. - A field is set at the top of a method and cleared (or just abandoned) at the bottom.
- You see defensive checks like
if (this.workingTotal !== null)scattered in places far from where the field is filled. - A cluster of fields is touched by exactly one method and its private helpers โ and by nothing else.
- Field names confess their nature:
_temp,_current,_working,_buffer,_inProgress. - A comment says something like "only valid during processing" โ a written admission of the smell.
- Calling methods in the "wrong order" breaks the object (
calculate()must run beforegetResult()or you get garbage). - Two calls to the same method interfere with each other, because the second run reads leftovers from the first.
Here is the symptom table:
| Symptom you see | What it really means |
|---|---|
| Field is null except during one method | The field is method-scratch-space, not a property of the object |
if (field != null) guards far from the filling code | Readers cannot tell when the field is valid; they guess with guards |
| Fields + one method form a private island | An entire hidden class is trapped inside this class |
"Call prepare() before run()" rules | The object has secret temporal rules living only in people's heads |
| Stale values left after a method finishes | The next caller may silently compute with last time's data |
| Object cannot be used by two callers at once | The temporary fields make the object accidentally non-reentrant |
The strongest single test: pick a suspicious field and ask, "If I printed this object at a random moment, would this field's value mean anything?" For a real property like student.name, yes, always. For a temporary field, the answer is "well, it depends on whether computeReport() is currently running" โ and that "it depends" is the smell talking.
Think of it as Ravi's school year. The bag is carried roughly 220 days a year. How many of those days does each item in it actually earn its place?
A field used one day out of two hundred is not a property. It is a passenger.
โ ๏ธ Why it is a problem
Cost 1: The class lies to every reader. Fields are documentation. When ReportBuilder declares currentRow, runningTotal, and pageBuffer as fields, a new teammate reasonably assumes a ReportBuilder has those things. The truth โ "those are garbage except during the 40 milliseconds build() runs" โ is written nowhere. People learn it the painful way, like Kavya digging past the helmet for a pencil.
Cost 2: Null checks breed like rabbits. Because the field is usually invalid, every reader who is not sure adds a guard. Guards hide the real logic, and the one place someone forgets a guard becomes a null reference crash. The defensive code is a tax paid on every line, forever.
Cost 3: Hidden order-of-call rules. Temporary fields create invisible sequencing: fill first, use second, never look in between. The compiler does not know these rules. Tests pass when methods are called in the lucky order, and production finds the unlucky one.
Cost 4: The object becomes unsafe to share. If two parts of the program use the same instance โ or two threads, or one re-entrant call โ they trample each other's temporary fields. A class that looks like a harmless service is secretly single-use.
Cost 5: Testing gets heavier. You cannot test the algorithm without building the whole host object and putting it into the right "filled" state first. The algorithm and its data are trapped inside a bigger creature.
Notice that three separate branches of rot grow from the same root. That is why this smell deserves attention even though it never looks dramatic in a code review.
The most dangerous branch is the stale-read one, because it produces a wrong answer, not a crash. Here is the torn-glove accident as a message sequence โ two modules sharing one object, the second silently reading the first one's leftovers:
No exception. No log entry. Just a certificate with the wrong score, discovered by a parent at the annual day function. And here is what hunting that bug feels like, hour by hour:
The mood scores tell the story: stale-field bugs are miserable to chase precisely because the wrongness happens between method calls, where no debugger is looking.
๐ป A real-life code example
Let us code Sports Day itself. The school has a Student class, and one day someone added Sports Day scoring to it โ using temporary fields:
// BAD CODE: the cricket kit lives inside the school bag
class Student {
// Real, always-valid properties of a student
name: string;
rollNumber: number;
className: string;
// ---- Sports Day "kit": only meaningful during computeSportsScore ----
private eventTimings: number[] | null = null; // null 364 days a year
private penaltyPoints: number | null = null; // null 364 days a year
private bestTiming: number | null = null; // null 364 days a year
constructor(name: string, rollNumber: number, className: string) {
this.name = name;
this.rollNumber = rollNumber;
this.className = className;
}
computeSportsScore(timings: number[], falseStarts: number): number {
// Pack the kit...
this.eventTimings = timings;
this.penaltyPoints = falseStarts * 2;
this.bestTiming = null;
this.findBestTiming(); // fills this.bestTiming
const score = this.applyPenalties(); // reads bestTiming + penaltyPoints
// ...and "unpack" it. Or forget to. Who checks?
this.eventTimings = null;
return score;
}
private findBestTiming(): void {
if (this.eventTimings === null) {
throw new Error("Call computeSportsScore first!"); // hidden rule!
}
this.bestTiming = Math.min(...this.eventTimings);
}
private applyPenalties(): number {
// Guards everywhere, because nothing here is trustworthy
if (this.bestTiming === null || this.penaltyPoints === null) {
throw new Error("Scoring not in progress!");
}
return Math.max(0, 100 - this.bestTiming - this.penaltyPoints);
}
}Count the problems, just like Lakshmi ma'am checking homework:
- Three fields are
nullfor 364 days a year โ every reader ofStudentmust mentally skip them. - Two private methods start with throw-if-null guards, which is the class shouting "I do not trust myself."
- Notice the bug already hiding in there:
computeSportsScoreresetseventTimingsat the end but forgets to clearbestTimingandpenaltyPoints. The next person who reads those fields gets last run's leftovers โ the torn glove in the side pocket. This is not an artificial example โ this is precisely how the wrong-certificate bug in Figure 4 is born. - If the report module and the certificates module both call
computeSportsScoreon the same instance at the same time (async code, anyone?), the fields trample each other.
It also helps to see the life of one of these fields as a state machine. A healthy field has two states: constructed and valid, then gone. A temporary field has a whole secret life:
Every transition on the right-hand path โ Stale, WrongRead, SilentBug โ exists only because the field outlives the computation. Kill that mismatch and the whole right side of the diagram disappears.
๐งน Cleaning it up, step by step
We will clean this in small, safe steps โ never breaking the code in between. This is Amma's kit-bag plan, applied to code.
Step 1: Make the temporal rules visible. Before moving anything, group the kit fields together and document the truth. This step changes no behaviour; it only stops the lying:
// Step 1: at least admit it (an honest intermediate stage, not the goal)
class Student {
name: string;
rollNumber: number;
className: string;
// SCRATCH STATE: valid ONLY inside computeSportsScore and its helpers.
// TODO: extract into its own class.
private scratch: {
eventTimings: number[];
penaltyPoints: number;
bestTiming: number | null;
} | null = null;
// ...
}Already better: there is now one nullable thing instead of three, and its name confesses what it is. But the kit is still in the school bag.
Step 2: Extract Class โ give the kit its own bag. Move the scratch fields and the methods that use them into a new class. One instance of this class is created per computation, so inside it, every field is valid from birth:
// Step 2: the kit bag โ packed on Sports Day, thrown away after
class SportsScoreCalculation {
private readonly eventTimings: number[];
private readonly penaltyPoints: number;
private bestTiming = 0;
constructor(timings: number[], falseStarts: number) {
this.eventTimings = timings; // valid from the first moment
this.penaltyPoints = falseStarts * 2;
}
run(): number {
this.findBestTiming();
return this.applyPenalties();
}
private findBestTiming(): void {
// No guard needed โ eventTimings CANNOT be null here. Ever.
this.bestTiming = Math.min(...this.eventTimings);
}
private applyPenalties(): number {
// No guard needed here either.
return Math.max(0, 100 - this.bestTiming - this.penaltyPoints);
}
}Step 3: Slim the host class back to honesty. Student now contains only things that are true about a student every day of the year:
// Step 3: the school bag carries school things โ always valid, all of them
class Student {
constructor(
public readonly name: string,
public readonly rollNumber: number,
public readonly className: string,
) {}
computeSportsScore(timings: number[], falseStarts: number): number {
return new SportsScoreCalculation(timings, falseStarts).run();
}
}Here is the finished design โ two classes, each with one honest lifetime:
Look what we earned, line by line:
- Every null check vanished. Not moved โ vanished. In
SportsScoreCalculation, the constructor fills every field before any method runs, so there is no invalid moment to guard against. - The stale-data bug is impossible now. The kit object is thrown away after
run(). There is nothing left to be stale. The torn glove goes home with the kit bag on the same evening. Studentbecame stateless for scoring โ two modules, ten threads, can all score at once, because each call builds its own private kit.- The algorithm is testable alone.
new SportsScoreCalculation([12.5, 11.9], 1).run()โ noStudentneeded at all.
This exact move โ turning a method's scratch fields into a dedicated object โ has a formal name: Replace Method with Method Object. It is Extract Class specialized for the "one big algorithm" situation.
College corner: the deep principle here is lifetime alignment. Every piece of state has a natural lifetime: per-application, per-object, per-request, per-call. Bugs concentrate wherever state is stored at a longer lifetime than its natural one โ per-call data in a per-object field, per-request data in a singleton. The Extract Class cure is really a lifetime correction: the new object's lifetime equals the computation's lifetime, so validity is guaranteed by construction rather than by discipline. This is also why the fix automatically buys reentrancy and thread-safety for the host: a method that builds all its state locally (on the stack, or in a fresh object) is safe to run concurrently, while a method that parks state on this is not. When you later study dependency injection scopes โ singleton, scoped, transient โ you will recognise this exact smell as "scoped state in a singleton service".
And the payoff in numbers โ count the defensive lines before and after:
Zero and zero. Not because we were careful, but because the design made the unsafe states unrepresentable. That is always the best kind of safety.
A common half-fix is to keep the fields but add more null checks "to be safe." That treats the symptom and feeds the disease: every new guard makes the code longer and makes the temporal rules even harder to see. The cure is moving the fields to where they are always valid, not guarding them harder where they are not.
The same smell in C# ๐ผ
The identical pattern in C#, shortened โ a billing class that stashes invoice-calculation scratch on the instance:
// BAD: scratch fields on a long-lived service
class InvoiceService
{
private List<decimal>? _lineAmounts; // null between invoices
private decimal _discount; // stale between invoices!
public decimal Total(Order order)
{
_lineAmounts = order.Lines.Select(l => l.Price * l.Qty).ToList();
_discount = order.IsFestivalSeason ? 0.10m : 0m;
return ApplyDiscount(Sum());
}
private decimal Sum() => _lineAmounts!.Sum(); // trust me, it's filled
private decimal ApplyDiscount(decimal s) => s * (1 - _discount);
}That _lineAmounts! null-forgiving operator is the C# version of "the team just knows." And if this service is registered as a singleton in dependency injection โ which services usually are โ two simultaneous requests will overwrite each other's _discount. The fix is the same kit-bag move:
// GOOD: one short-lived calculation object per invoice
class InvoiceService
{
public decimal Total(Order order) => new InvoiceCalculation(order).Run();
}
class InvoiceCalculation
{
private readonly List<decimal> _lineAmounts;
private readonly decimal _discount;
public InvoiceCalculation(Order order)
{
_lineAmounts = order.Lines.Select(l => l.Price * l.Qty).ToList();
_discount = order.IsFestivalSeason ? 0.10m : 0m;
}
public decimal Run() => _lineAmounts.Sum() * (1 - _discount);
}readonly fields, no nulls, no !, thread-safe by construction. The C# compiler now proves what comments used to promise.
๐บ๏ธ Where this smell hides in real projects
Once you know the cricket kit, you will find it in surprising places:
- Singleton services in dependency injection containers. A service registered once for the whole app quietly stores per-request scratch in fields. Works fine in testing (one request at a time), corrupts data under real load. This is one of the most common production-only bug sources in web backends.
- Report and export generators.
currentPage,runningTotal,rowBufferas fields of aReportGeneratorthat lives forever โ classic "algorithm helpers needed shared state." - Parsers and importers.
_currentLine,_tokenBuffer,_errorsSoFaron a long-lived parser object. Each parse should be its own short-lived object instead. - Wizard / multi-step form handlers. Step 3's fields are garbage until step 2 runs. Sometimes this is genuinely a state machine (then model it as one โ see State); often it is just temporary fields wanting extraction.
- Game loops.
collidingPairsThisFrameand similar per-frame scratch lists stored on a manager object, mixed with permanent world state. - "Result" fields next to "compute" methods.
calculate()fillsthis.result; callers must remember to call them in order. The result wants to be a return value, not a field.
The common thread: a long-lived object is being used as a short-lived workspace. The lifetimes do not match, and the mismatch shows up as null and stale fields โ a school bag doing a kit bag's job.
โ๏ธ When it is okay to ignore
Honesty table โ not every field that is sometimes empty is a crime:
| Situation | Verdict | Why |
|---|---|---|
| Lazy cache: field stores an expensive computed value, with clear fill/clear rules | Fine | It represents a real property of the object, deliberately computed late |
| Memoization of a pure function's result | Fine | Same value would be computed every time; storing it is an optimization, not scratch |
Optional domain data (middleName may be absent) | Fine โ different thing | "Sometimes absent in the real world" is not "valid only during one method" |
| Reused buffer field in a measured hot path, documented loudly | Tolerable | A calculated performance exception; isolate it and write the warning comment |
| Tiny algorithm โ field lives for five lines on one screen | Usually leave it | Extracting a class costs more ceremony than the clarity it buys |
| Fields valid only during one method, with null guards spreading | Refactor | This is the smell proper; Extract Class pays off immediately |
| Scratch fields on a shared/singleton service | Refactor urgently | It is not just unclear โ it is a concurrency bug waiting for traffic |
Two questions place any suspicious field on the map: how much of the object's life is the field valid, and how widely is the object shared?
The singleton-with-scratch sits in the top-right corner for a reason: it combines the lying shape with real concurrent damage. That is the one to fix this sprint, not someday.
Quick judgement question: is the field a fact about the object (cache, optional data) or a fact about one computation (intermediate totals, working buffers)? Facts about a computation belong in an object whose lifetime is that computation.
๐ ๏ธ Which refactorings cure it
| Refactoring | Use it when |
|---|---|
| Extract Class | The main cure: move the temporary fields plus their methods into a new class where they are always valid |
| Replace Method with Method Object | One big algorithm spawned the fields; turn the whole algorithm into a short-lived object |
| Introduce Parameter / pass values through | The "shared state" is small; just pass two or three parameters honestly between helpers |
| Introduce Null Object | The field is legitimately sometimes absent; replace null with an object that behaves as "empty" and delete the guards |
| Inline the helpers back | The split into helpers was the mistake; a single 15-line method may need no shared fields at all |
๐ฆ Quick revision box
+----------------------------------------------------------------+
| TEMPORARY FIELD โ CHEAT SHEET |
+----------------------------------------------------------------+
| Story : Ravi carries the cricket kit in his school bag |
| every day; it is used only on Sports Day. |
| Smell : Field with a real value only during one method; |
| null / stale the rest of the object's life. |
| Spot it : null-guards far from the fill site, names like |
| _temp/_working, "call X before Y" rules, |
| fields touched by only one method. |
| Danger : Lying object shape, null crashes, stale data, |
| unsafe sharing, untestable algorithm. |
| Cure : Extract Class / Replace Method with Method Object |
| -> a kit bag packed per computation, then thrown. |
| Keep : Honest caches & optional domain data are NOT this. |
| Mantra : Match the field's lifetime to its owner's lifetime.|
+----------------------------------------------------------------+โ๏ธ Practice exercise
A library management system has this class. Find the cricket kit and extract it:
class Library {
books: Book[] = [];
members: Member[] = [];
// Used ONLY while computeFine runs:
private daysLate: number | null = null;
private finePerDay: number | null = null;
private maxFine: number | null = null;
computeFine(member: Member, book: Book, returnDate: Date): number {
this.daysLate = this.calcDaysLate(book.dueDate, returnDate);
this.finePerDay = member.isStudent ? 1 : 5;
this.maxFine = member.isStudent ? 50 : 200;
return this.applyCap();
}
private calcDaysLate(due: Date, ret: Date): number {
return Math.max(0, Math.ceil((ret.getTime() - due.getTime()) / 86400000));
}
private applyCap(): number {
if (this.daysLate === null || this.finePerDay === null || this.maxFine === null) {
throw new Error("Fine calculation not in progress");
}
return Math.min(this.daysLate * this.finePerDay, this.maxFine);
}
}Your tasks:
- Spot: List every temporary field and every hidden temporal rule (which method must run before which?). Also find the stale-data risk: which fields keep old values after
computeFinereturns? - Extract: Create a
FineCalculationclass whose constructor receivesmember,book, andreturnDate, fills all fields immediately, and exposes onerun(): numbermethod. No field should ever be null. - Verify: Rewrite
Library.computeFineas a one-liner. Then answer: can two parts of the app now compute fines on the sameLibraryinstance at the same time? Why was that dangerous before? - Story check: Draw (on paper) Figure 6's state diagram for the old
daysLatefield, and mark which state the torn-glove bug lives in. Then draw the same diagram for the newFineCalculationversion โ how many states are left? - Bonus:
calcDaysLateuses no fields at all. What does that tell you about where it belongs? (Hint: a pure function can be a standalone helper or a static method โ it never needed the bag in the first place.)
Frequently asked questions
- What exactly is a temporary field?
- It is an instance field that holds a useful value only while one particular method runs. The rest of the time it sits null, zero, or stale. The object's shape lies, because the field looks like a permanent property but is really scratch paper for one algorithm.
- Why do programmers create temporary fields?
- Usually to avoid a long parameter list. When a big algorithm is split into helper methods that all need the same values, promoting those values to fields feels easier than passing them as parameters everywhere. The cost appears later.
- What is the main cure for temporary fields?
- Extract Class, or its close cousin Replace Method with Method Object. Move the temporary fields and the methods that use them into a new small class whose object lives only for one computation. Inside that class, every field is always valid.
- Are caches and memoized values temporary fields?
- Not usually. A cache that stores an expensive computed result, with clear rules for when it is filled and cleared, is a deliberate design choice about a real property of the object. A temporary field is scratch space for one method that leaks into the object's shape.
- How do temporary fields cause null reference bugs?
- Because the field is only valid during one method, every other place that touches it needs a null check. The moment someone reads it at the wrong time, or forgets one guard, the program crashes or silently uses stale data.
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.
Long Parameter List: The Chai Order That Took Ten Instructions
Long Parameter List code smell made simple โ why methods with too many arguments cause bugs, and how parameter objects make calls short, clear, and safe.
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.
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.