top of page

Code Rule #01: Avoid private methods

  • Writer: Mateusz Roguski
    Mateusz Roguski
  • Apr 18
  • 10 min read

Updated: May 3

Retro-style illustration of a developer peeking into a cardboard box labeled “It’s private,” symbolizing a class with private methods hiding its internal implementation.
Private methods don’t organize code — they quietly hide its problems.

Table of Contents



Introduction


Every developer knows the feeling: a method gets too long, so you break it into smaller pieces — and tuck them away as private helpers. It feels clean. It feels organized. It feels responsible.

But it’s not. In this article, we’ll explore why private methods are more dangerous than they seem — and how to design better without them.


Private hides too much

…and it buries logic instead of organizing it

At first glance, private methods seem like a smart way to clean up large functions. You take a big chunk of logic, give it a name, and tuck it away where no one else can touch it. Clean and safe, right?

Not quite.

Private methods hide behavior so deeply that even your own tests can't reach it. The only way to verify their correctness is through indirect testing — invoking public methods and hoping the private logic behaves as expected. And when something breaks, debugging becomes slower, more fragile, and more guesswork-driven.

They’re also invisible to the rest of your system. Subclasses can’t override them. Decorators can’t adapt them. Collaborators can’t reuse them. They create hard boundaries not because the logic is cohesive, but because you’ve chosen to bury it.


And while they might feel like a form of encapsulation, it's often a false sense of safety. Real encapsulation means responsibility boundaries, not just access control.


Most importantly, private methods pretend to offer clarity, but they’re really a design smell. They signal that a class is doing too much — and instead of extracting behavior into proper collaborators, we’re just sweeping it under the rug.


Private methods don’t reduce complexity. They just hide it in places you can’t reach.


They encourage bloated, do-it-all classes

…and they mute the signal that something’s wrong


Private methods give classes permission to grow out of control.


As soon as you allow a class to “organize” its internal complexity using private methods, you lose one of the most important design signals: discomfort. That discomfort — the feeling that “this class is getting too big” — is valuable. It tells you that responsibilities are bleeding together. That logic wants to break free.


But private methods mute that signal.


  • Instead of extracting a new class, we tuck another behavior into a private method.

  • Instead of surfacing a new domain concept, we bury it behind private function handleX.

  • Instead of composing, we cram.


Over time, this leads to God objects — classes that do everything, know everything, and are impossible to test, reason about, or reuse. They become black boxes of hidden logic. And because it’s all hidden behind private boundaries, the only way to change anything is to wade through all of it.


And sometimes, this bloat isn’t even justified. Developers may extract private methods preemptively — a form of over-engineering — just to feel tidy. This violates YAGNI: "You Aren’t Gonna Need It." Not every chunk of code deserves a method. If there’s no reuse, no real complexity, and no delegation — maybe it doesn’t need to exist yet.


They block composition and reuse

…and they make flexibility impossible


Private methods are dead ends.


Once you put logic into a private method, you’ve declared: “No other part of the system can use this.” That might feel safe — but it’s often just shortsighted. Because the moment you need that behavior elsewhere, you're stuck:


  • You can’t call it from another class.

  • You can’t override it in a subclass.

  • You can’t decorate it, wrap it, or adapt it.


You have two choices: duplicate it or start refactoring under pressure.


Private methods block composition. Instead of passing behavior around or assembling logic from reusable parts, you're forcing your system to go through one tightly sealed class. And if that class also uses private state or tight coupling, you’re completely locked in.


They also break inheritance. If you later extend a class and want to override or adjust a part of its behavior, private methods make that impossible. You’re forced to either duplicate logic or rewrite the method that calls the private one — often copying large sections just to change a single detail. That’s not extensibility — that’s rigidity disguised as discipline.


And they make the Law of Demeter harder to follow. Private methods often end up reaching into internal state, pulling data, transforming it, then mutating something else — instead of delegating clearly. That’s not telling — that’s asking, poking, and controlling too much from one place.


Good design is made of replaceable, composable parts. Private methods break that flow.


They can’t be tested in isolation

…and that’s a long-term maintenance risk


Private methods create friction — and friction in testing always turns into risk.


If you put logic inside a private method, you’ve made a decision: this code cannot be tested directly. That might feel like a clean encapsulation — but it’s also a wall. The only way to test that behavior now is indirectly, through the public method that happens to call it.


That’s fine for simple delegation. But when private logic contains branching, loops, or business rules, you’re testing blind.


  • You can't test edge cases in isolation.

  • You can't verify input/output directly.

  • You can’t target it with fast, focused unit tests.


