- Author: Amin Boulouma, Software Engineer
- Github source code: https://github.com/aminblm/ai_systems_design_from_scratch
- Engineering Blog: https://aminblm.github.io/ai_systems_design_from_scratch/blog/
5 Hidden Benefits of Strict Encapsulation (Without Breaking Your Architecture)
In large-scale systems, the greatest source of bugs is not “bad code”—it is leaky abstractions. When a developer reaches across module boundaries to touch internal state or helper methods, they create tight coupling. This turns your architecture into a “spaghetti” of dependencies where changing one line of code causes a ripple effect of failures across the entire system.
The Real-World Scenario: Imagine you build a payment module. A different team, working on a billing service, decides to directly access the internal db_connection object in your module instead of using the provided process_payment() method. When you upgrade your database driver, their code crashes immediately. They relied on an implementation detail you assumed was private.
The Glossary
- Encapsulation: Wrapping your code in a “black box” so others can only use it the way you intended, without seeing the messy parts inside.
- Abstraction: Providing a simple button for a complex machine. You don’t need to know how the gears turn; you just need to know which button to push.
- Interface: The official set of “buttons” or rules you provide to other teams to interact with your code.
- Coupling: When two parts of a program are stuck together like glue. If you try to pull them apart, one (or both) will break.
- Client: The code or person using the module you created.
Why We Enforce Strict Interfaces
We enforce strict encapsulation because it grants the freedom to refactor. If your internal implementation is hidden behind an interface, you can completely rewrite the engine of your module without notifying the clients. They only consume the interface, so as long as the interface remains stable, their code is protected.
Implementation
Simple Example: The “Leaky” vs. Encapsulated Approach
# THE LEAKY WAY: Allowing clients to touch internals
class PaymentGateway:
def __init__(self):
self.internal_db = "sqlite://db" # Client shouldn't touch this!
# THE ENCAPSULATED WAY: Everything internal is prefixed with '_'
class PaymentGateway:
def __init__(self):
self._internal_db = "sqlite://db"
def charge(self, amount: int):
# Only public method exposed
return f"Charged {amount} via {self._internal_db}"
Complex Example: Production-Grade Module Isolation
from abc import ABC, abstractmethod
# The Interface: This is all the client ever sees
class PaymentProcessor(ABC):
@abstractmethod
def process(self, amount: int) -> bool:
pass
# The Implementation: Hidden from the client
class _StripeProcessor(PaymentProcessor):
def process(self, amount: int) -> bool:
# Complex internal logic here
return True
# The Factory: The only entry point for clients
def get_processor() -> PaymentProcessor:
return _StripeProcessor()
# Client usage: The client has no idea _StripeProcessor exists.
processor = get_processor()
processor.process(100)
Quick Reference: Strategy Selection
| Strategy | When to use | Why? |
|---|---|---|
Private Members (_) |
Internal helper variables | Prevents accidental modification by clients. |
| Interfaces (ABC) | Defining public API contracts | Forces clients to depend on behavior, not structure. |
| Factory Pattern | Object instantiation | Hides concrete classes from the consumer entirely. |
Developer Checklist
- Is every variable or method that doesn’t need to be public prefixed with an underscore?
- Have I created an Abstract Base Class (ABC) or Protocol for my module interface?
- Does my client code require knowledge of my implementation classes?
- Can I swap my entire implementation file without changing the client’s code?
Takeaways
- Hide Everything: Default to private unless there is a clear, documented need for it to be public.
- Depend on Abstractions: Always design your system so that it depends on interfaces, never on concrete implementation.
- Respect Boundaries: Treat module boundaries as walls; if you need data from another module, go through the interface.
Counter-intuitive insight: The most rigid systems are often the ones that are most “open.” By hiding your internals, you actually create more flexibility, because your internal logic can change constantly without impacting the rest of the application.