Skip to content

Commit 3468ec4

Browse files
author
Tom Moulard
committed
chore: better content
1 parent 2260090 commit 3468ec4

File tree

1 file changed

+31
-82
lines changed

1 file changed

+31
-82
lines changed

content/posts/2025-12-05-factory-vs-singleton-go-http-apis.md

Lines changed: 31 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,13 @@ categories: ["Go", "Architecture", "tutoriel"]
1313

1414
Picture this: It's 3 AM. Production is on fire. You're desperately trying to debug why your HTTP API is returning cached data from *yesterday*. You trace the issue to your HTTP client... which is a singleton... that someone initialized with a 24-hour cache... and there's no way to reset it without restarting the entire service.
1515

16-
Sound familiar? No? Well, stick around anyway. By the end of this post, you'll understand why the **Factory pattern** will save your future self from these sleepless nights, and why **Singleton** is often the villain in disguise when building Go HTTP APIs.
16+
I've been there. Twice, actually. The second time was worse because I *knew* better but had inherited the codebase.
1717

18-
Let's dive in with the classic journalist's approach: **Who? What? When? Where? Why?**
18+
This post is about why the **Factory pattern** beats **Singleton** for HTTP clients in Go. Not because some design pattern book says so, but because I've debugged enough production incidents to have opinions.
1919

2020
---
2121

22-
## Who Should Care?
23-
24-
**You**, if you:
25-
- Build HTTP APIs in Go (or plan to)
26-
- Write code that needs to be tested (spoiler: all code does)
27-
- Have ever struggled to mock a dependency
28-
- Want your teammates to stop cursing your name in code reviews
29-
- Enjoy sleeping at night instead of debugging production issues
30-
31-
If none of these apply to you, congratulations! You're either a perfect developer or you've achieved enlightenment. For the rest of us mortals, let's continue.
32-
33-
---
34-
35-
## What Are We Talking About?
22+
## The Two Approaches
3623

3724
### The Singleton Pattern: The "One Ring to Rule Them All" Approach
3825

@@ -228,13 +215,13 @@ type User struct {
228215
}
229216
```
230217

231-
"But Tom," I hear you say, "that's more code!" Yes. And your future self will thank you. Here's why...
218+
"But that's more code!" Yes. About 40 more lines. I'll take 40 lines over a 3 AM debugging session any day.
232219

233220
---
234221

235-
## When Does This Matter? (The Testing Showdown)
222+
## Testing: Where Singleton Falls Apart
236223

237-
Let's try to test both approaches. This is where things get *spicy*.
224+
Here's where it gets painful. Try writing tests for both approaches.
238225

239226
### Testing the Singleton Approach (The Hard Way)
240227

@@ -407,7 +394,7 @@ func TestUserHandler_GetUser_InvalidJSON(t *testing.T) {
407394
}
408395
```
409396

410-
Look at that! Three different tests, all running in parallel, each with its own mock behavior. No shared state, no test pollution, no tears.
397+
Three tests, all parallel, each with isolated mock behavior. This is how testing should work.
411398

412399
### Table-Driven Tests: The Factory Pattern's Best Friend
413400

