Skip to main content

My 'Aha!' Moment: Why Public/Private Actually Matters (And I Was Wrong About Security)

Β· 14 min read
Mahmut Salman
Software Developer

"Why make something private? Just make everything public. If the coder knows which method to call, they call it. Problem solved, right?" That was me a week ago. I was so wrong. Here's my journey from "public/private is just fancy nonsense" to "holy crap, this actually makes sense!" 🀯

The Question That Started It All​

I was staring at this code:

@Component
public class JwtUtil {
public String generateToken(String email) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, email); // ← Calls private method
}

private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
// ... token building stuff
.compact();
}
}

My thought: "Why have two methods? Why make createToken() private? Just make everything public and let developers figure it out!"

My mentor: "Try reading just the public methods of a class. What do you understand?"

Me: "Uh... I guess I understand what the class does?"

Mentor: "Exactly. Now you're thinking like a senior developer."

Me: 🀯


Revelation #1: Public Methods Tell a Story​

The First "Aha!" Moment​

I tried an experiment. I looked at ONLY the public methods of JwtUtil:

public String generateToken(String email)
public Boolean validateToken(String token, String email)
public String extractEmail(String token)

What I understood in 30 seconds:

  • This class generates JWT tokens
  • It validates tokens
  • It extracts email from tokens

That's it! I didn't need to read ANY implementation code!

Then I Tried the AuthController​

@RestController
@RequestMapping("/api/auth")
public class AuthController {
@PostMapping("/register")
public ResponseEntity<RegisterResponse> register(...) { ... }

@PostMapping("/login")
public ResponseEntity<LoginResponse> login(...) { ... }

@PostMapping("/unlock")
public ResponseEntity<UnlockResponse> unlockAccount(...) { ... }

@GetMapping("/users")
public List<UserResponse> getAllUsers() { ... }
}

What I understood in 1 minute:

  • POST /register β†’ Register new user
  • POST /login β†’ Login user
  • POST /unlock β†’ Unlock account
  • GET /users β†’ Get all users

I understood the ENTIRE authentication API without reading a single line of implementation!

The Pattern Clicked​

Reading ALL methods (public + private): Hours Γ— 100 classes = weeks 😫

Reading ONLY public methods: Minutes Γ— 100 classes = few hours 🎯

That's the 80/20 rule in action! 80% understanding with 20% of the time.


Revelation #2: "Everything Public" Creates Confusion​

My Old Mindset​

// Me: "Just make EVERYTHING public!"
public class JwtUtil {
public String generateToken(String email) { ... }
public String extractEmail(String token) { ... }
public Claims extractAllClaims(String token) { ... } // Exposed!
public Boolean isTokenExpired(String token) { ... } // Exposed!
public Date extractExpiration(String token) { ... } // Exposed!
public <T> T extractClaim(String token, Function<Claims, T>...) { ... } // Exposed!
}

// Me using it later: "Wait... which method do I actually use?"
// "Do I call extractAllClaims? Or validateToken?"
// "If I call both, am I doing it right?"
// "Help! 😡"

The Better Design​

// Proper encapsulation
public class JwtUtil {
// PUBLIC: The official API
public String generateToken(String email) { ... }
public Boolean validateToken(String token, String email) { ... }
public String extractEmail(String token) { ... }

// PRIVATE: Internal implementation
private String createToken(Map<String, Object> claims, String subject) { ... }
private Claims extractAllClaims(String token) { ... }
private Boolean isTokenExpired(String token) { ... }
}

// Me using it: "Oh! Just 3 methods. Crystal clear!"
// generateToken() β†’ create
// validateToken() β†’ verify
// extractEmail() β†’ read
// Done! πŸŽ‰

My second "Aha!" moment: Public methods are like a restaurant menu - they show you what you CAN order. Private methods are the kitchen - you don't need to know how they make the food!


Revelation #3: Generics Aren't That Scary​

The Confusing Part​

I saw this everywhere:

ResponseEntity<UnlockResponse>
ResponseEntity<LoginResponse>
ResponseEntity<String>
ResponseEntity<List<User>>

My thought: "What's with the <> brackets? Why different types? Is this magic?"

The Light Bulb Moment​

My mentor: "Think of ResponseEntity<T> like a delivery box. What's in the box?"

πŸ“¦ Delivery Box (ResponseEntity)
- Same box every time
- Has tracking info (HTTP status code)
- Has shipping labels (HTTP headers)

