🧩 A quick Go API design exercise:
You start with:
func NewClient(url string, timeout time.Duration, retries int) *Client
More optional config keeps getting added: headers, proxy, TLS...
How would you keep this constructor from growing forever? 🤔
👇 Solution in reply
✅ Use functional options:
type Option func(*Client)
func WithRetries(n int) Option {
return func(c *Client) {
c.retries = n
}
}
NewClient can now accept:
func NewClient(url string, opts ...Option) *Client
👇 How it works
✅ Inject the behavior:
type Logger interface {
Log(string)
}
type Service struct {
sender Notifier
logger Logger
}
Inject both through the constructor.
💡 Service now depends on behavior, not implementations.
Production → PrintLogger
Tests → FakeLogger
🧩 A quick Go dependency injection exercise:
We already have a small dependency:
type Notifier interface {
Send(string, string) error
}
type Service struct {
sender Notifier
}
Now there's a new requirement 👇
Notify() logs on success:
func (s *Service) Notify(to, msg string) error {
if err := s.sender.Send(to, msg); err != nil {
return err
}
fmt.Println("sent:", to)
return nil
}
But Service shouldn't depend on fmt.
How would you design it? 🤔
👇 Solution next
✅ Not necessarily.
If the implementation is already known:
service := Service{
notifier: &EmailNotifier{},
}
is simpler and explicit.
💡 A factory makes more sense when the choice is dynamic - config, environment, etc.
Don’t add the pattern. Ask what problem it solves.
🧩 A quick Go Factory exercise:
You have:
func NewNotifier(kind string) (Notifier, error)
It returns an EmailNotifier or SMSNotifier.
Now imagine the app already knows it needs email. 🤔
Do you still need the factory?
👇 Solution in reply
@Anthony_K81 I believe it’s often abstracted away, and most people aren’t aware of the problem until they actually run into it. In practice, TTL gets chosen more widely because it’s easier and quicker to implement. Fencing adds complexity, and that complexity needs to be justified.
Your distributed lock is probably broken.
A TTL is not mutual exclusion. Pause longer than the lease and two clients will both believe they hold the lock. You cannot fix that with a smarter timeout.
@tanujDE3180 - Identify what's actually consuming the CPU at 2:17.
- I'd correlate pod/container/node and process-level CPU metrics around that timestamp
- Suspect scheduled activity - backups, DB maintenance, log rotation, Kubernetes jobs, or cloud infrastructure tasks.
✅ In the first one, Notifier is embedded, so its methods are promoted.
service.Send(...) works.
With a named field, it doesn't.
Small difference, but important when designing the API: embedding isn't just a shorter way of storing a dependency.
🧩 Quick Go exercise:
What's the difference between these?
type Service struct {
Notifier
}
and
type Service struct {
notifier Notifier
}
Both can use a Notifier, but they don't expose the same API.
What changes?
👇 Answer below
✅ Reduce it to what the caller needs:
type Notifier interface {
Send(string) error
}
EmailNotifier and SMSNotifier satisfy it implicitly.
💡 Ask:
"What does the caller need?"
Not:
"What can the implementation do?"
Small interfaces -> fewer dependencies, easier testing.
🧩 A quick Go interface exercise:
You have:
type Notifier interface {
Validate() error
Connect() error
Send(string) error
Disconnect() error
}
But the caller only sends notifications. 🤔
How would you redesign this interface?
👇 Solution in reply
Kleppmann on distributed locking + fencing tokens (2016): https://t.co/aNUTdPUOaf
Chubby’s sequencer is the same idea, older: https://t.co/voWvy1pO24
Antirez on Redlock: https://t.co/52SziWUS88