One Reason to Change
The Single Responsibility Principle is the S in SOLID, five rules for designing code that stays easy to change. It says: a type should have one job — and therefore only one reason to change.
When one type saves orders and sends emails, it changes when the storage changes and when the email text changes. Two jobs, two reasons, twice the risk of breaking one thing while touching the other.
In Go the rule applies to structs, their methods, and packages: keep each one focused on a single task.
Too Many Jobs in One Type
This order processor works — but it stores the order, writes the confirmation text, and "sends" it, all in one method. Testing the email text alone is impossible, and reusing the storage part elsewhere means dragging the email code along.
One type that does everything
Process() mixes two jobs: storing and notifying. Any change to either one means editing — and re-testing — this same type.
Output: Go program and captured output
package main
import "fmt"
type OrderProcessor struct{}
// Process does two jobs: storing AND notifying.
func (p OrderProcessor) Process(order string, customer string) {
fmt.Println("saving order to storage:", order)
fmt.Println("sending email to", customer+":")
fmt.Println(" Thank you! Your order", order, "was received.")
}
func main() {
p := OrderProcessor{}
p.Process("#1042 (2 books)", "[email protected]")
}
One Job per Type
The fix: one type per job. OrderStore only stores, EmailSender only sends. A small function coordinates them.
The output is the same — but now each part can be changed, tested, and reused on its own.
Split into two types
Same behavior, separated: the store knows nothing about emails, the sender knows nothing about storage. The processOrder function just connects them.
Output: Go program and captured output
package main
import "fmt"
// OrderStore has one job: storing orders.
type OrderStore struct{}
func (s OrderStore) Save(order string) {
fmt.Println("saving order to storage:", order)
}
// EmailSender has one job: sending emails.
type EmailSender struct{}
func (e EmailSender) SendConfirmation(customer, order string) {
fmt.Println("sending email to", customer+":")
fmt.Println(" Thank you! Your order", order, "was received.")
}
func processOrder(order, customer string) {
store := OrderStore{}
sender := EmailSender{}
store.Save(order)
sender.SendConfirmation(customer, order)
}
func main() {
processOrder("#1042 (2 books)", "[email protected]")
}
The payoff: reuse without fear
Because the sender is its own type, it sends a shipping notice too — no storage code involved. And the store archives an order without composing a single email. Each type grew inside its one job.
Output: Go program and captured output
package main
import "fmt"
type OrderStore struct{}
func (s OrderStore) Save(order string) {
fmt.Println("saving order to storage:", order)
}
func (s OrderStore) Archive(order string) {
fmt.Println("moving order to the archive:", order)
}
type EmailSender struct{}
func (e EmailSender) SendConfirmation(customer, order string) {
fmt.Println("email to", customer+": order", order, "was received")
}
func (e EmailSender) SendShippingNotice(customer, order string) {
fmt.Println("email to", customer+": order", order, "was shipped")
}
func main() {
store := OrderStore{}
sender := EmailSender{}
// Each type grows inside its own job.
sender.SendShippingNotice("[email protected]", "#1042")
store.Archive("#0997")
}