<T> = Package Contents (what's inside)
- Could be UnlockResponse
- Could be LoginResponse
- Could be String
- Could be List<User>

Same box, different contents!

Before Generics: The Dark Ages​

// OLD WAY (before generics)
List myList = new ArrayList();
myList.add("Hello");
myList.add(123);
myList.add(new LoginResponse(...));

// Getting things out:
String text = (String) myList.get(0); // Manual cast
Integer number = (Integer) myList.get(1); // Manual cast
String wrong = (String) myList.get(1); // πŸ’₯ CRASH at runtime!

Problem: You're responsible for remembering what's inside. Mess up? Runtime crash! (in production! πŸ”₯)

With Generics: The Modern Way​

// MODERN WAY
List<String> stringBox = new ArrayList<>();
stringBox.add("Hello"); // OK
stringBox.add(123); // ❌ COMPILER ERROR! Won't even compile

String text = stringBox.get(0); // No casting needed!

Benefit: Compiler catches mistakes BEFORE you run code!

How ResponseEntity<T> Works​

// Spring's class (simplified):
public class ResponseEntity<T> {
private T body; // ← The placeholder!
private HttpStatus status;
private HttpHeaders headers;
}

// When you use it:
ResponseEntity<UnlockResponse> response = ResponseEntity.ok(unlockData);
// ^^^^^^^^^^^^^^^
// Fills in the T placeholder

// Now T is replaced everywhere:
// public UnlockResponse getBody() { return body; }

The angle brackets are just templates! You fill in what goes inside the box.

My third "Aha!" moment: <T> is just saying "I'll tell you the type later when I use this class."


Revelation #4: My Security Assumptions Were Completely Wrong​

My Original (Naive) Thinking​

"My code runs on my server, in my RAM. Only my app can access it. Nobody can read my code. Even if they could, I didn't share credentials or JWT secrets. So I'm safe, right?"

My mentor: "Let me blow your mind..."

Reality Check #1: All Code Shares the Same JVM​

Java Virtual Machine (Single Process)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ YOUR CODE (Running) β”‚
β”‚ β”œβ”€ AuthService β”‚
β”‚ β”œβ”€ JwtUtil β”‚
β”‚ └─ All your classes β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ LIBRARY CODE (Also Running) β”‚
β”‚ β”œβ”€ Spring Framework β”‚
β”‚ β”œβ”€ Jackson JSON library β”‚
β”‚ β”œβ”€ log4j logging β”‚
β”‚ └─ [MALICIOUS CODE IF HACKED] ← OH NO! β”‚
β”‚ β”‚
β”‚ ALL SHARING SAME MEMORY! β”‚
β”‚ They can call each other's methods! β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Me: "Wait... you mean a hacked library can call MY methods?"

Mentor: "Yep! They're in the same JVM. It's like sharing an apartment - your roommate can access your kitchen."

Me: 😱

Reality Check #2: Your JAR Can Be Decompiled​

Me: "But they'd need my source code!"

Mentor: "Your JAR file IS your source code. Watch this..."

# Step 1: Your JAR is just a ZIP file
unzip ecommerce-app.jar

# Step 2: Upload .class files to online decompiler
# https://www.javadecompilers.com/

# Step 3: See ALL your code!
public class JwtUtil {
public String generateToken(String email) { ... }
private String createToken(...) { ... }
// Everything visible!
}

Me: "WHAT?! But... but... my private methods!"

Mentor: "Private methods don't HIDE anything. They just SIGNAL intent to developers."

Me: 🀯 (mind blown again)

Reality Check #3: How Attackers Actually Access Your Code​

Scenario 1: Compromised Dependency (Most Common!)​

1. You add library to pom.xml:
<dependency>
<groupId>com.malicious</groupId>
<artifactId>innocent-library</artifactId>
</dependency>

2. Attacker compromised this library's servers

3. Your app downloads the malicious version

4. Malicious code now runs IN YOUR JVM!

5. It can:
- Create instances of your classes
- Call your public methods
- Use reflection to call private methods
- Read your data

Real example: log4j vulnerability (2021) affected MILLIONS of servers worldwide!

Scenario 2: Reflection Can Call Private Methods​

// Inside malicious library:
Class<?> jwtClass = Class.forName("com.ecommerce.app.security.JwtUtil");

// Get PRIVATE method
Method privateMethod = jwtClass.getDeclaredMethod("extractAllClaims", String.class);

// Bypass private access! 🚨
privateMethod.setAccessible(true);

// Call it!
Object result = privateMethod.invoke(jwtUtilInstance, fakeToken);

Me: "So private methods CAN be called by attackers?!"

Mentor: "Yes. But that's not the point of private methods..."

What Private Methods ACTUALLY Protect Against​

My misconception:

"Private = Hidden from attackers"

Reality:

Private methods protect against:
βœ… Mistakes by other developers
βœ… Accidental security bypasses
βœ… Future maintainers doing wrong things
βœ… Internal malicious code misusing methods

Private methods DON'T protect against:
❌ External attackers with valid credentials
❌ Network attacks (need HTTPS for that)
❌ Stolen API keys (need secrets management)

My fourth "Aha!" moment: Private/public is Layer 1 of defense. You need multiple layers!


Revelation #5: Security is Layers, Not One Thing​

Defense in Depth (The Swiss Cheese Model)​

Layer 1: Code Structure (public/private)
└─ Prevents accidental misuse

Layer 2: Validation (validateToken)
└─ Checks signature is valid

Layer 3: Authentication
└─ Verifies user identity

Layer 4: Authorization
└─ Checks user permissions

Layer 5: HTTPS/TLS
└─ Protects data in transit

Layer 6: Secret Management
└─ Protects JWT_SECRET

Layer 7: Monitoring
└─ Detects suspicious activity

Layer 8: Regular Updates
└─ Patches vulnerabilities

ALL layers work together!
If one fails, others still protect.

Swiss cheese analogy: Each layer has holes. But when stacked, holes don't line up. Attackers can't get through!

How Private Methods Help (Even Though Not Perfect)​

// WITH everything public:
public Claims extractAllClaims(String token) { ... }
public Boolean isTokenExpired(String token) { ... }

// Developer might do:
Claims claims = jwtUtil.extractAllClaims(forgedToken); // No validation! 🚨
Boolean expired = jwtUtil.isTokenExpired(token); // Might crash!

// WITH proper encapsulation:
public Boolean validateToken(String token, String email) { ... }

// Developer MUST do:
if (jwtUtil.validateToken(token, email)) { // Forced validation! βœ…
String email = jwtUtil.extractEmail(token); // Now safe!
}

Private methods guide developers down the safe path!


Revelation #6: Two Methods Are Better Than One​

The Question I Asked​

"Why separate generateToken() and createToken()? Isn't that just overengineering?"

The Restaurant Analogy That Explained Everything​

BAD WAY (No separation):

Chef makes Burger from scratch:
1. Cut bread
2. Cook meat
3. Add lettuce
4. Add tomato
5. Wrap it up

Chef makes Sandwich from scratch:
1. Cut bread
2. Cook chicken
3. Add lettuce
4. Add tomato
5. Wrap it up

Chef makes Wrap from scratch:
1. Cut tortilla
2. Cook meat
3. Add lettuce
4. Add tomato
5. Wrap it up

Problem: Repeating steps for every dish! 😫

GOOD WAY (Reusable assembly line):

Assembly Line (private):
1. Get base (bread/tortilla)
2. Cook protein
3. Add toppings
4. Wrap it up

Public methods call assembly:
- makeBurger() β†’ calls assembly with "bread + meat"
- makeSandwich() β†’ calls assembly with "bread + chicken"
- makeWrap() β†’ calls assembly with "tortilla + meat"

Result: One reusable process! 🎯

Applied to Code​

// ONE METHOD (bad):
public String generateToken(String email) {
return Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date(...))
.setExpiration(new Date(...))
.signWith(key, HS256)
.compact();
}

