2. Project setup
Before writing anything, let’s set up the module and get oriented — where files will go, what tools you’ll want on hand, and the handful of commands you’ll be running constantly for the rest of this tutorial. None of this is exciting on its own, but skipping it means stopping to figure this out later, in the middle of a chapter where it’ll be more disruptive.
Create the module
mkdir servoorders && cd servoorders
go mod init example.com/servoorders
go get github.com/okian/servo/v3
That’s the whole setup — one module, one dependency so far. (The tutorial’s own copy, at
examples/tutorial inside the servo repo, points
github.com/okian/servo/v3 at the local checkout with a replace directive instead of a published
version, because it’s developing servo itself alongside the tutorial. You won’t need that; plain
go get is enough.)
Where things will go
You don’t need to create any of this yet — each directory shows up in the chapter that actually needs it. Keep this page open as a map for “where does this belong”:
servoorders/
go.mod
Makefile local dev commands (below)
cmd/orders/ spec.go + main.go — the injector chapter 13
cmd/ordersgin/ the Gin injector chapter 11
cmd/ordersgrpc/ the gRPC injector chapter 12
deploy/ docker-compose.yml, Dockerfile chapter 19
internal/
config/ typed configuration chapter 3
domain/ core types, no dependency on anything else here chapter 4
repository/ OrderRepository, UserRepository interfaces chapter 5
postgres/ the implementation of both chapter 5
migrations/ embedded SQL, applied on startup chapter 5
cache/ OrderCache interface chapter 6
redis/ the implementation chapter 6
broker/ EventPublisher interface chapter 7
natsbroker/ the implementation chapter 7
notifier/ a subscriber consuming events chapter 7
service/ OrderService: the business logic chapter 8
auth/ JWT issuing and verification chapter 9
session/ per-user state, one instance per logged-in user chapter 14
transport/
api/ HTTP server, router, handlers chapter 10
ginapi/ the same API in Gin chapter 11
grpcapi/ gRPC and REST sharing one port chapter 12
openapi/ API contract, embedded and served chapter 10
admin/ health/readiness/metrics, on their own port chapter 15
observability/ logging, metrics, tracing setup chapter 15
resilience/ circuit breaker, rate limiting chapter 16
mocks/ generated mocks for tests chapter 8 onward
Two things about that shape are deliberate.
An implementation sits under the interface it satisfies. repository/ declares
OrderRepository; repository/postgres/ is the only thing that implements it. Same for
cache/redis and broker/natsbroker. The alternative — postgres/ and repository/ as siblings —
reads as two unrelated packages, and you learn which implements which by opening them. Nesting says
it in the path, and it makes the direction of dependency obvious: an adapter imports its port, never
the other way round.
The names still carry their suffixes: broker/natsbroker rather than broker/nats, because the
NATS client library’s own package is called nats and a file importing both would need an alias on
one of them. Directory nesting buys the grouping without that collision.
Everything lives under internal/, with cmd/ holding the entry points. That is the ordinary
shape for a Go application, and it is worth knowing what it buys, because the mechanism is often
confused with an unrelated one.
Unexported identifiers hide symbols within a package. internal/ restricts who may import the
package at all, and it is the only thing in Go that does: a package under internal/ is importable
only by code rooted at the directory containing that internal/. A flat layout leaves every package
publicly importable the moment the module is fetchable — by another repository, or more likely by a
sibling module in the same monorepo — and “nobody will import it” is a policy nothing enforces.
So internal/ is the default worth reaching for in an application, and the top level is where you
put the handful of packages you genuinely intend other people to use. servo is indifferent either
way: it resolves providers under internal/ exactly as it does anywhere else, and a spec file in
cmd/ may import its own module’s internal/ packages, since the restriction is scoped to the
directory containing internal/.
The cost is one path segment in every import — example.com/servoorders/internal/repository/postgres rather
than example.com/servoorders/postgres — which is why plenty of small services skip it. That is a
defensible trade to make deliberately, and a bad one to make by not thinking about it.
servo’s own internal/ layout is
worth a look for the other half of the picture: a module that really is imported by other people,
where what sits at the top level is a deliberate, small public API.
Install what you’ll need
| Tool | You’ll use it for | Install |
|---|---|---|
servo |
Generating and checking the wiring (chapter 13) | go install github.com/okian/servo/v3/cmd/servo@latest |
Docker + docker compose |
Running Postgres, Redis, NATS locally | docs.docker.com |
golangci-lint |
The CI lint step (chapter 18) | go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest |
You can actually skip installing servo globally — every command in this tutorial works equally
well as go run github.com/okian/servo/v3/cmd/servo <command>, and that’s what the Makefile below
uses, so a fresh clone of the finished project works with nothing pre-installed except Go and
Docker.
Write the Makefile
Add a Makefile at the root now. You’ll be reaching for these commands from chapter 5 onward, and
it’s one less thing to assemble under pressure later:
.PHONY: up down test test-integration run run-gin run-grpc generate check
up:
docker compose -f deploy/docker-compose.yml up -d
down:
docker compose -f deploy/docker-compose.yml down -v
test:
go test ./...
test-integration:
TEST_POSTGRES_DSN="postgres://orders:orders@localhost:5432/orders?sslmode=disable" \
TEST_REDIS_ADDR="localhost:6379" \
TEST_NATS_URL="nats://localhost:4222" \
go test ./... -v
run:
go run ./cmd/orders
# The same service layer behind two other transports, built in chapters 11
# and 12. Neither binary exists yet; the targets go in now so the Makefile
# gets written once.
run-gin:
go run ./cmd/ordersgin
run-grpc:
go run ./cmd/ordersgrpc
generate:
go run github.com/okian/servo/v3/cmd/servo generate
check:
go run github.com/okian/servo/v3/cmd/servo check
Two test targets, not one, and that split is worth understanding now rather than discovering by
accident: test never touches the network. Every package we write will skip its own integration
tests automatically when the matching TEST_*_DSN/TEST_*_ADDR variable is unset, so make test
stays safe to run constantly, from anywhere, with nothing running in the background. make
test-integration is the one that actually needs Postgres, Redis, and NATS up first (make up) —
you’ll use it starting in chapter 5, the moment there’s a real database to test against.
Diagnostics
go: github.com/okian/servo/v3@...: reading github.com/okian/...: 404 Not Found— you’re following along outside the servo repo and haven’t rungo get github.com/okian/servo/v3yet (or you’re pointed at an unpublished fork — use areplacedirective at your local checkout in that case, the same wayexamples/tutorial/go.moddoes).make: docker: command not found— install Docker before continuing; nothing from chapter 5 onward works without it.
Do’s and don’ts
- Do commit
go.sumonce it exists — it’s what makesgo buildreproducible across machines. Never gitignore it. - Don’t reach for a task runner heavier than
makeuntilmakegenuinely runs out of expressiveness (usually: real conditionals, or cross-platformshdifferences). Five targets doesn’t justifyjustor a shell-script framework yet.
Next
Chapter 3: Configuration — the first package this service actually needs, and the first thing every other package will depend on.