I remember years ago when I first started learning Angular…
Dependency Injection made zero sense to me. None.
The worst part? Angular is built around DI, so I just pretended I got it. Even in interviews, I’d repeat fancy words I didn’t actually understand. Fake it till you make it, right?
Well, in this piece, I want to finally demystify it. And I promise, it’s not that deep once you break it down.
But to get it, we’ve gotta go way back…
Full Code(including prev article):
git clone https://github.com/sklyt/goway.git
From 00
If you’re anything like me, your intro to code probably looked like this:
var sum = 4 + 4 // yes, `var` was popular in Js once upon a time
Then the tutorial says: “Hey, you might want to reuse this logic. Let’s make a mini math library.”
We got functions.
function add(a, b){
return a + b
}
So functions are just a realization: Hey, I can bundle single lines of code and reuse them.
Right?
Dependency Injection is kinda like that, but instead of bundling single lines of code, we’re bundling functions and objects into something reusable.
We call that bundle a service.
It’s basically like a little npm library you make inside your own project. The only reason it’s not a full library is because you haven’t published it yet.
Let’s bring it back again.
DI in Simple Terms
Just like functions group single lines of logic...
DI groups related functions and objects into a single reusable unit you can plug into other parts of your app.
Without DI, you’d end up doing something like this:
import { function1, function2, functionN, Object1, Class1 } from "module"
With DI, it’s more like:
import { MyModuleService } from "module"
// all the useful functions and objects are composed inside here
But you don’t just jam anything together. DI is for grouping related stuff, functions or objects that belong together.
In our storage
project, we already have two things that qualify:
OnlineStore and LocalStore.
Remember why? First article: interface-driven design, they both fulfill the same interface:
type Storage interface {
Save(key string, data []byte) error
Load(key string) ([]byte, error)
}
So instead of doing this manually:
online := storage.NewOnlineStore("https://jsonplaceholder.typicode.com", gohttplib.WithTimeout(2*time.Second), gohttplib.WithBearerToken("secret-token-123"))
local := storage.NewLocalStore("./data")
// Then calling each store individually below 👇
We can collapse everything into a StorageService:
online := storage.NewOnlineStore("https://jsonplaceholder.typicode.com", gohttplib.WithBearerToken("aiohsfnals"), gohttplib.WithTimeOut(2 * time.Second))
local := storage.NewLocalStore("./data")
log_ := logger.NewLogger("APP")
store := storage.NewStorageService(local, online, log_) // Service
data, err := store.Get("foo")
if err != nil {
log_.Log(err.Error())
}
log_.Log(string(data))
Yep, all that cache handling and fallback logic from useStorageGet
?
Gone. Now it lives inside the service.
NewStorageService
Create a new file: internal/storage/service.go
Full implementation:
package storage
import (
"errors"
"fmt"
"goway/internal/logger"
)
type StorageService struct {
local Storage
online Storage
log *logger.Logger
}
func NewStorageService(local, online Storage, log *logger.Logger) *StorageService {
return &StorageService{local, online, log}
}
func (svc *StorageService) Get(key string) ([]byte, error) {
data, err := svc.local.Load(key)
if err != nil && !errors.Is(err, ErrNotFound) {
return nil, fmt.Errorf("cache error: %w", err)
}
if err != nil && errors.Is(err, ErrNotFound) {
svc.log.Log(fmt.Sprintf("cache miss: %s", key))
} else {
return data, nil
}
data, err = svc.online.Load(key)
if err != nil {
return nil, fmt.Errorf("online fetch failed: %w", err)
}
if err := svc.local.Save(key, data); err != nil {
svc.log.Log(fmt.Sprintf("warning: failed to populate cache: %v", err))
}
if len(data) == 0 {
return nil, ErrNotFound
}
return data, nil
}
In storage.go
, add this:
import "errors"
var ErrNotFound = errors.New("service: key not found anywhere")
Now our main.go
looks like this:
func main() {
online := storage.NewOnlineStore("https://jsonplaceholder.typicode.com",
gohttplib.WithBearerToken("aiohsfnals"),
gohttplib.WithTimeOut(2 * time.Second),
)
local := storage.NewLocalStore("./data")
log_ := logger.NewLogger("APP")
store := storage.NewStorageService(local, online, log_)
data, err := store.Get("foo")
if err != nil {
log_.Log(err.Error())
}
log_.Log(string(data))
}
My file went from 80+ lines to less than 30.
Now I guess I have to give you the official Dependency Injection mantra and why people say it matters:
Why DI Matters
DI isn't just a pattern, it’s a mindset. It encourages you to:
- Decouple dependencies from the code that uses them.
- Focus on high-level logic instead of implementation details.
- Build systems that are flexible and testable.
- Swap out implementations without touching the core code.
- Mock dependencies easily for testing.
- Keep your main logic clean.
⚠️ Disclaimer: That’s the textbook explanation. That’s how it was sold to me back in the day.
Do I really know what “focus on high-level logic” means? Honestly... still not sure.
But the modularity part? 100% true.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more