Skip to main content
CleanCodeMastery

Hide Method: The Secret Masala Stays Inside the Kitchen

Hide Method explained simply — why a method that only the class itself uses should not sit on the public menu, and how lowering visibility to private or internal shrinks the API, protects internals, and frees you to change code without fear.

25 min read Updated June 11, 2026beginner
refactoringhide methodencapsulationvisibilityprivateinternalpublic apitypescriptcsharp

🍛 The Kitchen's Secret Masala Never Leaves the Kitchen

There is a famous biryani house in Hyderabad — Nawab's Kitchen. People say its biryani tastes like nowhere else, and the reason is the house masala: a blend of spices that Ustad Karim, the head cook, mixes early every morning, behind the kitchen door, before even the waiters arrive.

Now think about how the restaurant actually works. A customer — let us follow Farhan, a regular — walks in and gets a menu: biryani, kebab, firni. Farhan orders from the menu; that is the deal between restaurant and customer. What Farhan never gets to do is walk into the kitchen, open Ustad Karim's masala dabba, and start mixing spices. The grinding, the roasting, the secret ratio — all of that stays inside the kitchen. Farhan does not even know those steps exist, and that is precisely the point: tomorrow Ustad Karim can roast the saunf a little longer or swap one chilli for another, and Farhan's order does not break, because Farhan never depended on the steps — only on the dish.

One year, a new manager had a "brilliant" marketing idea: print the masala steps on the back of the menu, and let interested customers prepare their own mix at a special counter. It lasted three weeks. Customers made bad masala and blamed the restaurant. Worse, when Ustad Karim improved step 4 — a little more roasted saunf — twenty regulars complained that "the recipe on the menu says otherwise!" The kitchen was now frozen by outsiders' habits. The owner quietly reprinted the menus with dishes only, and Ustad Karim got his kitchen back.

In code, your class's public methods are the menu. Your helper methods — the grinding and roasting steps — should stay inside the kitchen, marked private. When a helper accidentally sits on the menu, the refactoring that moves it back inside is called Hide Method.

Figure 1: A leaked kitchen step travels back behind the door

🧠 What is Hide Method?

Hide Method means reducing a method's visibility — typically from public to private (or protected, or C#'s internal) — because nothing outside the class actually needs to call it.

Before — a kitchen step printed on the menu:

class Bill {
  public subtotal(): number { /* ... */ return 540; }
 
  public taxFor(amount: number): number {     // only used inside this class!
    return amount * 0.05;
  }
 
  public total(): number {
    return this.subtotal() + this.taxFor(this.subtotal());
  }
}

After — the step goes back inside the kitchen:

class Bill {
  public subtotal(): number { /* ... */ return 540; }
 
  private taxFor(amount: number): number {    // hidden: an internal step
    return amount * 0.05;
  }
 
  public total(): number {
    return this.subtotal() + this.taxFor(this.subtotal());
  }
}

One keyword changed. Why does it matter so much? Because the public interface of a class is a promise. Every public method says to the whole codebase: "You may call me. I will keep existing, keep my name, keep my behaviour — depend on me." That promise is expensive. Once outsiders depend on taxFor, you cannot rename it, change its parameters, or delete it without breaking them — exactly like Ustad Karim could not improve step 4 once it was printed on the menu.

A private method makes no promise to anyone. As the Refactoring Guru catalog puts it, when you change a private method you only need to worry about not breaking the current class, because you know it cannot be used anywhere else. You can rename it on a whim, split it in two, or delete it entirely — the kitchen is yours.

There is a second, quieter benefit: discoverability. A class with thirty public methods is like a menu with thirty pages — new teammates cannot tell the dishes from the kitchen steps. Hide the helpers, and the menu that remains is the real contract: short, intentional, learnable.