You’re left writing broader integration tests — slower, more brittle, more fragile under change.


Even worse, private methods often force mocking. You end up mocking collaborators just to reach a specific private path. That encourages testing implementation over behavior. Instead of treating classes like black boxes, you start poking into their internals just to cover a few more branches. This creates a subtle, toxic dependency: your tests now depend on how things are done, not what they do.


And in many ecosystems, private methods can’t be intercepted or stubbed at all. Tools like spies, mocks, or monkey patching won’t help — meaning you're locked into writing indirect, brittle tests.


Good design exposes interfaces, not implementations.


Private methods demand that you peek behind the curtain.


They signal the wrong abstraction

…and the private method is often just a workaround


Private methods don’t just hide logic — they often hide bad design.


When a class contains too many private methods, it's usually a sign that the abstraction is wrong. Instead of clearly representing a single responsibility, the class is juggling unrelated behaviors. And to “make it work,” developers start carving out chunks of logic and stuffing them into private methods.


This isn’t a solution. It’s a workaround.


The moment you find yourself deep inside a class, writing yet another private method because you don’t want to expose this logic elsewhere, stop. You’ve likely hit a wall. That private method is telling you something important:


This logic doesn’t belong here — but I don’t know where else to put it.


That’s not a code problem. That’s a design problem.


And the longer you ignore it, the more tangled things become. Every new private method makes the class harder to refactor, harder to extend, harder to extract. And when you finally need that logic elsewhere? You’re stuck rewriting or ripping apart the internals of a class that was never meant to grow this much.


Private isn’t safe — it’s stuck

…and you’ve locked the door on your own code


It’s easy to reach for private and think: “This keeps my class clean. This keeps logic safe.”


  • Private doesn’t mean protected.

  • It doesn’t mean tested.

  • It doesn’t mean reusable.

  • It doesn’t mean maintainable.


It just means: you’ve locked the door — and future-you (or your teammates) may not have the key.


Use private when the boundary is truly internal and simple. But when behavior becomes meaningful, promote it to a role. Name it. Compose it. Test it. Make it part of the system — not a hidden detail.


Design principles that private methods often break

Private methods don’t just clutter your code — they quietly erode the architecture underneath. Here are the core design principles they often violate:

  • Single Responsibility Principle (SRP): Private methods are often a sign that a class is taking on too many roles. Instead of surfacing distinct responsibilities, it hides them in internal helpers.

  • Open/Closed Principle (OCP): You can’t extend or override private logic without rewriting the class. Private methods close behavior to adaptation.

  • Liskov Substitution Principle (LSP): When logic is private, subclasses can’t swap behavior safely — they’re forced to copy or bypass instead.

  • Interface Segregation Principle (ISP): Not violated directly, but private methods often signal that behavior isn’t expressed via interfaces when it should be.

  • Dependency Inversion Principle (DIP): Private logic hides dependencies and behaviors that should be injected or delegated. This leads to hardwired internals.

  • Law of Demeter: Private methods often control too much. They navigate and mutate internal state instead of delegating clearly.

  • Tell, Don’t Ask: Instead of telling collaborators to do work, private methods often query internal state, make decisions, and act on everything — breaking delegation.

  • You Aren’t Gonna Need It (YAGNI): Developers often extract private methods preemptively, trying to make code look cleaner without a clear need. It adds indirection with no purpose.


What to do instead

…when you feel the urge to write a private method


Private methods are often a shortcut — a way to make messy code look tidy. But what looks like cleanliness is often concealment. What looks like encapsulation is often a design flaw waiting to grow.


Instead of hiding complexity, surface structure. Instead of shrinking visibility, promote clarity. Here’s what to do instead:


Extract a class, not a method


If a chunk of logic deserves a name, it probably deserves a class.


When you isolate behavior into a dedicated object — a service, a value object, a policy — you gain clarity, reusability, and testability. You also make that behavior explicit, with a clear responsibility.


Classes should be composed of collaborators, not containers of logic.


Favor composition over containment


Don’t trap behavior inside a class just because it “belongs there.” If something is logically separate, give it its own home.


Inject collaborators. Delegate responsibility. Let each piece of logic live in a place where it can be reused, tested, and extended independently.


The job of a class is to coordinate behavior, not to bury it.


Use interfaces to expose behavior — even internally


A private method is a hidden contract. A real interface is an explicit one.


Instead of hiding behavior behind private methods, define it as a public interface — even if it’s only consumed internally. This gives you the freedom to swap implementations, test logic directly, and evolve design safely.


