My 'Aha!' Moment: Why Public/Private Actually Matters (And I Was Wrong About Security)
"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β
- Look at a library you use (Spring, Jackson) - read ONLY public methods
- Try using reflection to call a private method (eye-opening!)
- Decompile your own JAR file (scary but educational!)
- 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β
-
When should I make something public vs private?
- Rule of thumb: Start private, make public only when needed?
-
How do I design a good public API?
- 3-5 public methods per class seems right?
-
What about protected and package-private?
- When would I use these?
-
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:
- Guiding developers to use your code correctly
- Telling a clear story about what your class does
- Signaling intent - "here's the contract, everything else is internal"
- Preventing mistakes - making it hard to misuse your code
- 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:
- Pick a class you're working on
- List ONLY the public methods
- 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 π
