We're building Towami — a commerce platform that combines white-label online stores with country marketplaces. One codebase produces four long-running services, each with a separate entry point and responsibility.
The Four Binaries
cmd/
├── site/ → marketplace portal + country catalogs
├── shops/ → merchant storefronts on custom domains
├── admin/ → back-office panel
└── worker/ → background jobs via Asynq (no HTTP)
site serves the main domain and multiple country subdomains. shops serves merchant storefronts on platform subdomains and custom domains. admin is the back-office dashboard. worker processes email, Telegram, and import tasks through Redis queues.
The repository also has an importer command. It is a CLI utility rather than a long-running service, so it is outside the four in the title.
Shared internal/ With Clean Boundaries
All four binaries import from a single internal/ package tree:
router → middleware → handler → service → repository → model/DB
↘ templates (Templ)
↘ dto
The key is sub-packaging by application: handler/site/, handler/shops/, and handler/admin/, with the same split for services, repositories, DTOs, and templates. Each binary imports only the packages it needs. The worker, for example, does not import HTTP handlers or templates.
Go's internal directory also prevents code outside the parent module tree from importing these packages. It does not enforce our layer order, though; that remains an architectural convention backed by review and tests.
Why Not Microservices?
Early on, we considered splitting into separate repos. Three things stopped us:
Shared models. Products, shops, and users are the same entities everywhere. Duplicating model definitions across repos means sync hell.
Atomic refactors. When we change a database column, the handler, service, repository, DTO, and template all update in one commit.
Deployment simplicity. Four builds with CGO_ENABLED=0 produce self-contained binaries that can be managed as independent service units. We do not need container orchestration, a service mesh, or versioned APIs between these cooperating applications.
This is still a modular monolith: the binaries share code and data contracts. Separate executables alone do not make them microservices.
The Gotcha: Air and Hot Reload
In development, each service runs in Docker with Air for hot reload. Without scoped watch rules, an edit to shared code can rebuild every service.
We solved this with scoped watch configs. The worker Air config ignores templates/, handler/, and router/. The admin config ignores handler/site/ and handler/shops/. Each service only rebuilds when files relevant to it change.
# Worker: exclude HTTP and UI layers
[build]
exclude_dir = [
"internal/templates",
"internal/handler",
"internal/router",
"assets",
"storage",
]
Bottom Line
A Go monorepo with one cmd/ entry point per service gives us process-level separation while keeping atomic refactors and one go.mod. It is a good fit while the applications share a schema and are maintained by one team. If independent ownership or deployment becomes the real constraint, the boundary can change later.
Next up: how one of those binaries serves multiple country sites on different subdomains with a single process.
This article is based on lessons from building Towami, a multi-country marketplace and white-label storefront platform. Follow for more practical notes on Go, HTMX, infrastructure, and SaaS.
Top comments (0)