LESSON 1.3
Abstraction and interfaces
Give booking code a small notification contract instead of the details of an email provider.
6 min read
Booking confirmation starts by calling an email provider directly. Tests need a network stub, SMS support adds a branch, and provider-specific options spread through the booking code. The booking operation needs to announce a confirmation; it does not need to know how SMTP works.
The problem
function confirm() {
return "SMTP provider: booking confirmed";
}
// Provider details and booking behavior now change in the same function.The object boundary
An abstraction describes the capability a caller needs while leaving its implementation open. An interface is one way to express that description. The useful contract here is send(message), not a copy of every method on an email SDK. A small boundary is easier to implement honestly, test with a fake, and replace without changing the caller.
The Booking object receives a notifier when it is constructed. Confirming a booking delegates to that notifier, so construction is where the application selects email, SMS, or a test fake. This is constructor injection. It does not require a framework or a container. The application can build two booking services with different notifiers in the same process without changing a global registry or resetting shared state between tests.
A method signature is only part of the contract. State whether sending is synchronous, whether failure is returned or thrown, and whether retries may duplicate a message. In this tiny example send returns a string instead of performing network I/O. A real async provider would need an async contract, and callers would need to await it. Pretending a slow remote operation is a cheap local getter hides an important design decision.
In code
Both versions run the same small example. Java uses a single Example class; save it as Example.java.
When the boundary earns its place
Introduce the boundary where substitution has a concrete use: a test fake, an external provider, or a policy that varies independently. A single pure function can be passed directly when it is the entire contract. Creating interfaces for every private helper produces more names without making change easier. Keep provider translation in an adapter and keep booking invariants inside the booking model; the abstraction should not become a place to put unrelated work.
JavaScript and Java
Java checks that a declared implementation satisfies an interface at compile time. JavaScript uses duck typing: an object with a compatible send method can participate without declaring a relationship. Runtime validation can catch a missing method, but cannot prove that its behavior is correct. Java interfaces disappear as runtime objects; they are not automatically an HTTP protocol or a dependency-injection container. In both languages, tests must still check the semantic contract.
Try it
Dependency Injection Container LLD
Identify the objects and state the rule each one owns before adding a pattern.