Intro
There aren’t as many as in other ecosystems, but Go has its share of full-stack solutions that could be called web frameworks. Still, this project is not about trying to build yet another one. Aquamarine is more of a personal exercise. It’s a way to capture the patterns and structures I’ve come to appreciate over years of working on different Go projects.
These ideas come from hands-on work. When there were a few reasonable ways to approach something, I simply went with what felt more natural and pragmatic. Not because it was better in some universal sense, just because it made sense in the flow of real projects.
Much of what this kit will generate, I already tend to do almost automatically when starting a new Go project. It’s not about ego; it’s just a form of structured entertainment. I enjoy the process, and I find it satisfying to encode these patterns in a way that makes consistency effortless and repeatable.
Some of these patterns are practically hardcoded in my fingers at this point. And while I enjoy setting things up manually, I also find it useful to have a way to bootstrap new ideas quickly. The goal is to follow those same familiar lines without repeating the same choreography every time.
So no, I wouldn’t really call Aquamarine a framework. Frameworks aren’t all that common in the Go ecosystem, and this project isn’t trying to introduce one. I tend to associate frameworks with rigid, highly opinionated ways of doing things, and that’s not the intention here. It’s closer to a code generator. A way to apply patterns I’ve seen work well across teams, companies, and projects of different sizes. Not all of them were equally polished, of course, but the ones I enjoyed the most followed similar principles. This just tries to capture that in a reusable way. Nothing revolutionary, just something that feels solid and familiar.
To generate something meaningful, we first need a concrete use case to shape around. That’s why this effort starts with a very simple application: a Todo list. It’s the classic step after “Hello World”. Sure, it could all fit in a single main.go file, but that’s not the point. The goal is to build it in a way that surfaces all the edges: naming, structure, flows, conventions. Every corner case the generator might need to handle later should be encountered here first.
The idea of a platform kit goes a bit beyond just code generation. Initially, shared features live in a common internal package, but there’s room for that to evolve. At some point, it might make sense to extract them into a standalone module, usable as a library. Or maybe offer a choice: import it, or generate the code directly into your project if you prefer something you can tweak and own fully. Both paths are valid. That kind of flexibility is very much part of what I want Aquamarine to explore.
What to Expect
Aquamarine favors a command-query style of interface design. This isn’t strict CQRS. Commands do return data. But the philosophy is the same: routes represent actions (commands and queries), not just resources being acted upon. Event sourcing isn’t part of the initial setup, but support for it is on the roadmap as a possible extension for use cases that need it.
This approach is closely aligned with Domain-Driven Design. Endpoints are named after business actions, not technical operations. Instead of GET /tasks/123
, you might have /edit-task
or /assign-task-to-user
. The semantics are clearer. This helps establish a shared language between developers, stakeholders, and users. Ideally, a non-technical person should be able to understand the intent of an application just by looking at its URLs.
Features are organized by bounded contexts. Each feature lives in its own package and may include one or more related business entities. Routing, handlers (web and API), app services, repository interfaces, gRPC servers, pub/sub hooks. All of it is grouped together under the feature’s namespace. These features are designed to be self-contained. Ideally, they don’t share data structures or tables directly with one another. Instead, interactions between features should happen through clearly defined interfaces or messaging mechanisms, such as gRPC for synchronous calls or NATS for asynchronous events. This makes it easier to break out features into separate services later on if needed.
That said, not everything has to be modeled as a command-query feature. There are perfectly valid reasons, pragmatic or technical, for offering RESTful endpoints as well. Some clients expect it. Some operations are trivial. Aquamarine supports this too.
RESTful resources are also feature-packaged. Each resource lives in its own namespace with its own routing, handlers, services, and interfaces. The difference is mainly in the shape of the URLs and the verbs used: GET /res/task/123
and POST /res/task
instead of /get-task-details
or /create-task
. REST resources follow conventional paths but live side by side with the more expressive command-query ones.
No approach is enforced. Aquamarine provides the structure and the primitives. It’s up to you to decide how strictly to follow them. These principles set the tone for what Aquamarine will generate. But principles are one thing. Implementation is another. So how exactly will this be built? That’s what the next section explores.
Building from Something Real
In order to generate useful code, the generator has to know what it’s generating. That means starting from something concrete. Something that reflects the kinds of patterns and situations you’d want to support in larger, real-world projects.
That “something” is a simple Todo list app.
Yes, a Todo list might sound like the most generic example out there. But it’s good exactly because of that. It’s simple enough not to get lost in the domain, but rich enough to explore structure, naming, flows, and conventions. The goal isn’t to build the most ergonomic Todo list ever. It’s to make sure we touch all the relevant edges: command-query endpoints, RESTful alternatives, feature packaging, service layers, repository interfaces, web rendering, and JSON APIs.
This app is the proving ground. It’s where patterns are tested, decisions are made, and structure emerges. Everything the generator will produce later has to work cleanly here first.
We’ve talked about the preference for a DDD-inspired structure. But interestingly, in this reference app, it’s not the Todo list itself that follows that approach. Instead, the authentication and authorization feature is the one built using this style. It’s a bit paradoxical. The auth layer ends up being more complex than the feature it protects. But there’s a reason.
The goal is twofold. First, to demonstrate the DDD-friendly structure in practice. Second, to deliver a reusable component that will become part of the generated foundation for other services. This auth layer is not just for demo purposes. It’s meant to be part of the platform kit itself.
The design follows a hybrid RBAC + ABAC model. Users are assigned roles. Roles define permissions. Individual users can also receive specific permissions on top of their role. This avoids the need for creating one-off roles just to grant a single exception. Then there are resources: entities, identifiers, routes. Each declares the permissions required to interact with them.
The Todo list feature, on the other hand, is implemented following a more traditional RESTful approach. While it’s uncommon for a single service to expose both DDD-style and RESTful endpoints, this reference app includes both because the generator will need to support them. It’s a practical foundation for what comes next.
Both features, auth and todo, are being built with the same underlying structure, even if their routing styles differ. Each one includes or will include:
- Web and API routing
- Web and API handlers
- An app services layer, where all business logic lives (never inside handlers), so it’s reusable across web, gRPC, or even CLI interfaces
- Repo interfaces (with concrete implementations living outside the feature package; for practicality, the first implementation is based on SQLite)
- Server-side rendered HTML pages with embedded assets (templates, styles, etc.)
- A template manager to help load, catalog, cache, and inject templates easily into handlers
- A query manager to define and group queries separately, so query logic doesn’t clutter business logic with long SQL strings
- A migration manager
- A seed manager to pre-populate initial data in development environments
This list will likely evolve. But it already covers most of the pieces that a small-to-medium Go service would need to get off the ground quickly and cleanly.
Additional features are also planned as the project evolves:
- Inter-context sync access using gRPC
- Inter-context async access using pub/sub interfaces (with NATS likely as the first concrete implementation)
- NoSQL support (e.g. MongoDB, as an alternative implementation of the same repository interfaces)
- Support for external template and asset loading (as an alternative to embedded resources, allowing live edits or CDN-level caching when needed)
- Unit testing scaffolding, including a fixture manager that may share much of its core with the seed system
- Asset processing pipeline
- Containerization and orchestration-ready setup
Not everything needs to be in place from day one. The idea is to make space for it as the design matures. The focus is on keeping things simple now, while allowing room to grow.
Closing thoughts
This document isn’t part of a content strategy. I’m not writing Aquamarine to have something to publish, if anything, the writing is just a consequence of the work.
Documenting the process helps me think, prioritize, and stay consistent. In that sense, this post is more of a structured braindump. It’s useful not just for others, but for myself. At some point, it might make sense to split it into a proper guide or series of posts, but that will happen naturally if the project evolves in that direction.
For now, consider this a living outline. It will grow as the project grows.
You can find the source code for the Todo List reference app here:
github.com/aquamarinepk/todo