When one feature changes, its routes, rules, and persistence code should be easy to find together. Group application behavior by feature and keep the code that connects features in a visible composition root. Arkoren does not discover files or require a particular directory layout.
A service with only the Quickstart endpoint can keep its handler
in internal/bootstrap. Extract a package when that behavior has enough rules,
tests, or dependencies to deserve its own boundary—not to fill empty directories.
Growing Structure
For a greeting service that later adds a catalog, an application-owned layout could be:
hello/
├── cmd/server/main.go
├── internal/
│ ├── bootstrap/
│ │ ├── app.go
│ │ ├── bootstrap.go
│ │ ├── logging.go
│ │ ├── mcp.go
│ │ ├── router.go
│ │ └── server.go
│ ├── greeting/
│ │ ├── greeting.go
│ │ ├── http.go
│ │ └── http_test.go
│ └── catalog/
│ ├── catalog.go
│ ├── store.go
│ └── store_test.go
└── go.mod
cmd/server owns process entry, signals, and exit status. bootstrap constructs
dependencies, mounts features, and registers lifecycle work. greeting and
catalog own application behavior. internal keeps these implementation packages
behind Go’s internal import boundary.
The generator creates only the executable and bootstrap, not these feature packages or tests. Add files as the application needs them. Environment loading can remain beside construction; introduce an application settings package only when the parsing or sharing described in Configuration justifies one.
Feature Ownership
A feature package owns related behavior from its incoming request to its data
operations. Put its HTTP adapters, business rules, queries, and tests together
rather than spreading them across global handlers, models, and
repositories folders.
That does not mean putting HTTP or SQL inside every rule. Keep calculations and decisions in plain Go functions when they do not need I/O. Let adapters handle request decoding and responses, and let a feature-owned store handle database access. Dependencies should be explicit fields or arguments, not package globals.
If a feature adds jobs, events, or schema changes, keep those declarations with the feature too. Application startup and deployment decide when to run them; the presence of a file does not register a worker or apply a migration. Follow Migration Deployment for schema ownership and rollout ordering.
Mounting Routes
To extract the Quickstart’s endpoint, move its Greeting type and greet helper
into internal/greeting, changing their package declaration to package greeting.
In that package’s http.go, import net/http,
arkoren.dev/core/http/router, and arkoren.dev/core/service/cache, then expose
one route-registration function:
// Routes mounts greeting endpoints using the caller's cache.
func Routes(routes *router.Router, visits *cache.Cache) {
routes.Post("/greetings/{name}", func(w http.ResponseWriter, r *http.Request) error {
return greet(w, r, visits)
})
}
This function registers handlers; it neither constructs a listener nor starts or stops the cache. The handler keeps the request context for cache calls. The feature borrows a facade whose lifetime the caller owns.
Inside the existing Router(visits) factory callback in
internal/bootstrap/router.go, replace the old greeting registration with:
greeting.Routes(routes, visits)
Import your application’s package, example.com/hello/internal/greeting, and
remove net/http from router.go if the old closure was its only use there.
Keep middleware registration before feature routes. Remove the original helper
file from bootstrap only after moving its implementation, so there is one
owner and one registration of the endpoint.
Composition Root
The composition root is where concrete dependencies meet. In the starter,
main passes bootstrap.Compose to app.Run; the root constructs shared
dependencies and passes them into typed factories. The Quickstart’s cache wiring
stays there even after the handler moves:
driver := memorycache.New()
compose.Cleanup(driver.Stop)
compose.Startup(driver.Start)
visits := cache.New(driver)
routes := compose.Use(Router(visits))
This fragment uses the Quickstart’s application.Composer callback and imports
for cache and memorycache. Keep diagnostics and listener selection after it.
The acquiring owner registers cleanup once, immediately after acquisition.
Do not add another cleanup hook in every feature that receives the facade.
Factories capture dependencies when obtained; their callbacks construct objects
and mount routes when selected through compose.Use. Returning a value does not
memoize it, transfer cleanup ownership, or start it. Construct a shared dependency
once and pass that same value to its consumers. Split growing bootstrap code into
typed factories rather than a global service lookup. See
Application Composition for shared outputs and
conditional features.
Feature Boundaries
When one feature needs an immediate answer from another, depend on the narrow
behavior it needs. For example, an application-owned checkout feature could
define this interface, with context imported:
// Availability supplies the stock observation needed by checkout.
type Availability interface {
Available(ctx context.Context, sku string) (bool, error)
}
Bootstrap can pass a catalog implementation that satisfies it without making checkout import the whole catalog package. This observation alone does not reserve stock; a checkout requiring an atomic reservation needs a contract that expresses that operation instead.
If no immediate answer is needed, an application event can notify another feature after a change. The in-process event bus is not a durable cross-process handoff; use a queue when work must survive that boundary. If two packages continually require each other’s details, reconsider whether they represent one feature rather than adding more wiring to preserve the split.
Testing Features
Test pure rules directly. Test HTTP behavior through the feature’s registered router using request/response recorders; a listening socket is unnecessary. Supply local drivers or fakes for the contracts the feature consumes, then use integration tests for the backend-specific behavior those substitutes cannot establish. Testing Overview shows these boundaries.
Local infrastructure is optional application tooling, not a layout requirement. If you add a Compose file, own its credentials, health checks, and teardown in your project. Follow the chosen driver’s setup rather than assuming every test needs a container.
The result is still ordinary Go packages: move or combine them as the application changes. Before deploying the assembled executable, review Deployment for runtime configuration, probes, and shutdown.