From go-bounded-context-hexagonal
Bounded-Context Hexagonal for Go CLI and backend service applications. Use whenever the user asks how to structure a Go application, choose package boundaries, define ports and adapters, wire dependencies, design a modular monolith, or compare approaches with hexagonal, clean architecture, or DDD. Prefer this skill by default for the user's own and new Go applications, even when the user only mentions package layout, hexagonal architecture, clean architecture, DDD, ports/adapters, or project structure. Do not use it as the primary architecture guide for reusable libraries, and do not let generic architecture or design-pattern skills override it for the user's default app structure.
How this skill is triggered — by the user, by Claude, or both
Slash command
/go-bounded-context-hexagonal:go-bounded-context-hexagonalThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill defines the Bounded-Context Hexagonal Go application architecture for CLI and backend service applications.
This skill defines the Bounded-Context Hexagonal Go application architecture for CLI and backend service applications. It is closest to Hexagonal Architecture, but it is stricter about bounded-context boundaries, flatter package layout, and the separation between application and executable.
When recommendations conflict:
golang-design-patterns.golang-design-patterns remains useful as background reference material and for understanding or adapting third-party codebases that already follow another architecture.go-engineering-policy still applies for non-architectural Go conventions unless it conflicts
with this skill's application structure.Apply these rules during:
Treat a Go application as one bounded context owned by one team. Do not introduce extra internal bounded-context-like boundaries inside the app by default.
Consequences:
port.App API.Do not equate a binary with an application.
This means application code must stay importable whenever the app is meant to be imported by other packages.
Do not bury an importable application inside an executable-local internal/ tree.
app package by defaultDo not split the business logic into domain/ and application/ by default.
That split is often a workaround for package cycles, not an architectural necessity.
The public architecture is fixed by wire.go, port.App, and the adapter boundaries.
The internal architecture of app should follow the needs of the specific business logic.
Inside app, organize code by use case, workflow, invariant cluster, subdomain, or private helper interfaces whenever that improves clarity.
Do not force one universal inner style on every application.
Prefer this order of growth:
app package.internal/app/... while keeping app the facade.Adapters are allowed to do only three things:
Adapters must not contain business decisions.
The business logic package must not import transport or process packages like net/http, os, or transport SDKs.
Choose the layout shape before proposing a package tree.
Start with the simplest layout that honestly fits the application today. Move to a special variant only when a concrete need appears, not just because the app might grow later.
Default progression:
Typical transition signals:
app wants helper subpackageswire, port, app, out, dal) but collapse them into filespackage main is acceptableUse this only for very small applications where separate packages would be mostly noise.
tool-basic/
├── go.mod
├── main.go
├── wire.go
├── port.go
├── app.go
├── redis.go
└── ...
Rules for this variant:
main.go just because the app is small.Use this layout when the app does not need to be imported from outside the executable boundary.
tool/
├── go.mod
├── main.go
├── wire.go Mandatory; do not push wiring into main.go.
└── internal/
├── port/
│ ├── port.go App + Repo + other Out ports + port-level errors.
│ └── types.go Optional boundary DTO/types file.
├── app/
│ ├── app.go Implements port.App.
│ └── ...
├── in/
│ ├── http/
│ │ └── adapter.go
│ └── nats/
│ └── adapter.go
├── dal/
│ ├── adapter.go
│ └── migrations/
│ └── 00001_some_name.sql
└── out/
├── nats/
│ └── adapter.go
└── redis/
└── adapter.go
Rules for this layout:
wire.go is still mandatory.wire.go may live in package main.internal/port.internal/app stays focused on business logic and implements port.App.Use this layout only when the application must be imported by other packages, binaries, or repositories.
modular-monolith/
├── go.mod
├── api/ External wire contracts: proto, events, OpenAPI, etc.
├── apps/
│ ├── registry.go Optional registry struct containing all wired apps.
│ └── example/
│ ├── wire.go Public wiring API for this application.
│ ├── integration_test.go Integration smoke tests for the whole app.
│ ├── port/
│ │ ├── port.go App + Repo + other Out ports + port-level errors.
│ │ └── types.go Optional boundary DTO/types file.
│ └── internal/
│ ├── app/
│ │ ├── app.go Implements port.App.
│ │ └── ...
│ ├── in/
│ │ ├── grpc/
│ │ │ └── adapter.go
│ │ ├── http/
│ │ │ └── adapter.go
│ │ └── nats/
│ │ └── adapter.go
│ ├── dal/
│ │ ├── adapter.go
│ │ └── migrations/
│ │ └── 00001_some_name.sql
│ └── out/
│ ├── nats/
│ │ └── adapter.go
│ └── redis/
│ └── adapter.go
├── cmd/
│ ├── cli/
│ │ └── main.go
│ └── server/
│ └── main.go
├── dom/ Optional shared pure business types if public outside repo.
├── internal/
│ └── dom/ Optional shared pure business types if only repo-internal.
└── platform/
├── log/
├── metrics/
├── repo/
└── serve/
Rules for this variant:
wire.go is mandatory.integration_test.go next to wire.go is recommended as the first place to smoke test the whole app.port.App, not the concrete internal/app type.port/ is public because it is the bounded-context contract.internal/app stays private and implements port.App.cmd/*/main.go stays thin and should only handle process concerns plus consuming the prepared values returned by wire.go.main may call apps.Registry() to assemble all applications and return a registry such as type Registry struct { Example example_port.App; Other other_port.App; ... }.apps/registry.go is the composition root for all application modules in the repository and should not accumulate unrelated functionality.api/* defines wire formats between processes.port/* defines the in-process contract of the application.internal/in/* adapts api/* or transport-specific DTOs to port.App.internal/out/* and internal/dal/* may depend on api/* when external wire formats are shared.Prefer one port.go file containing all application-facing interfaces:
AppRepo / DAL port(s)Why:
Transaction Script style, Repo methods often mirror App methods closely.App methods.Optional split:
types.goerrors.go only if port.go becomes noisyEven when split into files, keep them in the same port package.
Errors are part of the port contract, not internal implementation details.
port outside app except in the Flat variantTreat port as the application boundary contract and app as its implementation.
That means the default shape is either:
port as a separate package and app implementing itWhy this split matters:
app grows and wants helper subpackages, a port.go inside app starts pushing those subpackages to import app just to reach the contractapp package may need to import its own helper subpackagesSo prefer these defaults:
port.go and app.go in one package is fine because the app is intentionally flatinternal/port + internal/appport/ + private internal/appDo not put port inside app as the default for non-flat layouts.
If you do collapse them temporarily, treat it as a local simplification for a flat app, not as the growth path.
App is the direct application APIFor importable applications, direct in-process calls must go through port.App.
Do not expose internal/app as public API.
This is how:
in/cli adapterTreat the DAL port as a Transaction Script style boundary.
Do not invent extra transaction abstractions unless the concrete case requires them.
A separate app/tx.go layer is usually unnecessary.
Use task-centric DAL methods that reflect business operations and their transactional needs.
This usually gives clearer transaction boundaries than entity-shaped Get/Save repositories.
It also allows SQL to be optimized for each business task instead of being distorted by ORM-like repository shapes.
Like app, the internal structure of dal is free to follow the actual persistence complexity.
Start with one package, then split by files, helpers, or private subpackages only when that improves clarity.
internal/in/*Use internal/in/* only for application-owned inbound adapters reached through transport or messaging.
Typical examples:
Do not create internal/in/cli.
CLI belongs to the executable layer, not to the application-owned inbound adapters.
mainFor CLI binaries:
main.go or cmd/*/main.go as the CLI adapterwire.goport.App directlyThis avoids creating fake abstraction layers for Cobra, Kong, urfave/cli, and similar frameworks.
internal/dalUse internal/dal for the owned database adapter.
Keep migrations near it.
This is separate from out/ because the owned DB is special:
internal/out/*Use internal/out/* for outbound adapters except the owned DB.
Examples:
out/* may depend on api/* when shared external wire contracts exist.
wire.go is mandatory in all package modes.
Its responsibility is wiring only:
Do not make wire.go start servers, background workers, or other lifecycle-managed processes by itself.
That would make the same wiring harder to reuse from CLI code and integration tests.
Do not put dependency graph construction into main.go.
main.go should only coordinate:
wire.goRecommend one main wiring entrypoint per application as the default. In practice, the API that is convenient for the application's own integration tests is often also the API that works well for:
cmd/server/main.gocmd/cli/main.goA single entrypoint helps keep the application boundary coherent. Only introduce multiple wiring entrypoints when the concrete use cases truly diverge.
The main wiring entrypoint should usually return a value that can satisfy all key consumers with minimal adaptation:
port.Appport.App access for introspectionThe stable rule is: make wiring explicit, reusable, testable, and separate from main.go.
Do not optimize wire.go for one executable in a way that makes the same application harder to reuse elsewhere.
Use shared pure business types only in repositories that contain multiple applications, for example apps/* modular monolith repositories.
Possible locations:
dom/ when the shared types are part of public repo APIinternal/dom/ when they are shared only inside the repositoryGood examples:
MoneyEmailCountryCodeTimeRangeDo not turn dom/ into a generic dumping ground.
It must stay pure and must not depend on ports, adapters, or infrastructure.
Prefer short, high-signal names for packages and frequently used identifiers. Short names matter most for packages that are referenced often in code. For package names that mostly stay in the directory tree and are rarely imported directly, clarity and local convention matter more. Recommended package names:
domappinoutdalsrvsvcsubrepo when dal is not the better fitDo not repeat the same business noun at every level.
If the repository or directory already says order_service, do not add redundant names like:
order_service.goorder/order.goservice/order_service.goPrefer consistent generic filenames where the package already provides context:
adapter.go for the main adapter implementation fileapp.go for the main business-logic fileport.go for the main port contract filewire.go for wiringPrefer New as the main constructor name when the package name already explains what is being created.
port.App.For importable applications, external repositories' tests may import the app and start it via public wiring APIs. That is a valid use case and should shape the package mode choice.
In repositories with multiple apps/*, treat each application as the same application module you would otherwise deploy as a separate service.
Moving these modules into one binary changes transport and wiring, not ownership, dependency mapping, or interaction design.
Use apps/registry.go as the in-process composition root for these modules.
Typical shape:
package apps
type Registry struct {
Example example_port.App
Other other_port.App
}
Guidelines:
main or another top-level composition root may call apps.Registry() to assemble all applications.apps/registry.go is a composition root, not a service locator and not a place for unrelated business logic.port.App is the direct in-process API of an application module.When proposing a design or refactor, follow this checklist:
App boundary for the whole bounded context.wire.go explicit and mandatory.port.App as the direct public API for importable apps.internal/in/cli.in/, out/, dal/, app/ over deep adapter/primary/secondary/... trees.dom/ or internal/dom/ only for truly shared pure types in repositories with multiple applications.app or dal grows, split by files first, then by helper subpackages, and only then question the bounded context.apps/registry.go as the composition root.Avoid these default moves unless the specific case truly needs them:
domain/, application/, and service/adapter/primary/secondary directory treesinternal/in/climain.goport.Appinternal/apps/registry.go into a generic dumping ground or service locatorCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub powerman/skills --plugin go-bounded-context-hexagonal