SOLID Principles in GO
They are foundation for writing clean, scalable, and maintainable code, in real-world backend development.
Mostly in LLD rounds, we are not directly asked about SOLID principles. But we are expected to demonstrate and explain them in theoretically while designing a system.
🔷 S — Single Responsibility Principle (SRP)
"One thing should do only one job, only reason to change" The single responsibility says , a function, class, module or a struct should be designed to have a single responsibility just one single purpose.
Why is this important?
if a piece of a code have multiple responsibility, then
Real-Life Example:
Think of a journal notebook:
// SRP compliant design
type Report struct {
Title string
Content string
}
func (r Report) Generate(){
// Responsible only for report generation
}
type FileSaver struct{}
func (fs FileSaver) Save(report Report, filename string) {
// Responsible only for saving
}
🔶 O — Open/Closed Principle (OCP)
"Your code should be open for extension & closed for modification".
Real-World Analogy
Think of power socket at home:
// creating a payment gateway interface for multiple payment modes
type PaymentProcessor interface {
Pay()
}
type CreditCard struct{}
func (c CreditCard ) Pay() {
// credit card logic
}
type Paytm struct{}
func (p Paytm ) Pay() {
// paytm logic
}
func ProcessPayment(p PaymentProcessor) {
p.Pay()
}
Now, suppose if we have to add UPI.
🔷 L — Liskov Substitution Principle
This is the one where people bit struggles to understand it. In simple words it says that, if we have a piece of code that works with base type (interface or parent ) it should also works with its subtypes ( implementation ) without breaking its behavior.
for eg. In natural language and mathematics, we say:
"A square is a rectangle"
...feels intuitive and correct. But in code, "is-a" should really mean "behaves like" — and this is where we get it wrong. We cant inherit the rectangle in square due to difference in behavior.
When we say:
"Type B inherits from Type A" : ...we're telling the compiler and the reader:
"You can use B wherever you expect A — it behaves like A" : And that is the core of Liskov Substitution Principle.
# Violation of Liskov substitution principle
Rectangle:
SetWidth(w) → width = w
SetHeight(h) → height = h
Square:
SetWidth(w) → width = w, height = w ← side effect!
SetHeight(h) → height = h, width = h ← breaks expectations!
creating a function that calculates area, considering the fact a square is a rectangle
func ResizeAndCheckArea(r *Rectangle) {
r.SetWidth(5)
r.SetHeight(10)
fmt.Println("Expected Area: 50, Got:", r.Area())
}
// but it will fails with the square, if we tried to use the square as rectangle because height and width are not independent.
Why we use Liskov's Substitution principle ?
🔶 I — Interface Segregation Principle (ISP)
It's says, Clients (types that implements interface) should not be forced to dependent upon interface they dont use. Or in even simpler terms, keep interfaces small and focused do not make them fat.
Go has a powerful advantage here, Interfaces in Go are implicitly satisfied. So it enforce us to avoid bloated interfaces naturally and define the methods we actually need.
// interface violating ISP
type Machine interface {
Print()
Scan()
Fax()
}
// Correct ISP
type Printer interface {
Print()
}
type Scanner interface {
Scan()
}
type Faxer interface {
Fax()
}
🔷 L — Dependency Inversion Principle (DIP)
Let's understand the simple definition first before explaining it through the example. Dependency inversion is a strategy of depending upon interfaces or abstract function or classes rather then upon concrete functions and classes.
In other words, our higher level modules should not depend upon the low level modules. Both should depend upon abstraction.
Let’s simplify it more :
Traditionally, high-level modules directly use low-level modules. That causes tight coupling and fragile code.
DIP inverts this dependency — instead of high-level modules depending directly on concrete details, both should depend on interfaces (abstractions).
Now understand it with real life analogy ( Power Outlet and Home Appliances):
At our homes, we use different types of appliances like A.C., fan etc. They all need electricity to work. To operate that appliance we plug them into wall socket or maybe powerbank or even a generator. But the appliance really does'nt care where the electricity come from - as long as it follows standard plug and voltage.
Here our appliance depends upon the general source of electricity not a specific wall socket.
Why Do We Need the Dependency Inversion Principle ?
How We Achieve It ?
High-Level Module (OrderService)
↕
Interface (OrderRepository)
↕
Low-Level Module (MySQLRepository or MongoRepository)
let's try to implement DIP through an example : Suppose a userService(high level module) sends a message using messager (low level module). We apply DIP by introducing an interface Messenger, and inject a concrete implementation. at run time.
package main
import "fmt"
// Abstraction
type Messenger interface {
SendMessage(to string, message string)
}
// Low-level module
type EmailMessenger struct{}
func (e EmailMessenger) SendMessage(to string, message string) {
fmt.Println("Email sent to", to, "with message:", message)
}
// High-level module
type UserService struct {
messenger Messenger // depends on abstraction
}
// Dependency Injection via constructor
func NewUserService(m Messenger) *UserService {
return &UserService{messenger: m}
}
func (u *UserService) NotifyUser(email string) {
u.messenger.SendMessage(email, "Welcome to our platform!")
}
func main() {
emailMessenger := EmailMessenger{} // low-level concrete
userService := NewUserService(emailMessenger) // inject it into high-level module
userService.NotifyUser("user@example.com") // works via abstraction
}