“Program to an Interface, Not an Implementation” — Explained for a Junior Developer
Every senior engineer remembers the moment this phrase finally clicked for them — and every senior engineer remembers how confusing it sounded the first ten times someone said it out loud. This guide is written the way a patient mentor would actually walk a junior developer through it: slowly, with real examples, and without any jargon left unexplained.
The Big Idea, in One Breath
Picture handing a junior developer a walkie-talkie and asking them to talk to a friend across the yard. They press the button, they speak, and their friend hears them. They don’t need to understand radio frequencies, antennas, or batteries to make that work — they just need to know “press this button to talk.” Now imagine, instead, you handed them a soldering iron and a spool of wire, and told them to wire a direct connection between two specific walkie-talkies, by hand, matching every wire to its exact colour and terminal. It would work too — for those two exact walkie-talkies. Swap either one out for a different model, and the whole careful wiring job has to be redone from scratch.
That’s the entire difference this famous phrase is trying to teach. When code depends on a walkie-talkie’s simple “press this button” promise, it’s programming to an interface. When code depends on the exact wiring of one specific device, it’s programming to an implementation. Both approaches technically work today. Only one of them survives the moment something needs to change — and in real software, something always eventually needs to change.
This is genuinely one of the best returns on investment you can offer a junior developer early in their career. It’s a simple habit, it takes about ten minutes to teach properly, and it quietly improves almost everything they write afterward, often before they’ve even fully articulated why.
It’s worth being upfront, right at the start, about why this particular phrase trips up so many junior developers the first time they hear it, despite how simple it sounds once it clicks. Part of the trouble is the word “interface” itself, which means slightly different things depending on who’s using it and in what context — sometimes a specific keyword in a programming language, sometimes a much broader idea about separating promises from details. Part of the trouble is that the phrase is usually first encountered inside a dense textbook, stated as an abstract rule with no story attached, long before the reader has personally lived through the pain the rule exists to prevent. This guide takes the opposite approach on purpose: story first, pain felt firsthand, and only then the formal name for the lesson already learned.
Where the Phrase Comes From
It’s worth telling a junior developer exactly where this idea came from, because knowing its origin makes it feel less like an arbitrary rule and more like hard-earned wisdom worth taking seriously. The phrase was written down formally in 1994, in the influential book on object-oriented design patterns by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides — four authors often nicknamed the “Gang of Four.” Buried early in that book, among the foundational principles the rest of the text builds on, sits this exact guidance: depend on an interface, not on a concrete class.
What makes this worth sharing with a junior isn’t the history trivia itself — it’s what the history reveals. This advice wasn’t a guess. It came from four experienced engineers who had already watched, many times over, what happened to codebases that ignored it: brittle, hard-to-change systems where a single small requirement shift could force changes to be made in a dozen unrelated places. The phrase survived, mostly unchanged, for over thirty years, repeated in nearly every serious book on software design since, because the problem it addresses hasn’t gone anywhere — it shows up in every single programming language, in every era, in every kind of application.
This isn’t a rule someone invented for its own sake. It’s a written-down lesson, learned the hard way, by people who’d already been burned by not following it.
It’s also worth sharing with a junior developer that this specific phrase didn’t arrive out of nowhere, even within that 1994 book — it built directly on decades of earlier thinking about abstraction and modularity that stretched back well before object-oriented programming became popular. Engineers had been discovering, independently, in language after language and project after project, that code depending on stable promises aged far better than code depending on specific details. The Gang of Four’s real contribution wasn’t inventing this idea from scratch; it was naming it clearly, writing it down precisely, and connecting it explicitly to a whole catalogue of concrete design patterns that put the idea into practice. That act of clear naming is a large part of why the phrase spread so widely and lasted so long — a good idea, well-named, travels much further than the same idea left unspoken.
What “Interface” Actually Means Here
Here’s a genuinely common point of confusion worth clearing up early, because it trips up nearly every junior developer at least once: the word “interface” in this phrase doesn’t only mean the specific interface keyword available in languages like Java, C#, or TypeScript. It means something broader — any stable promise about what a piece of code will do, kept separate from any detail about how it does it. An abstract class can serve this same purpose. In some languages, even an informal, well-documented convention can serve it. The keyword is one tool for expressing the idea; it isn’t the idea itself.
A useful way to help a junior developer test their own understanding here: ask them to describe an interface without using the word “interface” at all. If they can say something like “it’s a promise about what will happen, without saying how” — they’ve genuinely understood it. If they can only describe it by pointing at the specific keyword in their language, they’ve learned the syntax, but not yet the underlying idea, and it’s worth spending a little more time on this section before moving on.
A restaurant menu is an interface. It promises that ordering “the soup” will result in soup arriving at your table. It says absolutely nothing about which chef is cooking today, what brand of stove is being used, or which supplier delivered the vegetables this week. None of that matters to you as a customer — you only care about the promise on the menu, and that promise stays exactly the same even if the entire kitchen staff changes overnight.
It’s worth pushing this a little further with a junior developer, because the menu analogy has a genuinely useful second layer worth drawing out. A well-run restaurant doesn’t just have one menu item map to one dish — it can have several different dishes on the menu, each fulfilling a different promise, and it can also completely change how any one dish is cooked behind the scenes without ever needing to reprint the menu, as long as the dish that arrives still matches what was promised. This is precisely the same freedom a well-designed software interface gives a team: the promise stays stable and public, while the reality behind it is free to evolve, be optimised, or be entirely rebuilt, invisibly, from the perspective of anyone only ever interacting with the promise itself.
What “Implementation” Means
The other half of the phrase deserves the same careful attention. An implementation is the actual, specific, working code that fulfils a promise — the real kitchen behind the menu, doing the real cooking. In object-oriented code, this usually means one particular, concrete class: not “a payment processor” in the abstract, but specifically StripePaymentProcessor, with its own exact methods, its own exact internal logic, its own exact way of talking to a specific external service.
The moment code depends directly on that specific class — creating instances of it directly, calling its methods by name, assuming details only it happens to have — that code has quietly tied its own fate to this one particular kitchen. If the kitchen closes, changes its recipe, or gets replaced by a different one entirely, the code depending on it directly has to be found, opened, and edited, wherever it happens to live in the codebase.
Depending on an implementation — the risky versionclass OrderService {
private StripePaymentProcessor pay
= new StripePaymentProcessor();
void checkout(Order order) {
pay.chargeViaStripe(order.total);
}
}
Depending on an interface — the flexible versionclass OrderService {
private PaymentProcessor pay;
OrderService(PaymentProcessor pay) {
this.pay = pay;
}
void checkout(Order order) {
pay.charge(order.total);
}
}
Notice, in the second version, that OrderService no longer knows or cares that Stripe is involved at all. It only knows about a general promise called PaymentProcessor. Stripe could be swapped for a different provider tomorrow, and this class would never need to be touched again.
It’s worth pointing out to a junior developer exactly what changed between these two versions, because the difference is smaller than it might first appear, and that smallness is actually the point. Nothing about what checkout does has changed at all — it still charges an amount, still completes an order, still behaves identically from the outside. The only thing that changed is where the decision about “which specific payment processor” gets made — moved out of OrderService entirely, and handed instead to whoever creates an OrderService in the first place. This is worth dwelling on, because it directly answers a question junior developers often ask at this exact point: “isn’t this just moving the problem somewhere else?” The honest answer is yes, in a sense — but it’s moved to exactly one deliberate, chosen place, rather than left scattered accidentally across every piece of code that happened to need a payment processor.
A Scenario Every Junior Recognises
Rather than starting with theory, it often helps to start with a story a junior developer has almost certainly already lived through themselves, even if they didn’t have a name for it at the time.
Imagine a junior developer builds a small feature that sends a welcome email when someone signs up. They write a class that directly creates an instance of a specific email-sending library and calls its specific method to send the message. It works. The feature ships. Three months later, the team decides to switch email providers, because the old one raised its prices. Suddenly, this “small” change means searching the entire codebase for every place that specific email library was used directly, and carefully rewriting each one — a task that turns out to be far bigger, and far riskier, than anyone expected from what sounded like a simple provider swap.
This exact story — “a simple swap turned into a scary, sprawling change” — is one of the most common origin stories for a developer finally, genuinely understanding why this principle matters. It’s worth letting a junior developer feel this pain once, safely, in a low-stakes project, rather than only ever hearing about it secondhand.
It’s worth asking a junior developer, after telling this story, a simple follow-up question: “how would you have known, on day one, that this feature would need to change three months later?” The honest answer, almost always, is that they couldn’t have known for certain. This is a genuinely important point to land carefully, because it heads off a common, slightly defeatist reaction — “well, I can’t predict the future, so what’s the point of preparing for change I can’t foresee?” The point isn’t predicting the future correctly. The point is that depending on an interface costs very little upfront, and pays off enormously on the occasions change does arrive, while depending directly on a specific implementation costs nothing upfront and can cost dearly later. It’s less a bet on a specific future, and more a cheap insurance policy against several plausible ones.
Rewriting It Together, Step by Step
The most effective way to teach this principle isn’t to explain it once and move on — it’s to sit beside a junior developer and rewrite a real piece of their own tightly coupled code together, narrating each step out loud.
Find the direct dependency
Look for a line that creates a specific class directly —
new SomeSpecificClass()— sitting inside logic that shouldn’t really care which specific class it is.Ask what promise is actually needed
Strip away the specific class name and ask: “What does this code actually need to be able to do?” The answer becomes the shape of the interface.
Write the interface first
Define the promise on its own — its name, its methods, what each one takes in and gives back — without referring to the original specific class at all.
Make the old class honour the promise
Update the original specific class to implement the new interface, formally declaring that it fulfils this promise.
Change the dependency, not the logic
Update the code that was creating the specific class directly so it instead depends on the interface, received from outside if possible.
Walking through these five steps together, on real code a junior developer already wrote themselves, tends to make the idea click far faster than any amount of abstract explanation. The moment they see their own class stop caring which specific implementation it’s using — and realise it still works exactly the same as before — the lesson tends to stick permanently.
Why This Actually Matters
A junior developer deserves honest, concrete reasons, not just “because it’s good practice” — a phrase that tends to produce nodding compliance rather than genuine understanding.
One place, not everywhere
Swapping a specific implementation later means writing one new class, not hunting through the whole codebase for every place the old one was used directly.
Fake it convincingly
A lightweight, fake stand-in can be swapped in during tests, letting logic be verified without needing the real, slow, complicated dependency running too.
What, not how
Code depending on PaymentProcessor tells a reader what it needs, without burying that intent under one specific vendor’s name.
New, not edited
Supporting a second implementation later means adding a new class, not editing and re-testing code that already worked.
If a junior developer asks “but won’t this happen anyway, eventually, even without an interface?” — that’s a genuinely good question, worth answering honestly: yes, the change is still possible without an interface. The interface just decides whether that future change costs an afternoon or costs a week of anxious, error-prone searching.
It’s worth adding one more benefit to this list that junior developers often only appreciate once they’ve worked on a team for a while, rather than solo: interfaces make code reviews genuinely easier to conduct well. A reviewer looking at a change that depends on a clear, well-named interface can evaluate whether the logic correctly uses that promise, without needing to simultaneously verify every detail of some specific, unfamiliar third-party library buried underneath. This might sound like a small, secondary benefit compared to the others, but on a busy team reviewing dozens of changes a week, it adds up to a genuinely significant amount of saved time and reduced mental strain, spread across everyone involved.
Common Junior Misconceptions
A handful of misunderstandings show up again and again while teaching this principle, and it’s worth addressing them directly before they quietly calcify into bad habits.
“I should add an interface to literally every class.”
This is the single most common overcorrection, and it’s worth naming clearly and early: interfaces aren’t free. Every interface added is a small, permanent cost in extra files and extra indirection to navigate. The principle isn’t “always use interfaces” — it’s “depend on a promise, not a specific detail, wherever a genuine reason to vary or substitute that detail actually exists.”
“An interface with one implementation is pointless, so I’ll skip it entirely.”
This is the opposite overcorrection, and it’s just as worth correcting. Sometimes an interface with a single implementation today is still worth having — particularly where testing benefits from a swappable stand-in, even if a genuine second real implementation may never arrive.
“This only applies to object-oriented languages.”
The specific mechanics — an interface keyword, implementing classes — are indeed most associated with object-oriented languages. But the underlying idea, depending on a stable promise rather than a specific detail, shows up just as meaningfully in functional programming, through passing functions as parameters instead of hard-coding one specific function directly.
“Once I’ve written an interface, my job here is basically done.”
This is a subtler misconception worth flagging, because it can slip past even a junior developer who’s otherwise understood everything correctly so far. Writing the interface is only half the job — the interface still has to be a genuinely good one, capturing the right promise at the right level of detail. A poorly designed interface, one that accidentally bakes in assumptions from its first implementation, or one that tries to promise too much or too little, can end up causing just as much friction as having no interface at all, just with an extra layer of ceremony wrapped around the same underlying problem.
A junior developer who’s just learned this principle often swings hard toward interface-everywhere for a few weeks. This is a completely normal phase — gently redirect it with real examples rather than simply forbidding it outright.
Patterns a Junior Already Half-Knows
One of the most satisfying moments in mentoring is showing a junior developer that several design patterns they’ve already encountered, perhaps without fully understanding why they’re shaped the way they are, are really just this exact principle, applied to a specific, common situation.
Swappable “how”
An interface defines one way of doing a task; different classes provide different specific approaches behind it.
Swappable creation
Code depends on the general kind of object it receives, never on exactly which specific class built it.
Swappable listeners
Code raising an event depends only on “something that can be notified,” never on which specific classes are actually listening.
A translated promise
Code depends on the interface it expects; the adapter quietly honours that promise using something else underneath.
Pointing this out explicitly to a junior developer — “you already used this idea when you learned the Strategy pattern, you just didn’t have the general name for it yet” — often produces a genuine, visible moment of things clicking into place. It reframes a long list of separate-feeling patterns into variations on one much smaller, much more manageable core idea.
This connection is worth using deliberately as a teaching tool, not just as an interesting observation. Whenever a junior developer next encounters an unfamiliar design pattern, encourage them to ask, before diving into its specific mechanics: “where’s the promise in this pattern, and where’s the detail it’s hiding?” More often than not, finding the answer to that question makes the rest of the pattern’s structure fall into place far more quickly than trying to memorise its diagram from scratch. This single habit — hunting for the promise-versus-detail split inside any new pattern — turns out to be one of the fastest ways for a junior developer to become genuinely comfortable with the entire design patterns catalogue, rather than treating each new pattern as an unrelated thing to memorise separately.
How This Connects to SOLID
For a junior developer who’s starting to encounter the SOLID principles, it’s worth drawing the connection explicitly. The Dependency Inversion Principle — the “D” in SOLID — is, in large part, a more formal, more rigorous restatement of this exact same idea: important logic and specific implementation details should both depend on a shared abstraction between them, rather than the logic depending directly on the detail.
The Interface Segregation Principle — the “I” in SOLID — adds a genuinely useful refinement worth teaching right alongside the main idea: once you’re depending on interfaces, keep them small and focused, so nothing is forced to depend on promises it doesn’t actually need. Introducing these two principles together, rather than in isolation, helps a junior developer see “program to an interface” not as one isolated tip, but as the seed of a broader, coherent way of thinking that keeps showing up throughout more advanced design education.
One idea, said three different ways: depend on the promise, not the detail.
It’s worth being honest with a junior developer that SOLID as a whole can feel intimidating at first, with five separate-sounding principles to remember, each with its own formal definition. A genuinely useful teaching trick is to introduce Dependency Inversion and Interface Segregation specifically as “the two SOLID principles you’ve already basically learned,” rather than presenting all five at once as an equally unfamiliar block. This gives a junior developer an immediate, confidence-building foothold — two-fifths of a famously intimidating list, already understood — before tackling the remaining three principles, which build on genuinely different ideas and deserve their own separate, unhurried explanation at another time.
A Simple Mental Checklist
A short, concrete checklist tends to help a junior developer far more than a long explanation they have to recall from memory under pressure. Encourage them to run through this quickly, in their head, whenever they’re about to write new SomeSpecificClass() inside a piece of business logic.
- Does the logic I’m writing genuinely need to know which specific class this is, or does it only need to know what this thing can do?
- Could a reasonable second implementation of this exist someday — a different provider, a different storage method, a different algorithm?
- Would testing this logic be easier if I could swap in a lightweight, fake version of this dependency?
- If I answered “yes” to any of the above, have I written the interface first, before wiring up the specific class?
If all four answers point toward “no, this will always be exactly this one thing,” that’s a perfectly legitimate reason to skip the interface for now. The checklist exists to prompt a decision, not to force the same answer every time.
It’s worth encouraging a junior developer to physically write this checklist somewhere they’ll actually see it — a sticky note near their monitor, a pinned note in their editor, whatever sticks. The goal in the early weeks isn’t to have internalised the judgement yet; it’s to build the habit of consciously pausing at the right moment, before the specific implementation gets wired in directly out of pure momentum. Over time, running through these questions stops being a deliberate, effortful step and starts happening almost automatically, in the background, the same way an experienced driver checks their mirrors without consciously thinking through each step anymore.
When Not to Apply It
A genuinely responsible mentor teaches the exception right alongside the rule, because a junior developer taught only the rule tends to apply it everywhere, indiscriminately, until painful experience eventually teaches them the limits the hard way.
Where an interface earns its keep
A payment provider, likely to have alternatives someday. A notification channel, likely to grow in variety. A data source, useful to fake during testing. Anything crossing a genuine team or module boundary.
Where a direct dependency is honest
A tiny utility class with no realistic alternative. Simple value objects like a date range or an amount. Code deep inside one team’s private, stable internals. A one-off script never meant to be reused or extended.
It’s worth being explicit with a junior developer that this judgement call gets easier with experience, not with a longer list of rules memorised in advance. Early on, it’s completely reasonable to lean slightly toward “add the interface” while that judgement is still developing — the cost of an occasional unnecessary interface is usually smaller than the cost of a codebase full of hard-wired, difficult-to-change dependencies.
A genuinely useful way to build this judgement faster is to revisit old decisions honestly, every so often, rather than only ever moving forward. Encourage a junior developer to occasionally look back at an interface they added a few months earlier and ask, without any defensiveness, “did this actually end up earning its keep, or has it just been quietly sitting here unused the whole time?” Neither answer is a failure — an unused interface that never gained a second implementation isn’t evidence of bad judgement, it’s simply useful information about how this particular corner of the system actually evolved, and that information makes the next judgement call a little sharper than the last one.
A Short Practice Exercise
Theory sinks in far better alongside hands-on practice. A genuinely useful, low-pressure exercise for a junior developer: give them a small, deliberately tightly coupled piece of code — a class that logs messages by directly creating and calling one specific logging library — and ask them to refactor it using the five-step walkthrough from earlier in this guide, entirely on their own, before reviewing it together.
Start with tightly coupled code
A small class hard-wired directly to one specific logging library, with no interface in sight.
Ask them to define a “Logger” interface
What’s the smallest, most honest promise a logger should make? Usually just “record this message.”
Have the original class implement it
The existing logging library now formally honours the new promise, without changing its own behaviour.
Add a second, fake implementation
A tiny in-memory logger that just remembers messages in a list — perfect for tests, impossible to justify before the interface existed.
Write one test using the fake
The moment a test passes using the fake logger, the benefit becomes something they’ve felt firsthand, not just read about.
Resist the urge to hand them the finished interface. Let them design it themselves, even imperfectly — the act of deciding what belongs in the promise and what doesn’t is where the real learning actually happens.
Once this first exercise feels comfortable, a natural next step is to hand a junior developer a slightly larger, more realistic piece of legacy-style code — something with two or three different tightly coupled dependencies tangled together — and ask them to identify, entirely on their own, which ones are genuinely worth turning into interfaces and which ones aren’t, using the checklist from earlier in this guide. This second exercise shifts the focus from mechanics, which they’ve now practised, toward judgement, which is the genuinely harder and more valuable skill to build. Reviewing their choices together afterward, discussing not just what they decided but why, tends to be where the most useful mentoring conversations of the whole exercise actually happen.
How to Explain It in One Sentence
Every mentor eventually needs a short, memorable version of this idea, for the moment a junior developer asks “wait, can you say that again, quickly?” in the middle of a busy day. A few tested, genuinely effective one-liners:
The promise version
“Depend on what something promises to do, never on exactly how it does it.”
The remote-control version
“Write code the way a remote control works — buttons that do things, without caring what’s wired up behind them.”
The swap-test version
“If swapping this out later means editing this file, you’re depending on an implementation, not an interface.”
None of these needs to be recited word for word — the goal is giving a junior developer a phrase that genuinely makes sense to them, one they’ll actually remember and reach for on their own, months later, without needing to look it up again.
It’s worth actively inviting a junior developer to write their own version, rather than only offering these three as the final word. The act of putting the idea into their own language, drawing on whatever hobby, sport, or everyday experience genuinely makes sense to them personally, tends to produce a phrase that sticks far more durably than any version handed to them ready-made. A mentor’s job here is less about supplying the perfect analogy and more about creating the space for a junior developer to discover their own — the ownership of having found the right words themselves is often what makes the understanding permanent rather than borrowed.
Real-World Examples
The Universal Phone Charger
A modern charging cable’s connector is an interface — any device built to accept it can be charged, regardless of which company made the device or the charger. Before this kind of standardisation, every phone effectively depended on its own specific charger “implementation,” and losing or breaking that one specific charger was a genuine, familiar problem.
A Ride-Hailing App’s Driver Matching
A ride-hailing app’s core matching logic shouldn’t need to know or care whether a specific driver is using an Android phone or an iPhone — it only needs to know it can send a request and receive a response. That’s an interface at work, quietly letting the same matching logic work identically regardless of which specific device or app version each driver happens to be running.
A Junior Developer’s First Real Refactor
Nearly every experienced engineer can recall their own first time refactoring tightly coupled code into something depending on an interface instead — often prompted by exactly the kind of painful, sprawling change described earlier in this guide. Sharing that personal story with a junior developer, rather than only teaching the abstract principle, tends to make the lesson feel far more real and far more worth taking seriously.
A Household Appliance’s Power Standard
A washing machine, a toaster, and a lamp all plug into the exact same kind of wall socket, despite being wildly different appliances with completely different internal designs. None of them was built with knowledge of the others; all of them simply agreed to honour the same electrical interface. This is worth mentioning to a junior developer specifically because it shows the idea working at a scale far larger than any single codebase — an entire industry, spanning countless manufacturers who’ve never spoken to each other, coordinated smoothly through nothing more than a shared, stable promise.
Frequently Asked Questions
Does a junior developer need to understand SOLID before this principle makes sense?
No — this principle stands on its own and is genuinely easier to teach first, since it’s more concrete. SOLID’s Dependency Inversion Principle makes far more sense afterward, as a more formal restatement of something they already understand intuitively.
What’s the quickest way to spot code that violates this principle?
Search for the word “new” followed directly by a specific class name, sitting inside logic that shouldn’t really need to know which specific class it is. It’s not a perfect signal, but it’s a genuinely fast, reliable starting point.
Should a junior developer memorise the Gang of Four’s exact wording?
Not necessary — what matters is genuine understanding, not word-for-word recall. A junior developer who can explain the idea in their own words, using their own analogy, has learned it more durably than one who can only recite the original phrase.
How long does it usually take for this to genuinely click?
Often surprisingly quickly once it’s tied to a hands-on exercise rather than pure explanation — many developers report the idea clicking within a single mentoring session, though applying it with good judgement, rather than overusing it, takes longer and comes with practice.
Is this principle less relevant in dynamically typed languages without a formal interface keyword?
The underlying discipline matters just as much, even without a formal keyword enforcing it. Dynamic languages rely more on convention and documentation to keep the same promise-versus-detail separation clear, which arguably makes teaching this principle even more important, not less.
What’s the best first project for practising this?
A small, personal or practice project with at least one external dependency — a data source, a notification method, a payment step — gives a junior developer a safe, low-stakes place to feel the benefit of an interface firsthand, without production risk.
How should a mentor respond if a junior developer overuses interfaces after learning this?
With patience rather than correction alone — show them one or two concrete examples from their own recent code where an interface added cost without adding benefit, and let them reach the “maybe not everywhere” conclusion themselves, rather than simply being told to stop.
Is this principle still relevant given how much code is now written with AI assistance?
If anything, more relevant than ever — a codebase with clear, well-named interfaces gives an AI coding assistant a precise, checkable contract to work against when adding new functionality, while a codebase full of direct, hard-wired dependencies gives it far less reliable structure to build on correctly.
Glossary
| Term | Definition |
|---|---|
| Interface | A stable promise about what something will do, separate from how it does it. |
| Implementation | The actual, specific, working code that fulfils an interface’s promise. |
| Concrete Class | A specific, fully working class, as opposed to a general interface or abstract type. |
| Dependency Inversion Principle | The SOLID principle stating both high-level and low-level code should depend on shared abstractions. |
| Interface Segregation Principle | The SOLID principle favouring several small, focused interfaces over one large one. |
| Mock or Fake Object | A lightweight stand-in for a real dependency, used to isolate tests. |
| Tight Coupling | A strong, direct dependency where a change in one part forces changes elsewhere. |
| Refactoring | Restructuring existing code’s internal design without changing its outward behaviour. |
Key Takeaways
Teaching “program to an interface, not an implementation” well isn’t about reciting the Gang of Four’s exact words — it’s about giving a junior developer a felt, concrete sense of the difference between depending on a promise and depending on a detail, ideally through their own hands, on their own code, at least once. Everything else — the connection to design patterns, the tie to SOLID, the judgement about when to skip it — builds naturally on top of that first, hands-on understanding. A junior developer who’s truly internalised this one idea has quietly picked up a habit that will keep paying off, in ways they may not even consciously notice, for the rest of their career.
If there’s one closing piece of advice worth giving any mentor teaching this for the first time, it’s this: don’t rush past the discomfort of the first explanation not quite landing. Nearly every experienced engineer needed the idea explained more than once, in more than one way, before it truly settled in — often years apart, in completely different contexts, before the pieces finally connected. Patience with that process, more than any particular analogy or exercise in this guide, is what actually makes a junior developer’s understanding durable rather than superficial.
Remember This
- An interface is a promise about what will happen; an implementation is the specific code fulfilling that promise.
- The phrase comes from the Gang of Four’s 1994 design patterns book, born from hard-earned, real-world experience.
- “Interface” here means the general idea of a stable promise — not only the specific keyword in one language.
- Teaching through a real refactor, on a junior developer’s own code, works better than explanation alone.
- Many design patterns a junior already knows — Strategy, Factory, Observer, Adapter — are this exact idea in different clothes.
- This principle connects directly to the Dependency Inversion and Interface Segregation Principles in SOLID.
- The principle has real limits — not every class needs an interface, and teaching the exception matters as much as teaching the rule.
- A short, memorable one-liner, in the junior developer’s own words, tends to outlast any formal definition.