{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/go/app-structure/",
"description": "Organize Go apps by domain, not technology. Learn why models/controllers structure hurts and how bounded contexts create better separation.",
"path": "/go/app-structure/",
"publishedAt": "2025-09-20T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"Go"
],
"textContent": "I like to make the distinction between application structure and architecture. Structure is\nhow you organize the directories and packages in your app while architecture is how\ndifferent components talk to each other. The way your app talks to other services in a fleet\ncan also be called architecture.\n\nWhile structure often influences architecture and vice versa, this distinction is important.\nThis post is strictly about application structure and not library structure. Library\nstructure is often driven by different design pressures than their app counterparts. There\nare a ton of canonical examples of good library structure in the stdlib, but it's app\nstructure where things get a bit more muddy.\n\nAt work, I not only write Go in a distributed system environment but also review potential\ncandidates' assignments in the hiring pipeline. While there is no objectively right or wrong\nway to structure an app, I do see a common pitfall in candidates' submissions that is\nusually frowned upon in a Go application.\n\n> _App structure should be driven by what it does and not what it's built with. Let the\n> domain guide the structure, not technology or the current language specific zeitgeist._\n\nBen Johnson's [Standard Package Layout] is a good reference for this. He points out why\napproaches like monolithic packages, Rails style layouts, or grouping by module don't fit\nwell in Go. Then he lays out a map where the root package holds domain types, dependencies\nare grouped in separate packages, and the main package wires everything together.\n\nWhile Ben's post is focused on what you should be doing, I want to keep this discussion a\nbit more open-ended and just talk about one bad pattern that you probably should avoid. The\nrest of the app structure is subjective and should be driven by requirements. Use your\njudgement.\n\nThe mistake I often see is people making a bunch of generically named packages like\nmodels, controllers, handlers and stuffing everything there. App structure like the\nfollowing is quite common:\n\nIn Go there's no file level separation, only package level separation. That means everything\nunder models like order and product lives in the same namespace. The same is true for\ncontrollers and handlers.\n\nOnce you put multiple business domains under a generic umbrella, you tie them together. This\nmight make sense in a language like Python where file names are prefixed in the fully\nqualified import path. In Python you'd import them as follows:\n\nBut in Go the import path becomes this:\n\nThere is no file level delineation in Go. If you put different domains under the same\nmodels directory, there is no indication at import time what domain a model belongs to.\nThe only clue is the identifier name. This isn't ideal when you want clear separation\nbetween domains.\n\n> _In Go, packages define your [bounded context], not files within a package. Domains should\n> be delineated by top level packages, not by file names._\n\nFor your top level business logic, you want package level separation between domains. Order\nlogic should live in order, user logic should live in user. These packages will be\nimported in many places throughout the app, and keeping them separate keeps dependencies\nclear.\n\nIt could look like this:\n\nEach domain owns its own logic and optional adapters. If you need to find order related\ncode, you go to order. If you need user code, you go to user. Nothing is smooshed\ntogether under a generic bucket.\n\nThe details around how you layer your app can differ based on requirements, but the\nimportant point is that your top level directories shouldn't just be generic buckets\ncontaining all domains. That makes navigation harder. A better approach is letting the\ndomain guide the structure and only layering in technology when it matters.\n\nYou can place your transport concerns alongside the top level packages. A top level http\npackage can hold handlers that import service functions from the domain packages. You can\nput all handlers under http or split them into http/order and http/user. Both are\nvalid choices. If you put all handlers under http, that's fine because they are usually\nimported in one place where you wire routes. The same is true for database adapters. You can\nput them all under postgres or split them into postgres/order and postgres/user. Both\npatterns are acceptable. The key difference is that domains need package level separation,\nwhile technology packages can be grouped because they are only wired at the edge.\n\nBut depending on the complexity of your app, this is also absolutely fine:\n\nThe rule of thumb is that top level domains should never import anything from technology\nfolders like http or postgres. Instead, http and postgres should always import from\ndomain packages. You can add a linter to enforce this rule but since Go doesn't allow import\ncycles, this is automatically enforced by the compiler.\n\nDomains sit at the top. Technology packages depend on them, never the other way around. The\ncmd package wires everything together. This keeps the graph simple and keeps domains\nindependent.\n\n---\n\nAstute readers might notice that I have left out any discussion around the internal\ndirectory. This is intentional. Depending on your requirements, you might opt in for an\ninternal directory or not. This isn't important for our discussion. The main point I\nwanted to emphasize is that technology or architecture patterns shouldn't guide your app\nstructure. It should be based on something more persistent and nothing is more persistent\nthan your application's domain.\n\n\n\n\n[standard package layout]:\n https://www.gobeyond.dev/standard-package-layout/\n\n[bounded context]:\n https://martinfowler.com/bliki/BoundedContext.html",
"title": "Let the domain guide your application structure"
}