// Problem: Need admin token? Password reset token? API token?
// Copy-paste this code 4 times! ❌ Violates DRY principle
// TWO METHODS (good):
public String generateToken(String email) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, email); // Reuse!
}

public String generateAdminToken(String email, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", role);
return createToken(claims, email); // Reuse same builder!
}

public String generatePasswordResetToken(String email) {
Map<String, Object> claims = new HashMap<>();
claims.put("type", "password_reset");
return createToken(claims, email); // Reuse again!
}

private String createToken(Map<String, Object> claims, String subject) {
// Token building logic (written ONCE!)
return Jwts.builder()...
}

// Benefits:
// βœ… No code duplication
// βœ… Easy to add new token types
// βœ… Change JWT algorithm once, everywhere updates

My fifth "Aha!" moment: One public method for each use case, one private method for the technical details!


The 5 Lessons That Changed How I Code​

Lesson 1: Public Methods Define the Contract​

"Here's what you CAN do with this class. Everything else is implementation details you don't need to worry about."

Before: I'd read every method to understand a class. After: I read only public methods - 5x faster understanding!

Lesson 2: Generics Provide Compile-Time Safety​

"<T> isn't magic - it's just a placeholder for 'I'll tell you the type later.'"

Before: I thought generics were confusing syntax. After: I realize they prevent runtime crashes by catching errors at compile time!

Lesson 3: Encapsulation Prevents Mistakes​

"Private methods don't hide from attackers - they guide developers down the correct path."

