🧶 Intro: From Spaghetti to Structured
Ever written a function that starts by fetching user data, dumps it into a file, zips the file, then emails it out - all in one go?
It feels like this:
func exportEverything() {
fetchUsers()
parseUsers()
zipFile()
emailZip()
}
And sure, it works. Until the day something changes.
Suddenly, you need to skip zipping for certain users, or plug in an S3 upload. You end up sprinkling conditionals everywhere. Before you know it, you've summoned a micro-monolith. And worse? It's not even testable.
So… what do we do?
🧩 Enter: Chain of Responsibility Pattern
The Chain of Responsibility is a behavioral design pattern that lets you pass requests down a chain of handlers. Each handler does its job and optionally passes the request forward.
Think of it like a tech support escalation:
- Level 1 takes your call
- If they can't help, they pass it to Level 2
- If that fails, well… the manager steps in
In Go, we can use this pattern to cleanly break up our export logic into small, reusable, testable chunks.
🛠️ The Real-World Example: Exporting User Data
Let's say we have a process that:
- Fetches users from the DB
- Parses them into a file
- Zips that file
- Emails the zip to the requester
Using the Chain of Responsibility, each step becomes a handler:
type exportStepHandler interface {
execute(req *exportRequest) error
setNext(next exportStepHandler)
}
Each handler implements this interface and decides what to do with the request. Here's a quick peek:
🏃 Fetch Users
type exportFetchUsers struct {
next exportStepHandler
}
func (h *exportFetchUsers) execute(req *exportRequest) error {
fmt.Printf("RequestId(%d): Users fetched from DB\n", req.requestid)
return h.callNext(req)
}
🧻 Parse Into File
type exportParseIntoFile struct {
next exportStepHandler
}
func (h *exportParseIntoFile) execute(req *exportRequest) error {
fmt.Printf("RequestId(%d): Users parsed into file\n", req.requestid)
return h.callNext(req)
}
📦 Zip It Up
type exportZipFiles struct {
next exportStepHandler
}
func (h *exportZipFiles) execute(req *exportRequest) error {
fmt.Printf("RequestId(%d): file zipped\n", req.requestid)
return h.callNext(req)
}
✉️ Email the Zip
type exportEmailZip struct {
next exportStepHandler
}
func (h *exportEmailZip) execute(req *exportRequest) error {
fmt.Printf("RequestId(%d): Zip emailed\n", req.requestid)
return h.callNext(req)
}
🧩 Connecting the Chain
We then wire everything like a good old-fashioned factory:
func InitExportData() exportStepHandler {
emailStep := &exportEmailZip{}
zipStep := &exportZipFiles{}
zipStep.setNext(emailStep)
parseStep := &exportParseIntoFile{}
parseStep.setNext(zipStep)
fetchStep := &exportFetchUsers{}
fetchStep.setNext(parseStep)
return fetchStep
}
And then… 🧙 magic:
func main() {
exportChain := InitExportData()
exportChain.execute(&exportRequest{
requestid: 1,
})
}
🧠 Why This Pattern Rocks
✅ Readable: Each handler does one thing - clean and simple.
✅ Maintainable: Change one step without touching others.
✅ Flexible: Easily rewire or skip steps depending on context.
✅ Testable: Unit test each handler in isolation.
✅ Extensible: Want to log something or upload to S3? Just add a new handler!
🧵 Final Thoughts
If you've ever struggled with long, procedural workflows in Go, this pattern is a game-changer.
The Chain of Responsibility gives you a structure that scales. It turns a rigid flow into a flexible, testable, and modular pipeline.
So next time you think:
"Hmm, maybe I'll just add another if…"
Take a deep breath. Then chain it up. 🪢
Stay Connected!
💡 Follow me on LinkedIn: Archit Agarwal
🎥 Subscribe to my YouTube: The Exception Handler
📬 Sign up for my newsletter: The Weekly Golang Journal
✍️ Follow me on Medium: @architagr
👨💻 Join my subreddit: r/GolangJournal
Top comments (0)