Interfaces promote design discipline. Private methods avoid it.


Refactor aggressively when the class starts growing


The moment you feel the need to write a private method, pause and ask:

  • Is this logic reusable?

  • Does it represent a distinct responsibility?

  • Would this be easier to test somewhere else?

If the answer is yes, don’t write a private method — extract a new class. Let the boundaries emerge early instead of delaying the inevitable refactor.


Make methods public when appropriate — and trust your design


Making a method public doesn’t mean you’ve created a leaky API. You can still mark it as internal, document its purpose, and limit its use.


Don’t hide logic just because you’re afraid someone might call it. If it’s useful, composable, and well-scoped, let it be seen.


Use visibility to communicate intent — not to block access.


Let your tests shape your design


When you write tests first, private methods quickly feel awkward.

You’ll naturally start splitting logic into smaller, independent units. You’ll inject dependencies instead of calling internals. Your design will become clearer because your tests demand it.


Tests are a mirror — and private methods cloud the reflection.


Let naming expose missing boundaries


If you’re about to write private function checkEligibilityAndCalculateDiscount(), stop.

You just named a service. Promote it. Extract it. Give it its own file, its own test, and its own lifecycle.


Good names reveal hidden objects. Don’t suppress them — extract them.


Turn off private methods — and observe the results


As a bold experiment, forbid private methods entirely. Add a static analysis rule. Set it in stone for a sprint. Watch what happens.


You’ll see:

  • Where composition is missing,

  • Where your design is collapsing under too many roles,

  • Where logic is duplicated or trapped,

  • Where you’ve been hiding instead of modeling.

This isn’t about dogma. It’s about feedback.


When nothing is allowed to hide, your architecture becomes honest.


Design principles this approach promotes


When you stop hiding logic behind private methods and start composing behavior through clear, testable boundaries, you reinforce the following principles:


  • Single Responsibility Principle (SRP): Each class owns one responsibility. No private clutter masking multiple roles.

  • Open/Closed Principle (OCP): Behavior is open to extension through composition, not modification of hidden internals.

  • Liskov Substitution Principle (LSP): Objects can be safely replaced when there are no internal rules hidden behind private barriers.

  • Dependency Inversion Principle (DIP): Code depends on abstractions, not concrete, private implementations.

  • Law of Demeter: Collaboration is explicit and shallow. Logic is delegated, not tunneled through nested private calls.

  • Tell, Don’t Ask: Objects act through clear commands, not private micromanagement of internal state.

  • Design by Contract (DbC): Behavior is exposed through clear, testable interfaces. No more implicit, unreachable expectations.

  • Explicit Architecture: Logic has a place, a name, and a visible boundary. The system is understandable by both humans and tools.

  • Testability and Observability: Every behavior can be tested in isolation without reflection, workarounds, or brittle setups.


Impact on AI-Driven Development

…and why structure matters more than ever


We’re entering an era where AI is part of the team — generating code, reviewing changes, writing tests, and even suggesting architecture. But AI doesn’t understand your system the way a human does. It doesn’t know your business rules, your intent, or your hidden design compromises.


AI infers based on patterns, structure, and visibility.

That’s exactly why private methods cause trouble.


When behavior is locked behind private methods:

  • AI tools can’t reuse logic — they treat it as inaccessible.

  • AI can’t safely refactor — it risks breaking dependencies it can’t fully track.

  • AI can’t generate meaningful tests — because it doesn’t know how to isolate or reach private paths.

  • AI can’t explain your system well — it hallucinates or oversimplifies based on incomplete context.

Private methods reduce observability, and that limits what AI can do — whether you’re using it for documentation, code generation, or analysis.


Good architecture is no longer just for humans. It’s an interface for machines, too.


If you want modern tools to help you reason about complex systems, you need to write code they can reason about. That means less hiding, more structure, and clearer boundaries.


Private methods are the opposite of that.


Summary: Why You Should Avoid Private Method


Private methods look like order — but they’re often just hidden disorder.


  • They bury logic that should be modeled, tested, and reused.

  • They suppress architecture instead of expressing it.

  • They block humans from understanding, and block machines from helping.

If your system needs to grow, adapt, or collaborate — with developers, tools, or AI —then it needs to be visible, composable, and honest.


  • Avoid private methods.

  • Expose behavior.

  • Design with intention.

Sign up to get the latest coding tips and architecture insights.

  • LinkedIn
  • X

Code, architecture, and the future of software — one post at a time.

Contact me: 

bottom of page