In the previous article, we covered the first three letters of SOLID—SRP, OCP, and LSP. Today, we’ll wrap up the remaining two.
They are ISP (the Interface Segregation Principle) and DIP (the Dependency Inversion Principle).
DIP, in particular, is the theoretical foundation of dependency injection (DI)—something you inevitably encounter when using DI libraries in Swift, whether Swinject or Factory. By the end of this article, you’ll see why those libraries are designed the way they are.
ISP: Interface Segregation Principle
Let’s start with the definition.
Clients should not be forced to depend on methods they do not use.
That sounds abstract, but a concrete failure makes it immediately clear. Suppose we define a multifunction-printer protocol.
protocol Machine {
func print(_ doc: Document)
func scan(_ doc: Document)
func fax(_ doc: Document)
}
// An old printer can only print...
class OldPrinter: Machine {
func print(_ doc: Document) { /* Works correctly */ }
func scan(_ doc: Document) { fatalError("Cannot scan") } // Forced implementation
func fax(_ doc: Document) { fatalError("Cannot fax") } // Forced implementation
}
We’re forcing a printer that only prints to implement scanning and faxing too. Eventually, fake methods that throw exceptions appear—and that is also an LSP violation from the previous article. A fat interface breaks two principles in succession.
The solution is simple: split the interface by role.
protocol Printer { func print(_ doc: Document) }
protocol Scanner { func scan(_ doc: Document) }
protocol Fax { func fax(_ doc: Document) }
class OldPrinter: Printer { ... } // Print only
class MultiFunction: Printer, Scanner, Fax { ... } // Full multifunction printer
Each type promises only what it can do. In practice, the warning sign looks like this: if adopting a protocol requires an empty implementation accompanied by “This method has nothing to do with us…”, it’s time to split that protocol.
DIP: Dependency Inversion Principle
This principle is often misunderstood because of its name. Its definition takes two lines.
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
Let’s look at some code. Suppose an order service directly uses an email-sending library.
// The high-level module (business logic) directly depends on the low-level module (implementation details)
import SendGrid
class OrderService {
private let mailer = SendGridClient(apiKey: apiKey)
func completeOrder(_ order: Order) {
// Order processing...
mailer.send(to: order.email, message: "Order completed") // SendGridis tightly coupled
}
}
This structure has two problems. Replacing SendGrid with another service requires opening up the business logic, and real emails are sent every time you run a test.
Applying DIP reverses the direction of the arrows.
// The abstraction is defined by the high-level module (the order domain)
protocol NotificationSender {
func send(to: String, message: String) async throws
}
class OrderService {
private let sender: NotificationSender
init(sender: NotificationSender) { // Depends only on the abstraction
self.sender = sender
}
func completeOrder(_ order: Order) async throws {
try await sender.send(to: order.email, message: "Order completed")
}
}
// Details(SendGrid)follow the abstraction
class SendGridSender: NotificationSender { ... }
class SlackSender: NotificationSender { ... }
class FakeSender: NotificationSender { ... } // For testing
The dependency that originally pointed from “Order service → SendGrid” is now “Order service → Protocol ← SendGrid.” Since the details bow toward the domain, this is called “inversion.”
That’s Why DI Frameworks Exist
A natural question follows: “Then who creates SendGridSender()?”
The component that performs this assembly is a dependency injection (DI) container. DI libraries in the Swift ecosystem, such as Swinject and Factory, do exactly this. Classes depend only on protocols, while the library supplies the actual implementations.
DIP is a principle (direction), while DI is a technique (tool) for realizing it. Understanding this distinction alone can take your interview answers to the next level.
A Complete Summary of SOLID
Compressing SOLID from both articles into five lines gives us this.
| Principle | One-line summary |
|---|---|
| SRP | Separate code when the people requesting changes are different |
| OCP | Respond to frequent changes by adding rather than modifying |
| LSP | A child must not break the parent’s contract |
| ISP | Do not force implementations of methods they will never use |
| DIP | Do not make business logic depend on details |
All five ultimately point in the same direction: reduce the blast radius of change.
However, applying these principles mechanically to all code can instead violate KISS and YAGNI. Principles are tools to apply selectively where change actually happens frequently. I believe that sense of balance is the real skill.

![Cover image for [SOLID #2] SOLID Principles in Practice (Part 2): ISP · DIP and Why Dependency Injection Exists](/assets/images/posts/6d8f2c8e-6a69-4df6-a991-c232fcf9500a/1.jpg)