Push Down Method: Move a Method to the Subclass That Really Uses It
Learn the Push Down Method refactoring with a school-office story, honest superclass contracts, safe step-by-step moves in TypeScript and C#, and how it cures the Refused Bequest smell.
🏫 The certificate counter that confused the whole school
Come with me to a busy school office in Nagpur. The clerk at the front counter is Sunil kaka — he has been stamping certificates for twenty years and knows every student's face. On the wall behind him hangs a big white board listing every service the office gives: issue bonafide certificate, collect fees, update address, issue sports certificate.
Read that last one again. Issue sports certificate. The board says the office gives this service to all students. So what happens? Aarav, a class 6 boy who has never touched a football in his life, queues up for twenty minutes and asks for a sports certificate, because his cousin told him "every certificate comes from the office." Sunil kaka sighs and says, "Beta, this is only for students in the sports section. Go to Khan sir at the sports desk." Aarav walks away confused and a little embarrassed. The next day, a class 8 girl does the same. The day after, two more students. Sunil kaka has started keeping a count on a scrap of paper — eleven wrong-counter visits this month alone.
Why does this keep happening? Not because the students are careless. Because the board made a promise it cannot keep. It listed a service at the level of "the whole office", when in truth only one section — the sports section, run by Khan sir — ever provides it. Every student who reads the board is being misled by the board itself.
The headmistress, Mrs. Deshpande, hears about Sunil kaka's scrap-paper count and fixes the whole thing in five minutes. She removes "issue sports certificate" from the main office board and puts it on a small board at the sports section desk — the only place where the service is real. Now the main board carries only services that are truly for everyone. Aarav never queues at the wrong counter again, and Sunil kaka retires his scrap paper.
Here is Aarav's experience, before and after the board was fixed:
This exact fix exists in code, and it is called Push Down Method. When a method sits in a superclass but only one subclass (or a very small few) actually uses it, we move it down into that subclass. The parent's "board" — its public contract — then lists only abilities that every child genuinely has.
🎯 What is Push Down Method?
In object-oriented code, a superclass is a promise. When SchoolSection has a method issueSportsCertificate(), the language is telling every reader: every kind of section can do this. The science section, the arts section, the sports section — all of them.
But sometimes that promise is false. The method was placed in the parent by habit, or it used to be common long ago, or someone planned a future that never came. Today, only SportsSection has a real body for it. The other subclasses either ignore it, or — worse — override it just to throw a "not supported" error.
Push Down Method is the refactoring that repairs this. It says:
- Find the subclass (or subclasses) that really use the method.
- Copy the method into exactly those subclasses.
- Delete the method from the superclass, along with any sad "not supported" overrides.
- Fix any caller that was calling the method through the parent type.
After the move, the parent class is honest again. Anything you can call on a SchoolSection reference is guaranteed to work for every section. This is what makes polymorphism trustworthy — you can hold a parent-typed variable and call its methods without fear.
Martin Fowler lists this refactoring in his classic catalog, and notes that it is the exact inverse of Pull Up Method. The two form a pair of elevators in the class hierarchy: pull up what is shared, push down what is special.
The full decision is small enough to fit in one picture:
One-line summary: Push Down Method moves a method from the superclass into only the subclass(es) that truly use it, so the parent's contract promises nothing it cannot deliver.
College corner: the formal name for what the false board violates is the Liskov Substitution Principle (LSP) — the "L" of SOLID. LSP says any object of a subclass must be usable wherever the superclass is expected, without surprising the caller. A ScienceSection that throws NotSupportedException from an inherited method is the textbook LSP violation: it compiles as a substitute but explodes as one. Push Down Method restores LSP not by making the child behave, but by shrinking the parent's promise until every child can keep it. In exam language: strengthen nothing the parent requires, weaken nothing the parent promises.
📞 The wrong call, as a conversation
Watch the runtime conversation before the fix. The caller trusts the parent's board, and the trust is repaid with an exception:
That note in the middle is the whole disease in one line: a runtime surprise from a compiled promise. The compiler approved the call because the parent declared the method; the program crashed because the child refused it. After Push Down Method, the bad call does not crash — it does not compile, which is infinitely better, because the mistake is caught before any student ever queues.
🔔 When do we need it?
Watch for these signals in your codebase:
- A "not supported" override. A subclass overrides a parent method only to throw an error or return a dummy value. This is the loudest alarm bell of the Refused Bequest smell — the child is refusing the inheritance the parent forced on it.
- A method that only one child calls. Search for usages. If
issueSportsCertificate()is only ever called onSportsSectionobjects, its home is wrong. - A bloated parent. When the superclass keeps collecting "maybe someone will need this" methods, it slowly becomes a Large Class — a god-parent that knows about every child's private business. Pushing special methods down is one way to slim it.
- Planned generality that never arrived. Someone put the method in the parent thinking "all sections will need certificates someday." Years passed. Only sports ever did. The speculative design should now be undone.
Sunil kaka's scrap-paper count has a software equivalent: measure how the parent's services are actually used. In our school office, the picture looked like this:
That 10% slice is not small because the service is unimportant — it is small because only one section's students ever need it. A service used by one group does not belong on the everyone-board, no matter how often that group uses it.
One caution before you reach for this tool. If several subclasses use the method, pushing it down means copy-pasting it into each one, and that creates the Duplicate Code smell — you would be trading one problem for a worse one. In that case, either leave the method in the parent (it is genuinely shared) or, if a fixed group of children keeps sharing the same special methods, give that group its own intermediate parent using Extract Superclass.
🔍 Before and after at a glance
Here is the school office in TypeScript, with the false promise sitting in the parent:
// BEFORE: the parent promises a service only one child gives
abstract class SchoolSection {
constructor(protected sectionName: string) {}
issueBonafide(studentId: string): string {
return `Bonafide certificate for ${studentId} (${this.sectionName})`;
}
issueSportsCertificate(studentId: string): string {
// only meaningful for the sports section!
return `Sports certificate for ${studentId}`;
}
}
class ScienceSection extends SchoolSection {
issueSportsCertificate(studentId: string): string {
throw new Error("Science section does not issue sports certificates");
}
}
class SportsSection extends SchoolSection {
// uses the inherited issueSportsCertificate()
}And here is the code after Mrs. Deshpande's fix:
// AFTER: the service lives only where it is real
abstract class SchoolSection {
constructor(protected sectionName: string) {}
issueBonafide(studentId: string): string {
return `Bonafide certificate for ${studentId} (${this.sectionName})`;
}
}
class ScienceSection extends SchoolSection {
// nothing to refuse — the method never reaches here
}
class SportsSection extends SchoolSection {
issueSportsCertificate(studentId: string): string {
return `Sports certificate for ${studentId}`;
}
}The diagram shows the move clearly:
Notice two wins. First, ScienceSection no longer carries a throwing override — that whole method is gone. Second, the type system now prevents the wrong call: scienceSection.issueSportsCertificate() is a compile error, not a runtime surprise.
🪜 Step-by-step, the safe way
Never do this refactoring as one big jump. Move in small steps, compiling and running tests after each one. The whole journey passes through clear states, and you should be able to compile at every one of them:
-
Confirm the real users. Use your IDE's "Find Usages" on the method. List which subclasses actually need it. If the answer is "many of them", stop — this method belongs in the parent, and you should not push it down.
-
Copy the method into the target subclass. Do not delete anything yet. For a moment, the method exists in both places — that is fine and safe.
// INTERMEDIATE STEP: method temporarily lives in BOTH places
abstract class SchoolSection {
issueSportsCertificate(studentId: string): string {
return `Sports certificate for ${studentId}`;
}
}
class SportsSection extends SchoolSection {
// copied down — overrides the parent version for now
issueSportsCertificate(studentId: string): string {
return `Sports certificate for ${studentId}`;
}
}-
Check what the method touches. If the method body reads a field that only this subclass needs, plan to push that field down too (the companion refactoring Push Down Field). The parent should not keep data whose only customer has left.
-
Delete the parent's copy. Now remove the method from the superclass, and remove every "not supported" override from the other subclasses.
-
Let the compiler find broken callers. Any code that called the method on a parent-typed variable will now fail to compile. Visit each error. Usually the fix is simple — that caller always had a
SportsSectionin hand anyway, so change the variable's declared type. If a caller truly holds a mixed bag of sections, it must first check the type:
function printCertificates(section: SchoolSection, studentId: string) {
console.log(section.issueBonafide(studentId));
if (section instanceof SportsSection) {
console.log(section.issueSportsCertificate(studentId));
}
}- Compile and run all tests. Green tests mean the promise board is fixed and nobody's behaviour changed.
Be extra careful in dynamically typed languages and with reflection. In TypeScript the compiler catches every broken caller, but in plain JavaScript or Python a call through a parent-typed variable fails only at runtime. Search the whole codebase for the method name — including string-based calls — before deleting the parent copy.
📉 Measuring the win
Refactorings feel vague until you count something. Two numbers move sharply when you push a method down: the count of throwing "not supported" overrides (straight to zero) and the count of defensive instanceof checks scattered before calls (usually down to one or two honest ones at a type boundary).
In our school's real payroll codebase (coming up next), four classes carried throwing overrides for allowance methods they never earned. After the refactoring, that count is zero — not because someone remembered to delete them, but because there is nothing left to refuse.
And here is the decision in quadrant form. Place your method on this map before touching anything:
Top-right is home sweet home for a parent method. Bottom-left — used by one child, refused by the rest — is the sports certificate, screaming to be pushed down. The strange bottom-right corner (everyone calls it, yet some refuse it) means callers are already living with runtime errors; that is not a refactoring problem but a design emergency.
🧮 A bigger real-life example
Let us scale the story up. The school management portal models staff payroll. Someone long ago put calculateMatchAllowance() — extra pay for accompanying students to tournaments — on the base StaffMember class. Only coaches ever earn it. Teachers and clerks got a throwing override.
// BEFORE
abstract class StaffMember {
constructor(protected name: string, protected basicPay: number) {}
abstract monthlySalary(): number;
calculateMatchAllowance(matchDays: number): number {
return matchDays * 500;
}
}
class Teacher extends StaffMember {
monthlySalary(): number { return this.basicPay + 4000; }
calculateMatchAllowance(): number {
throw new Error("Teachers do not get match allowance"); // Refused Bequest!
}
}
class Clerk extends StaffMember {
monthlySalary(): number { return this.basicPay + 2000; }
calculateMatchAllowance(): number {
throw new Error("Clerks do not get match allowance"); // Refused Bequest!
}
}
class Coach extends StaffMember {
monthlySalary(): number {
return this.basicPay + this.calculateMatchAllowance(6);
}
}Three classes, two of them shouting refusals. After Push Down Method:
// AFTER
abstract class StaffMember {
constructor(protected name: string, protected basicPay: number) {}
abstract monthlySalary(): number;
}
class Teacher extends StaffMember {
monthlySalary(): number { return this.basicPay + 4000; }
}
class Clerk extends StaffMember {
monthlySalary(): number { return this.basicPay + 2000; }
}
class Coach extends StaffMember {
private matchDaysThisMonth = 6;
monthlySalary(): number {
return this.basicPay + this.calculateMatchAllowance(this.matchDaysThisMonth);
}
calculateMatchAllowance(matchDays: number): number {
return matchDays * 500;
}
}The parent is now three lines of pure shared truth. Teacher and Clerk lost code — always a happy moment — and Coach gained one small method plus the matchDaysThisMonth field that travelled down with it.
College corner: notice that StaffMember here is an abstract class, not an interface. It earns its place because it holds real shared state (name, basicPay) and would also be the natural home for genuinely universal logic like tax deduction. In C# and Java, an abstract class spends the child's single inheritance slot — class Coach : StaffMember uses the only extends a class will ever get — so everything you keep in it must be worth that slot. Every method you wrongly leave in the base is paying rent in the most expensive house in the language. Push Down Method is, in slot economics, how you stop wasting that rent.
💼 The same refactoring in C#
C# makes the smell even easier to spot, because refusing subclasses usually throw NotSupportedException. Here is a bank-account hierarchy where overdraft fees were wrongly placed on the base:
// BEFORE
public abstract class Account
{
protected decimal Balance;
public virtual decimal CalculateOverdraftFee(decimal overdrawn)
=> overdrawn * 0.05m; // only current accounts can overdraw!
}
public class SavingsAccount : Account
{
public override decimal CalculateOverdraftFee(decimal overdrawn)
=> throw new NotSupportedException("Savings accounts cannot overdraw");
}
public class CurrentAccount : Account
{
// silently inherits CalculateOverdraftFee
}After Push Down Method, the method exists only where overdrawing is legal:
// AFTER
public abstract class Account
{
protected decimal Balance;
}
public class SavingsAccount : Account
{
// nothing to refuse
}
public class CurrentAccount : Account
{
public decimal CalculateOverdraftFee(decimal overdrawn)
=> overdrawn * 0.05m;
}Note one C# detail: when the method moves down and no longer overrides anything, drop the virtual/override keywords. It is now an ordinary method of CurrentAccount. If some caller held an Account variable and called the fee method, the compiler will flag it — change that caller to use CurrentAccount, or pattern-match first: if (account is CurrentAccount current) { ... }.
🐍 And a warning from Python
Python will not stop you from leaving false promises in a base class — there is no compiler standing guard. That makes the discipline of Push Down Method even more important there, and tests your only safety net:
# BEFORE: the base class promises what only Coach delivers
class StaffMember:
def __init__(self, name: str, basic_pay: int) -> None:
self.name = name
self.basic_pay = basic_pay
def calculate_match_allowance(self, match_days: int) -> int:
return match_days * 500 # false promise for most staff
# AFTER: pushed down — only Coach carries the allowance logic
class Coach(StaffMember):
def __init__(self, name: str, basic_pay: int, match_days: int) -> None:
super().__init__(name, basic_pay)
self.match_days = match_days
def calculate_match_allowance(self) -> int:
return self.match_days * 500
def monthly_salary(self) -> int:
return self.basic_pay + self.calculate_match_allowance()After the move in Python, run the whole test suite and also grep the codebase for the method name — duck typing means a forgotten caller will only fail when that exact line runs, possibly in production on a Sunday wedding... sorry, wrong story. That one is in the Extract Subclass post.
College corner: in C#, watch the difference between override and method hiding. If CurrentAccount had declared public new decimal CalculateOverdraftFee(...) while the base still had its copy, calls through an Account variable would run the base version and calls through a CurrentAccount variable would run the child version — two behaviours for one object, decided by the variable's declared type. Java has no such hiding for instance methods (only for statics and fields), and TypeScript follows the prototype chain at runtime. This is exactly why step 4 — actually deleting the parent copy — matters: a half-finished push down in C# can silently fork behaviour.
🛠️ IDE support
You rarely need to do this move by hand — major IDEs automate it.
- IntelliJ IDEA / Android Studio (Java, Kotlin): select the method, then Refactor → Push Members Down. A dialog lists the class members with checkboxes; tick the method (and any field that should travel with it), and the IDE copies it into every direct subclass and removes it from the parent. JetBrains documents this under "Pull members up, push members down" — the two dialogs are mirror twins.
- JetBrains Rider / ReSharper (C#): the same Push Members Down refactoring exists, and it warns you when a member you are pushing is still used through the base type.
- Visual Studio (C#): there is no one-click push down, but the Find All References and Move helpers make the manual steps quick. (Its famous cousin, Extract Interface, lives in the Quick Actions menu — useful when the better fix is a narrower contract instead of a push down.)
- TypeScript editors (VS Code, WebStorm): no dedicated push-down command, but the compiler is your safety net — delete the parent copy and follow the red squiggles.
One important behaviour of the IDE dialogs: if more than one subclass exists, Push Members Down pushes the member into every direct subclass, creating duplicates. If only one child should receive it, delete the extra copies afterwards — or rethink whether an intermediate superclass is the better design.
⚖️ Push down vs pull up: the elevator pair
Students mix these up constantly, so here is the side-by-side:
| Question | Push Down Method | Pull Up Method |
|---|---|---|
| Direction | Parent → child | Children → parent |
| Trigger | Only one (or few) children use the method | Many children duplicate the same method |
| Smell it cures | Refused Bequest, Speculative Generality | Duplicate Code across siblings |
| Effect on parent | Slims down; contract becomes honest | Grows; contract becomes richer but still universal |
| Effect on polymorphism | The pushed method can no longer be called on the base type — by design | The pulled method becomes callable on the base type |
| Compiler's role | Flags every caller that wrongly used the parent type | Flags children that forgot a needed override |
The deciding question is always the same: who genuinely uses this behaviour? Everyone → keep it up or pull it up. One special child → push it down. A consistent subset → extract a middle superclass and push it exactly one level.
📊 Benefits and risks
| Aspect | Benefit | Risk / cost |
|---|---|---|
| Honest contract | The parent promises only what every child can do | Callers using the parent type lose access and must be fixed |
| Refused Bequest | All "not supported" overrides are deleted | None — this is pure win |
| Class size | The parent slims down; helps with Large Class | The receiving subclass grows slightly |
| Duplication | None, if only one subclass needs the method | Pushing to several subclasses creates Duplicate Code |
| Polymorphism | Calls through the base type become fully trustworthy | You can no longer call the special method polymorphically — by design |
| Future change | New subclasses do not inherit irrelevant baggage | If the method later becomes common again, you must pull it back up |
The middle-ground trick is worth repeating: when a subset of subclasses (say, SportsSection and NccSection) both need the method, do not duplicate it into both. Create an intermediate parent — ActivitySection — with Extract Superclass, push the method down exactly one level into it, and let both activity sections extend it.
🧪 Which smells does it cure?
| Smell | How Push Down Method helps |
|---|---|
| Refused Bequest | Removes the inherited member the subclass was refusing; the refusal overrides vanish entirely |
| Large Class | Shrinks an overloaded superclass by relocating members that only special children use |
| Speculative Generality | Undoes "someday everyone will need this" designs that never came true |
| Duplicate Code | Indirectly — by warning you not to push down when many children share the method, and to keep it in one shared home instead |
🧠 The whole idea on one map
📦 Quick revision box
+--------------------------------------------------------------+
| PUSH DOWN METHOD |
+--------------------------------------------------------------+
| Problem : Parent class has a method only ONE child uses. |
| Other children refuse it ("not supported"). |
| Story : School office board listed "sports certificate" |
| for ALL students; only sports desk gives it. |
| Fix : 1. Copy method into the child that uses it |
| 2. Push down any field it depends on |
| 3. Delete method (+ refusals) from the parent |
| 4. Fix callers that used the parent type |
| Cures : Refused Bequest, bloated parent (Large Class) |
| Beware : Many children need it? DON'T push down — |
| keep it up, or extract a middle superclass. |
| Inverse : Pull Up Method |
+--------------------------------------------------------------+✍️ Practice exercise
Try this on your own before peeking at any solution.
A delivery app has this hierarchy:
abstract class DeliveryVehicle {
abstract maxSpeedKmph(): number;
attachThermalBox(): string {
return "Thermal box attached for food orders";
}
}
class Bike extends DeliveryVehicle {
maxSpeedKmph(): number { return 60; }
}
class BicycleCart extends DeliveryVehicle {
maxSpeedKmph(): number { return 15; }
attachThermalBox(): string {
throw new Error("Bicycle carts carry parcels only, no food");
}
}
class Van extends DeliveryVehicle {
maxSpeedKmph(): number { return 80; }
attachThermalBox(): string {
throw new Error("Vans carry furniture, no food");
}
}Your tasks:
- Identify which subclass genuinely uses
attachThermalBox()and which ones refuse it. Sketch the quadrant from Figure 8 and place the method on it. - Apply Push Down Method in safe steps: copy down, delete from the parent, remove the throwing overrides. Compile (or run tests) at every state from Figure 6.
- There is one caller in the codebase:
function prepareForFood(v: DeliveryVehicle) { v.attachThermalBox(); }. Fix its parameter type so it compiles again. - Bonus: suppose tomorrow a new
Scooterclass also needs the thermal box. Two children now share the method. Sketch the intermediate superclass (FoodCapableVehicle?) you would extract so the method still lives in exactly one place. - College-level bonus: explain in two sentences why the original
BicycleCart.attachThermalBox()override violated the Liskov Substitution Principle, and why the after-version cannot violate it even in theory.
If your final parent class contains only maxSpeedKmph(), and no class anywhere throws "not supported", you have done it correctly — Mrs. Deshpande would put your name on the honest board.
Frequently asked questions
- What is the difference between Push Down Method and Pull Up Method?
- They are exact opposites. Pull Up Method moves a method from subclasses into the parent because many children share it. Push Down Method moves a method from the parent into a subclass because only that one child really uses it. Pull up when behaviour is common; push down when behaviour is special.
- What if two or three subclasses need the method, but not all?
- Copy-pasting it into each of them creates Duplicate Code. The better move is to create an intermediate superclass for just that group — for example, a SportsSection and an NccSection could both extend an ActivitySection that holds the shared method. Push the method down only one level, into that middle parent.
- Will callers that use the parent type break after pushing down?
- Yes, any code that called the method through a parent-typed variable will stop compiling. That is actually useful — the compiler shows you every caller that was wrongly assuming all children have this ability. Fix each one by using the subclass type, or by checking the real type before calling.
- Is a method that throws 'not supported' in a subclass really so bad?
- It is a classic sign of the Refused Bequest smell. The parent promises an ability, the child refuses it, and the failure only appears at runtime when someone calls it. Pushing the method down turns that runtime surprise into a compile-time guarantee — the method simply does not exist where it makes no sense.
- Does Push Down Method also apply to fields?
- There is a companion refactoring called Push Down Field that does the same job for data. Often you do both together — when you push a method down, any field that only this method used should travel down with it, so the parent keeps no orphan state.
Further reading
Related Lessons
Refused Bequest: The Child Who Refused the Sweet Shop Recipes
Learn the Refused Bequest code smell with a family sweet shop story, Liskov violations in TypeScript and C#, and the delegation cure explained step by step.
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.
Duplicate Code: Writing the Same Address on 50 Wedding Cards
Learn the Duplicate Code smell with a wedding card story. Understand DRY, the Rule of Three, and how Extract Method removes dangerous copy-paste code.
Extract Subclass: Give the Special Case Its Own Class
Learn the Extract Subclass refactoring with a tailor-shop story about urgent orders, flag-removal in TypeScript and C#, safe step-by-step moves, and when subclassing is the wrong tool.