#The count that started it
Starting a new Go service, out of irritation, I wrote down every decision I made before the first request was served. There were twenty-three. Router. Logger. Configuration format and precedence. How configuration reloads. Where dependencies are constructed. Whether that is manual wiring or a container. Shutdown ordering. What a health check returns and who registers one. Migration tool. Migration ordering across modules. Metrics library. Trace propagation. Error type. Whether errors carry the operation. Request identifier generation. Middleware ordering. Job runner. Retry policy. Where the API specification comes from.
Not one was interesting. I had made all of them before, and across three previous services I had made at least four of them differently for no reason I could reconstruct.
#Where minimalism sends the work
The argument for a minimal framework is that it does not impose. Bring your own router, logger and container. That is a real benefit.
It does not remove the decisions. A framework that decides nothing has moved twenty-three decisions from one author, who makes them once and lives with the consequences across every user, to every application author, who makes them under time pressure and slightly differently each time. Six services later you have six subtly incompatible codebases in one organisation and no shared middleware, because the middleware signature depends on which router each one chose.
Unopinionated frameworks do not produce fewer opinions. They produce more of them, held by more people, written down nowhere.
That outcome is the predictable one. Organisational structure shows up in the artefacts , and a decision left to each team is a decision each team makes for its own reasons.
#What the framework fixes
- Lifecycle. Components declare dependencies, the container computes a start order, shutdown runs the reverse. A failed start halts the boot naming what it needed.
- Dependency injection. Generics for type safety, constructor injection, and circular dependency detection at registration rather than at two in the morning.
- Configuration. Layered sources, environment overrides, hot reload, and auto-discovery that walks up from a nested service directory to find the repository's configuration. That sounds trivial until you have a monorepo where every run needs a different relative path.
- Health. Any component implementing the interface is discovered and reported. There is no list to maintain and therefore nothing to forget.
- Observability. Structured logs, metrics and trace propagation from the first line of the first handler rather than added during the incident that needed them.
- Extensions. Around twenty, each with the same registration and lifecycle shape, so learning one teaches you the rest.
1app := forge.NewApp(forge.AppConfig{2 Name: "my-app", Version: "1.0.0", HTTPAddress: ":8080",3})45app.Router().GET("/", func(ctx forge.Context) error {6 return ctx.JSON(200, map[string]string{"message": "Hello, Forge!"})7})89app.Run() // blocks until SIGTERM, then shuts down in dependency order
#Two things it got wrong
The extension interface was too large. Seven required methods, most of which most extensions satisfied with empty bodies. Writing one felt like filling in a form, and an empty implementation carries no information. It is now one required method with optional capabilities detected by type assertion. Counting how many of an interface's methods are usually empty is a good design signal .
Configuration reload was too eager. Every change fired every watcher, which is right for a log level and harmful for a pool size. Reload now offers the change to the owning component, which decides whether it can apply it live, needs a drain, or must refuse until restart .
#The honest downside
Disagree with one of the twenty-three and you will have a worse time than with a library of libraries. That cost cannot be designed away, since an opinion you can always override is a default with extra steps.
What I would say is that the cost is legible immediately. Brooks separates essential from accidental difficulty , and re-deciding shutdown ordering in every service is as accidental as difficulty gets.
References
- [1]D. L. Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules”, Communications of the ACM, vol. 15, no. 12, pp. 1053-1058, 1972doi:10.1145/361598.361623 ↗
- [2]Frederick P. Brooks Jr., “No Silver Bullet: Essence and Accidents of Software Engineering”, Computer, vol. 20, no. 4, pp. 10-19, 1987doi:10.1109/MC.1987.1663532 ↗
- [3]Melvin E. Conway, “How Do Committees Invent?”, Datamation, vol. 14, no. 5, pp. 28-31, 1968
- [4]Michael T. Nygard, “Release It! Design and Deploy Production-Ready Software”, Pragmatic Bookshelf, 2nd edition, 2018