@@ -488,9 +475,9 @@ func TestUserHandler_GetUser_TableDriven(t *testing.T) {
488475
489476
### Generating Mocks with Mocktail: Stop Writing Boilerplate!
490477

491-
Writing mock implementations by hand works, but let's be honest, it's tedious. For every interface you want to mock, you need to create a struct, implement all methods, handle the function callbacks... it adds up fast.
478+
Writing mocks by hand gets old fast. I've wasted too many hours writing boilerplate `DoFunc` callbacks.
492479

493-
Enter [**Mocktail**](https://github.com/paperballs/mocktail): a mock generator that creates **strongly-typed mocks** using `testify/mock`. Unlike other generators that use string-based method calls (hello, refactoring nightmares!), Mocktail generates typed methods that break at compile time when your interface changes.
480+
[**Mocktail**](https://github.com/paperballs/mocktail) generates strongly-typed mocks using `testify/mock`. The key difference from other generators: it uses typed methods instead of string-based calls. When your interface changes, compilation fails. No more "forgot to update the mock" bugs.
494481

495482
#### Installing Mocktail
496483

@@ -648,7 +635,7 @@ func (c *httpClientMockDo_Call) TypedReturns(resp *http.Response, err error) *ht
648635
}
649636
```
650637

651-
The key insight: **`TypedReturns` has the same signature as the real method**. If you change `Do`'s return type, Mocktail regenerates with the new signature, and your tests won't compile until you fix them. No more "oops, I forgot to update that mock" bugs in production!
638+
`TypedReturns` has the same signature as the real method. Change `Do`'s return type, regenerate, and the compiler tells you what broke. I've caught several bugs this way that would have shipped otherwise.
652639

653640
#### When to Use What
654641

@@ -657,13 +644,13 @@ The key insight: **`TypedReturns` has the same signature as the real method**. I
657644
- **Frequently changing interfaces**: Mocktail's type safety is invaluable
658645
- **Team projects**: Mocktail ensures consistency
659646

660-
The Factory pattern makes mocking possible; Mocktail makes it painless.
647+
Factory makes mocking possible. Mocktail removes the boilerplate.
661648

662649
---
663650

664-
## Where Does This Play Out? (Real-World Scenarios)
651+
## Real Scenarios Where This Matters
665652

666-
Let's look at some scenarios where Factory shines and Singleton... doesn't.
653+
Some cases I've actually dealt with:
667654

668655
### Scenario 1: Multi-Tenant API
669656

@@ -793,9 +780,9 @@ func setupClients(featureFlags FeatureFlags, userID string, config ClientConfig)
793780

794781
---
795782

796-
## Why Factory Wins (The Verdict)
783+
## The Trade-offs
797784

798-
Let's summarize the evidence:
785+
Here's how they compare:
799786

800787
| Aspect | Singleton | Factory |
801788
|--------|-----------|---------|
@@ -807,22 +794,18 @@ Let's summarize the evidence:
807794
| **Feature flags** | All or nothing | Per-instance control |
808795
| **Debugging** | "Which code path set this?" | Clear dependency chain |
809796

810-
### When Singleton IS Okay
797+
### When Singleton Actually Makes Sense
811798

812-
I'm not saying Singleton is *always* wrong. Use it for:
799+
To be fair, Singleton isn't always wrong:
813800

814801
- **True singletons**: There really can only be one (e.g., a process-wide metrics registry)
815802
- **Immutable configuration**: Loaded once at startup, never changed
816803
- **Resource pools**: Connection pools where you explicitly WANT sharing
817804
- **Logging**: Usually fine as a singleton (but consider structured logging with context)
818805

819-
The key question: **"Will I ever need to vary this behavior per context?"**
806+
The question I ask myself: "Will I ever need different behavior in different contexts?" If there's any chance the answer is yes, Factory. If it's truly global and immutable, Singleton might be fine.
820807

821-
If yes Factory. If truly no Singleton *might* be okay.
822-
823-
### The Factory-by-Default Philosophy
824-
825-
My recommendation: **Start with Factory, reach for Singleton only when you have a specific, justified reason.**
808+
In practice, I default to Factory and only reach for Singleton when I have a specific reason.
826809

827810
```go
828811
// Default approach: Factory + Dependency Injection
@@ -853,9 +836,9 @@ func main() {
853836

854837
---
855838

856-
## Refactoring from Singleton to Factory
839+
## Migrating from Singleton to Factory
857840

858-
Already have a codebase full of singletons? Here's a migration path:
841+
If you're stuck with a singleton-heavy codebase (I've been there), here's how to migrate incrementally:
859842

860843
### Step 1: Define an Interface
861844

@@ -915,11 +898,9 @@ func main() {
915898

916899
---
917900

918-
## Interactive Playground
919-
920-
Since Klipse is installed on this blog, you can experiment with some Go code right here!
901+
## Interactive Example
921902

922-
Try modifying this simple example to understand how interfaces enable mocking:
903+
If you want to play with the concepts, here's a simplified version:
923904

924905
```go
925906
import "fmt"
@@ -965,13 +946,13 @@ func main() {
965946
}
966947
```
967948

968-
See how the `UserService` doesn't care whether it gets a `RealClient` or `MockClient`? That's the power of interfaces and the Factory pattern!
949+
`UserService` doesn't care which client it gets. That's the point.
969950

970951
---
971952

972-
## Production Tips: Making Your Factory-Based Code Bulletproof
953+
## Production Hardening
973954

974-
Now that you're convinced Factory is the way to go, here are some production-hardening tips I've learned the hard way:
955+
Some things I've learned from production incidents:
975956

976957
### 1. Always Limit Response Body Size
977958

@@ -1028,46 +1009,14 @@ json.NewEncoder(w).Encode(result)
10281009

10291010
---
10301011

1031-
## Key Takeaways
1032-
1033-
### Use Factory When:
1034-
1035-
- You need different configurations for different contexts
1036-
- You want to test your code with mocks
1037-
- You're building multi-tenant systems
1038-
- You need to support feature flags
1039-
- You want explicit, traceable dependencies
1040-
- You value your sleep
1041-
1042-
### Singleton Might Be Okay When:
1043-
1044-
- There truly can only be one (process metrics, runtime config)
1045-
- The instance is immutable after initialization
1046-
- You're explicitly building a shared resource pool
1047-
- You've thought about it and can justify the trade-offs
1048-
1049-
### The Golden Rule
1050-
1051-
> **Make dependencies explicit. Your tests will thank you. Your teammates will thank you. Future you will thank you.**
1052-
1053-
---
1054-
1055-
## Conclusion
1056-
1057-
The Factory pattern isn't just about creating objects. It's about:
1058-
- **Flexibility**: Different configurations for different needs
1059-
- **Testability**: Easy mocking without black magic
1060-
- **Clarity**: Explicit dependencies you can trace
1061-
- **Maintainability**: Code that's easier to change
1062-
1063-
Singleton has its place, but it's a smaller place than most codebases give it. When building HTTP APIs in Go, the Factory pattern combined with dependency injection is almost always the better choice.
1012+
## Wrapping Up
10641013

1065-
So the next time you reach for `sync.Once` and a package-level variable, ask yourself: "Will I regret this at 3 AM?"
1014+
Factory adds some boilerplate. About 40-50 lines for a typical HTTP client setup. In exchange, you get testable code, flexible configuration, and fewer 3 AM incidents.
10661015

1067-
If the answer is "maybe," use a factory instead. Your future self will send you a thank-you note.
1016+
Singleton isn't evil. It's just overused. Most HTTP clients don't need to be singletons, and the ones that do are rarer than you'd think.
10681017

1069-
Happy coding! And may your tests always be green and your production always be boring.
1018+
Next time you're about to write `var instance *http.Client` with `sync.Once`, ask yourself if you'll ever need to mock it, configure it differently, or debug it in production. If the answer to any of those is "maybe," use a factory.
10701019

10711020
---
10721021

1073-
*Got questions? Found a bug in my examples? Want to argue that Singleton is actually fine? Drop a comment below or find me on [GitHub](https://github.com/tommoulard)!*
1022+
*If you spot a bug in the examples or want to argue that Singleton is fine, find me on [GitHub](https://github.com/tommoulard).*

0 commit comments

Comments
 (0)