Before: I thought private was about security. After: I understand it's about preventing accidental misuse and signaling intent.

Lesson 4: Security is Layers​

"No single mechanism protects you. Stack multiple defenses."

Before: I thought "my code is safe because it's on my server." After: I understand defense in depth - multiple overlapping protections.

Lesson 5: Separate What from How​

"Public methods handle WHAT (business logic). Private methods handle HOW (technical details)."

Before: I'd put everything in one method. After: I separate concerns - easier to maintain, extend, and test!


The New Questions I'm Asking​

Now that I understand why public/private matters, I ask different questions:

When Reading Code​

  • ❌ Old: "What does this private method do?"
  • βœ… New: "What story do the public methods tell?"

When Writing Code​

  • ❌ Old: "Should I make this public so I can call it from anywhere?"
  • βœ… New: "Is this part of my class's public contract, or an implementation detail?"

When Reviewing Code​

  • ❌ Old: "Does this code work?"
  • βœ… New: "Is the public API clear? Can I understand what this class does in 30 seconds?"

When Thinking About Security​

  • ❌ Old: "My code is safe because it's private."
  • βœ… New: "What layers of defense do I have? If one fails, what protects me?"

My "Senior Developer" Moment​

Last week, a junior developer asked me:

Junior: "Why did you make createToken() private? Won't we need to call it from other classes?"

Me: "Try reading just the public methods of JwtUtil. What does the class do?"

Junior: "It generates and validates tokens..."

Me: "Exactly! That's the API. createToken() is an implementation detail. If we made it public, developers wouldn't know if they should call generateToken() or createToken() directly."

Junior: "Oh! So public methods tell the story!"

Me: πŸŽ“ (passing on what I learned!)


The Resources That Helped Me​

Books​

  • Clean Code by Robert C. Martin - Chapter on meaningful names
  • Effective Java by Joshua Bloch - Item 15: Minimize accessibility

Concepts to Study​

  • Encapsulation - Hiding implementation details
  • SOLID Principles - Especially Single Responsibility
  • Defense in Depth - Layered security
  • Generics - Type safety at compile time

Practical Exercises​

  1. Look at a library you use (Spring, Jackson) - read ONLY public methods
  2. Try using reflection to call a private method (eye-opening!)
  3. Decompile your own JAR file (scary but educational!)
  4. Refactor a class to have 3-5 public methods max

My Takeaway: Public/Private is About Communication​

The biggest revelation? Public/private keywords aren't about security or hiding.

They're about communication:

Public methods say:
"This is what I do. This is how you use me."

Private methods say:
"This is how I work internally. Don't worry about it."

It's documentation through code structure!


Questions I'm Still Exploring​

  1. When should I make something public vs private?

    • Rule of thumb: Start private, make public only when needed?
  2. How do I design a good public API?

    • 3-5 public methods per class seems right?
  3. What about protected and package-private?

    • When would I use these?
  4. How do senior developers think about API design?

    • What's their mental model?

To My Past Self (One Week Ago)​

Dear Past Me,

Yes, you're right that technically, private methods can be called via reflection. And yes, decompiling reveals your code.

But you're missing the point.

Public/private isn't about hiding from attackers. It's about:

  1. Guiding developers to use your code correctly
  2. Telling a clear story about what your class does
  3. Signaling intent - "here's the contract, everything else is internal"
  4. Preventing mistakes - making it hard to misuse your code
  5. Enabling safe refactoring - change internals without breaking users

You'll understand this better after you:

  • Spend 5 hours debugging because someone called a method they shouldn't
  • Try to understand a class with 20 public methods (good luck!)
  • Refactor a private method and see nothing breaks
  • Read just public methods and understand a whole codebase in hours instead of weeks

Trust me, this isn't "fancy nonsense." This is how professionals write maintainable code.

Future You


Your Turn!​

Try this exercise:

  1. Pick a class you're working on
  2. List ONLY the public methods
  3. Ask yourself: "If I showed this list to someone else, would they understand what this class does?"

If the answer is no, you might have:

  • Too many public methods (expose less!)
  • Methods with unclear names (rename!)
  • Missing core methods (add them!)

Share your "Aha!" moment in the comments - when did public/private finally click for you? πŸ’¬


P.S. If you're reading this and thinking "this is obvious," congratulations - you're thinking like a senior developer! But for those of us who learned "make everything public" in tutorials, this is a genuine revelation. We all start somewhere! πŸš€


Created: October 20, 2025 Mood: 🀯 Mind = Blown Coffee Consumed: 4 cups Times I Said "Aha!": 5 Times My Mentor Said "I Told You So": Too Many πŸ˜