College corner — information hiding: this refactoring is the everyday face of one of the oldest, deepest ideas in software engineering. In 1972, David Parnas argued that modules should be split not by the steps of the processing, but by design decisions each module hides from the others. A module's secret might be a data structure, an algorithm, a file format — and the module's public interface should reveal what it does while concealing how. The payoff Parnas predicted is exactly what we see here: when the "how" is hidden, it can change without a ripple. Every private keyword is a tiny act of Parnas-style design — you are declaring "this is my module's secret; depend on my menu, not my kitchen." The same idea scales up: classes hide methods, packages hide classes (C# internal), services hide databases behind APIs. Coupling shrinks at every level for the same reason — outsiders physically cannot depend on what they cannot see.

💡

A useful daily habit: make every new method private by default, and promote it to public only when a real outside caller appears. It is painless to widen visibility later — the compiler never complains about that direction. Narrowing it later, after strangers have started calling, is the hard direction. Start hidden; earn publicity.

Visibility is not a one-time decision but a lifecycle. Here is how a well-managed method moves through it:

Figure 2: The visibility lifecycle of a method — publicity is earned, not default

Notice the arrows pointing back down — that is Hide Method, and notice that deletion is only ever safe from the Private state, where the compiler can prove nobody calls it.

🔍 When do we need it?

Watch for these signs:

  1. A public method whose callers all live in its own class. Run Find Usages. If every call comes from inside, the public keyword is a false promise — the method is a kitchen step wearing a menu badge.
  2. A bloated public surface. A Large Class with dozens of public methods usually contains many helpers that leaked out. Hiding them is the gentlest first step in taming such a class — no logic moves, yet the class becomes dramatically easier to understand.
  3. Public methods nobody calls at all. Often these were exposed "in case someone needs it someday" — the Speculative Generality smell. Hide them now; delete them later if they stay unused.
  4. Fear of renaming. You want to give a helper a better name but hesitate because "something somewhere might call it." That hesitation is the cost of false publicity. Hide the method, and renaming becomes free.
  5. After other refactorings leave leftovers. Refactoring creates helpers: when you cure a Long Parameter List by introducing a parameter object, or split up Data Clumps, the old building-block methods often stop being needed from outside — but stay public out of inertia. Likewise, as a class grows richer behaviour, low-level getters and setters that outsiders once needed can frequently be hidden. Sweep through after every big refactoring and lower what no longer needs to be open.

When is it not the right move? When real outside callers exist and are legitimate — then the method genuinely is part of the menu. When subclasses need it — use protected, not private; do not over-tighten. And when frameworks call the method invisibly (reflection, DI, serialization callbacks) — hiding it may compile fine and still break the app at runtime. Verify before you hide.

When one team audited their ReportCard class, the menu-to-kitchen ratio looked like this:

Figure 3: Audit of a typical bloated class — most public methods were never really menu items

More than half the "public API" was kitchen steps, and a sixth was pure speculative generality — public methods with zero callers anywhere.

A quick chart to judge any individual method — who calls it, and how much you still expect its internals to change:

Figure 4: Where should this method sit on the visibility ladder?

Reading it: taxFor — internal-only and frequently tweaked — is a "hide it today." The pricing chain has outside callers re-cooking the recipe, so first give them a proper dish (a new public method), then hide the steps. bookTicket is the honest menu: outside callers, stable contract.

🪄 Before and after at a glance

Before — every kitchen step on the menu:

// BEFORE: 6 public methods — but only 2 are the real menu
class BiryaniKitchen {
  public orderBiryani(plates: number): string { /* ... */ return "served"; }
  public orderKebab(plates: number): string   { /* ... */ return "served"; }
 
  public prepareSecretMasala(): string   { return "masala"; }   // leaked!
  public marinateMeat(masala: string): string { return "marinated"; }
  public layerRice(meat: string): string { return "layered"; }
  public slowCookOnDum(pot: string): string { return "biryani"; }
}
 
// somewhere in ordering-screen code, a "clever" shortcut appeared:
const masala = kitchen.prepareSecretMasala();   // a customer inside the kitchen

After — the menu shows dishes; the steps are invisible:

// AFTER: 2 public methods (the menu), 4 private steps (the kitchen)
class BiryaniKitchen {
  public orderBiryani(plates: number): string {
    const masala = this.prepareSecretMasala();
    const meat = this.marinateMeat(masala);
    const pot = this.layerRice(meat);
    return this.slowCookOnDum(pot);
  }
 
  public orderKebab(plates: number): string { /* ... */ return "served"; }
 
  private prepareSecretMasala(): string  { return "masala"; }
  private marinateMeat(masala: string): string { return "marinated"; }
  private layerRice(meat: string): string { return "layered"; }
  private slowCookOnDum(pot: string): string { return "biryani"; }
}
 
// kitchen.prepareSecretMasala();   // compile error — kitchen door is closed

The class does exactly what it did before — not one line of logic changed. But its shape changed completely: a reader now sees instantly that this class offers two things to the world, and everything else is internal cooking.

Watch one order travel through the system — the customer's conversation is short; the kitchen's conversation with itself is long, and invisible:

Figure 5: Farhan orders one dish; the four kitchen steps happen behind the door

🪜 Step-by-step, the safe way

Hide Method looks like a one-word edit, and mechanically it is. The care goes into verifying before and after. Here is the full recipe.

Step 1: List the candidates. Go through the class and ask of each public method: "Is this a dish, or a cooking step?" Helpers called only by other methods of the same class are your candidates.

Step 2: Find every caller of the candidate. Use Find Usages / Find All References across the whole solution, including test projects and any dependent repositories. Sort callers by where they live:

  • All in the same class → candidate for private.
  • Same class plus subclasses → candidate for protected.
  • Same assembly/module only (C#) → candidate for internal.
  • Genuine outside callers → stop; this method really is on the menu.

Step 3: Check for invisible callers. Compile-time search cannot see reflection, dependency injection, serializer callbacks, or framework lifecycle hooks. Search the codebase for the method's name as a string, and check route tables, DI registrations, and attributes.

Step 4: Lower the modifier — to the most restrictive level that still serves every legitimate caller.

// the entire edit:
private taxFor(amount: number): number { /* unchanged body */ }

Step 5: Compile. The compiler now re-checks every call site for you. If an out-of-scope caller you missed exists, you get a clean, complete error list. For each one, choose deliberately: move that calling logic inside the class, expose a narrower intentional public method that does what the caller actually wanted, or accept that the method must stay visible.

Step 6: Run the tests. If a test was calling the now-private method directly, rewrite it to test through the public behaviour instead — order the dish and taste it; do not barge in and lick the masala spoon.

Step 7 (the reward): improve the hidden method freely. This is why we hide. Now that taxFor is private, rename it to gstFor, change its parameters, split it — the compiler guarantees the blast radius is one file.

Sweep by sweep, the public surface of a class shrinks toward its honest size:

Figure 6: Public method count across three hiding sweeps of one class

The curve flattens at four — and that is correct. The goal is not zero public methods; it is a menu exactly as long as the promises the class genuinely intends to keep.

⚠️

The dangerous callers are the ones the compiler cannot see. Methods invoked by reflection, called by a DI container, used as serialization hooks, or bound by name in a framework (route handlers, lifecycle callbacks, test fixtures) will not appear in Find Usages — and hiding them produces code that compiles perfectly and fails at runtime. Before hiding anything in a framework-managed class, check the framework's documentation, and grep for the method name as a string. And on a published library, remember: reducing visibility is a breaking change for consumers — it belongs in a major version, not a patch.

🏗️ A bigger real-life example

A school in Bhopal runs report-card software. Over the years, every helper inside ReportCard was made public "just in case," and outsiders began depending on the kitchen steps:

// BEFORE: ten public methods; the real menu is only three
class ReportCard {
  public generate(studentId: string): string { /* ... */ return "card"; }
  public emailToParent(studentId: string): void { /* ... */ }
  public classTopperList(): string[] { /* ... */ return []; }
 
  // Helpers that leaked onto the menu:
  public fetchMarks(studentId: string): number[] { /* ... */ return []; }
  public computeAverage(marks: number[]): number { /* ... */ return 0; }
  public assignGrade(avg: number): string { /* ... */ return "A"; }
  public formatHeader(studentId: string): string { /* ... */ return ""; }
  public formatMarksTable(marks: number[]): string { /* ... */ return ""; }
  public applyGraceMarks(marks: number[]): number[] { /* ... */ return marks; }
  public roundOffAverage(avg: number): number { /* ... */ return avg; }
}

Sure enough, outside code started cooking for itself:

// In the sports-day module, far from ReportCard:
const marks = reportCard.fetchMarks(studentId);
const avg = reportCard.computeAverage(reportCard.applyGraceMarks(marks));
const grade = reportCard.assignGrade(avg);   // recomputed the recipe by hand!

This is Farhan mixing masala at the special counter. The sports module now has its own copy of the grading recipe — and when the school changes the grace-marks rule, the report card updates but the sports list silently disagrees. Two answers to "what is Asha's grade?" in one program.

The cure has two parts. First, give the outside caller what it actually wanted — a dish, not ingredients:

class ReportCard {
  // NEW: the dish the sports module was trying to cook for itself
  public gradeFor(studentId: string): string {
    const marks = this.applyGraceMarks(this.fetchMarks(studentId));
    return this.assignGrade(this.roundOffAverage(this.computeAverage(marks)));
  }
  // ...
}
 
// Sports module, after:
const grade = reportCard.gradeFor(studentId);   // one honest order from the menu

Second, hide every step:

// AFTER: menu of four; kitchen of seven
class ReportCard {
  public generate(studentId: string): string { /* ... */ return "card"; }
  public emailToParent(studentId: string): void { /* ... */ }
  public classTopperList(): string[] { /* ... */ return []; }
  public gradeFor(studentId: string): string { /* ... */ return "A"; }
 
  private fetchMarks(studentId: string): number[] { /* ... */ return []; }
  private computeAverage(marks: number[]): number { /* ... */ return 0; }
  private assignGrade(avg: number): string { /* ... */ return "A"; }
  private formatHeader(studentId: string): string { /* ... */ return ""; }
  private formatMarksTable(marks: number[]): string { /* ... */ return ""; }
  private applyGraceMarks(marks: number[]): number[] { /* ... */ return marks; }
  private roundOffAverage(avg: number): number { /* ... */ return avg; }
}

Count the wins:

  • One recipe, one kitchen. The grading rules now run in exactly one place. When the grace-marks policy changes, every screen in the school agrees, automatically.
  • A learnable class. A new teammate reading ReportCard sees four public methods and understands the class's whole job in ten seconds.
  • Freedom inside. Tomorrow the cook can rename computeAverage to weightedAverage, merge the two formatting helpers, or rewrite grading entirely — with a guarantee that nothing outside this file can break.

The finished structure, drawn as a class diagram — the menu on top, the kitchen below, and outsiders connected only to the menu:

Figure 7: Outsiders touch the menu; the seven kitchen steps are unreachable

And here is what the change felt like for the humans involved:

Figure 8: Life with the recipe on the menu versus behind the kitchen door

⚙️ The same refactoring in C#

C# gives you a finer-grained ladder of visibility than most languages, so Hide Method can be tuned precisely:

public class ReportCard
{
    // The menu — the promise to the whole world:
    public string Generate(string studentId) { /* ... */ return "card"; }
 
    // Kitchen step, used only inside this class:
    private decimal ComputeAverage(IReadOnlyList<int> marks)
        => marks.Count == 0 ? 0 : (decimal)marks.Average();
 
    // Needed by ReportCard subclasses (e.g., KindergartenReportCard), nobody else:
    protected virtual string AssignGrade(decimal average)
        => average >= 90 ? "A+" : average >= 75 ? "A" : "B";
 
    // Shared by other classes in THIS library, but not part of the public API:
    internal string FormatHeader(string studentId) { /* ... */ return ""; }
 
    // Subclasses within this assembly only — the strictest blend:
    private protected int[] ApplyGraceMarks(int[] marks) { /* ... */ return marks; }
}

Reading the ladder from most hidden to most open:

ModifierWho can call itKitchen picture
privateThis class onlyThe head cook's personal masala dabba
private protectedSubclasses, same assembly onlyFamily recipe — shared with your own branch outlets only
protectedThis class + subclasses anywhereRecipe handed down to franchise kitchens
internalAny code in the same assemblyOpen to all staff of this restaurant, invisible to customers
protected internalSame assembly OR any subclassStaff plus all franchises
publicEveryone, foreverPrinted on the menu — a promise

Two C#-specific notes worth remembering:

  • internal is Hide Method's best friend in library code. A helper used across several classes of your NuGet package can be internal: every class inside the package sees it; no consumer outside ever does. The package's public API stays a clean, small menu. (If your own test project needs access, [assembly: InternalsVisibleTo("MyLib.Tests")] opens the kitchen door to exactly one trusted guest.)
  • Default visibility already helps you. In C#, members with no modifier are private by default — the language itself believes in "start hidden." Hide Method often just restores what the default would have given you.
// Consumer code, after hiding:
var card = new ReportCard();
card.Generate("BPL-0421");        // fine — on the menu
// card.ComputeAverage(...);      // CS0122: inaccessible due to protection level
// card.FormatHeader(...);        // CS0122 from outside the assembly

That CS0122 error is not the compiler being rude. It is the kitchen door, doing its job.

Python deserves a special note, because Python famously has no enforced private — its kitchen door is a curtain, not a lock. The community relies on naming conventions, and they work surprisingly well:

# Python: hiding by convention — the leading underscore
class ReportCard:
    def generate(self, student_id: str) -> str:          # menu
        marks = self._apply_grace_marks(self._fetch_marks(student_id))
        return self._format_card(student_id, marks)
 
    # One underscore: "kitchen step — please do not call from outside."
    def _fetch_marks(self, student_id: str) -> list[int]: ...
    def _apply_grace_marks(self, marks: list[int]) -> list[int]: ...
    def _format_card(self, student_id: str, marks: list[int]) -> str: ...
 
    # Two underscores trigger name mangling — a stronger hint, rarely needed:
    def __secret_weighting(self, marks: list[int]) -> float: ...

Linters flag outside calls to _underscore methods, from module import * skips them, and IDE autocomplete de-emphasises them. The lesson transfers both ways: even where the compiler cannot lock the door, declaring what is kitchen and what is menu still pays — and in languages that can lock it, there is no reason not to.

🧰 IDE support

Tools not only perform this refactoring — they actively suggest it:

  • JetBrains Rider / ReSharper: run a code inspection and look for "Member can be made private"-style suggestions — the analyzer detects members whose actual usage is narrower than their declared visibility and offers a one-click fix (Alt+Enter). Find Usages (Shift+F12 / Alt+F7) gives you the caller map for Step 2.
  • IntelliJ IDEA (Java): the classic inspection "Declaration access can be weaker" highlights exactly these methods, and Refactor → Change Signature or the quick-fix lowers the modifier with a safety check across the project.
  • Visual Studio: Find All References (Shift+F12) answers "who calls this?"; Roslyn analyzers flag unused members (e.g., IDE0051 for unused private members — handy for the follow-up question "now that it is hidden, is it used at all?"). Code-style rules can also require explicit accessibility modifiers so visibility is always a conscious choice.
  • VS Code (TypeScript): Find All References (Shift+F12) plus the compiler are enough: change public to private, and every out-of-scope caller appears in the Problems panel as your complete to-do list.

One habit multiplies the value of all these tools: treat every analyzer suggestion of "can be made private" as a small gift — each one is a promise you can take back for free.

⚖️ Benefits and risks

BenefitsRisks / Costs
Smaller, intentional public API — newcomers learn the class from its menu, not its kitchenHiding a method that reflection, DI, or a framework calls by name breaks the app at runtime while compiling cleanly
Hidden methods can be renamed, reshaped, or deleted with a guaranteed one-file blast radiusOver-tightening to private when subclasses need the method breaks inheritors — use protected deliberately
Outsiders stop re-cooking internal recipes — logic stays in one place and cannot drift into duplicatesOn a published library, reducing visibility is a breaking change requiring a major version bump
The compiler enforces the boundary forever — no code review needed to keep strangers out of the kitchenTests calling the method directly must be rewritten to go through public behaviour (usually an improvement, but it is work)
Each hidden helper reduces the promises the class must keep — change becomes cheaper over timeVisibility alone does not fix a badly designed class; a hidden mess is still a mess (pair with Extract Class when needed)

🩺 Which smells does it cure?

SmellHow Hide Method helps
Large ClassA bloated public surface shrinks to the true contract; the class becomes readable even before deeper surgery
Speculative Generality"Someone might need it someday" methods go back inside; if still unused, the unused-member analyzer flags them for deletion
Duplicate CodeOutsiders can no longer copy a recipe step by step from public helpers — they must order the one real dish, so the logic stays single
Inappropriate IntimacyClasses lose the ability to poke at each other's internals; the only conversations left are through intentional interfaces
Dead CodeHiding is step one of safe deletion: once private, an unused helper is provably unreachable and can be removed without fear

The whole idea on one map — Hide Method sits in the same locking-down family as Remove Setting Method and Encapsulate Collection:

Figure 9: Hide Method at a glance — what to hide, how, and why

📝 Quick revision box

+=================================================================+
|                 HIDE METHOD — REVISION CARD                     |
+=================================================================+
| SMELL SIGN : public method whose callers all live in its        |
|              own class (a kitchen step on the menu)             |
| PICTURE    : secret masala stays INSIDE the kitchen;            |
|              customers only order dishes from the menu          |
+-----------------------------------------------------------------+
| THE MOVE   : 1. List helper-looking public methods              |
|              2. Find Usages across the WHOLE solution           |
|              3. Check invisible callers: reflection, DI,        |
|                 serializers, framework hooks (grep the name!)   |
|              4. Lower to the most restrictive level that works  |
|                 private -> protected -> internal -> public      |
|              5. Compile: errors = missed callers, fix each      |
|              6. Test via PUBLIC behaviour, not private spoons   |
|              7. Reward: rename/reshape the helper freely        |
+-----------------------------------------------------------------+
| C# LADDER  : private | private protected | protected |          |
|              internal | protected internal | public             |
| REMEMBER   : every public method is a PROMISE —                 |
|              start hidden, earn publicity                       |
+=================================================================+

🏋️ Practice exercise

A ticket-booking system for a Pune theatre has this class:

class TicketCounter {
  public bookTicket(showId: string, seats: number): string { /* ... */ return "PNR"; }
  public cancelTicket(pnr: string): void { /* ... */ }
 
  public findFreeSeats(showId: string): number[] { /* ... */ return []; }
  public calculateBasePrice(showId: string, seats: number): number { /* ... */ return 0; }
  public addWeekendSurcharge(price: number, showId: string): number { /* ... */ return price; }
  public applyGstAt18Percent(price: number): number { /* ... */ return price; }
  public generatePnr(): string { /* ... */ return ""; }
  public sendSms(phone: string, text: string): void { /* ... */ }
}
 
// Found elsewhere in the codebase:
// 1. The kiosk screen calls findFreeSeats(showId) to show a seat map.
// 2. The "group booking" module computes prices by chaining
//    calculateBasePrice -> addWeekendSurcharge -> applyGstAt18Percent itself.
// 3. Nothing anywhere calls generatePnr or sendSms except bookTicket.

Your tasks:

  1. Classify all eight methods: menu (truly public), kitchen step (hide), or "needs investigation." Justify each in one line, and place each on the Figure 4 quadrant chart.
  2. generatePnr and sendSms have no outside callers. Hide them, compile, and confirm the blast radius. Which one might a framework or scheduled job plausibly call by name — and how would you check before hiding?
  3. The group-booking module is re-cooking the pricing recipe (finding #2). Design one honest public method — name it and write its signature — that lets you hide all three pricing steps. What bug category does this eliminate when GST rates change?
  4. findFreeSeats has a real outside caller (the kiosk). Should it stay public, or should the kiosk get a different, narrower method? Argue both sides in two sentences each.
  5. Draw the Figure 2 state diagram for applyGstAt18Percent: which states has it passed through in its life so far, and which transition are you about to perform?
  6. Bonus (C#): rewrite the finished class sketch using C# modifiers. Put the pricing helpers at private, imagine a sister class OnlineCounter in the same assembly that needs generatePnr, and choose the correct modifier for it. Add the InternalsVisibleTo line you would use for the test project. Bonus (Python): rewrite the class using underscore conventions and say what enforcement you lose.
  7. College-corner question: in Parnas's terms, what is the "secret" of TicketCounter that its module should hide — and is there evidence the class is hiding more than one secret? After hiding, the class still does booking, pricing, PNR generation, and SMS sending. Is this one kitchen or two? Which refactoring would you reach for next?

Frequently asked questions

How do I know for sure that nobody outside the class calls the method?
Do not guess — let the tools answer. Use your IDE's Find Usages / Find All References on the method and check where every caller lives. Then make the change and compile: the compiler will flag any caller you missed as an error, which is exactly the safety net this refactoring relies on. Be extra careful with reflection, dependency injection, serializers, and framework callbacks — those callers are invisible to the compiler, so search for the method's name as a string too.
Should my tests be allowed to call private methods?
Prefer not. A test that calls a private helper directly is gluing itself to an implementation detail — the very thing hiding is meant to free up. Test the private logic through the public method that uses it: order the biryani and taste it, rather than barging into the kitchen to inspect the masala. If a private helper feels so complex that it demands its own direct tests, that is often a sign it wants to be extracted into its own small class with its own public API.
What is the difference between private, protected, and internal — which should I pick?
Pick the most restrictive level that still serves every legitimate caller. private: only this class — the default choice for helpers. protected: this class and its subclasses — use only when an inheritor genuinely needs it. internal (C#): any code in the same assembly but nothing outside — perfect for helpers shared within one library. The rule of thumb is to start at private and loosen only when a real caller forces you to.
Is hiding a method a breaking change?
Inside your own application, no — the compiler checks every caller and your IDE updates nothing because nothing outside used it. On a published library, yes: any external code calling that method will fail to compile against the new version, so reducing visibility needs a major version bump and a migration note. This is exactly why keeping the public surface small from day one is so valuable — every method you expose today is a promise you must keep tomorrow.
My framework calls a method via reflection or dependency injection even though no code references it. Can I still hide it?
Sometimes not. Lifecycle hooks, serializer callbacks, DI-injected methods, and route handlers may be invoked by name or attribute at runtime, with zero compile-time references. Hiding them can break the app while still compiling cleanly. Before hiding, check the framework's requirements; many frameworks can call private members, but some cannot. When a method must stay public only for the framework, mark the intent clearly with comments or attributes so no human treats it as part of the real API.

Further reading

Related Lessons