Implement or review the database layer for a Go service following architecture standards
Implement or review the database layer for $ARGUMENTS following the specs below exactly.
This document describes the top-level structure for a service in this project. Before building any layer, read this file. Then read the spec file for the specific layer you are implementing.
Every service follows this layout:
services/<name>/
├── main.go # Env validation, dependency wiring, server start
├── handler/
│ └── handler.go # Transport layer
├── service/
│ └── service.go # Business logic
├── database/
│ └── database.go # Data access
├── client/
│ └── client.go # Outbound clients to other services or APIs
├── types/
│ └── types.go # Local interfaces for every dependency
└── Dockerfile
Each directory is its own Go package named after the directory.
Implement layers in this order. Each layer depends on the one before it.
types/ — define all interfaces first so every other layer has a contract to depend ondatabase/ — implement the data access layer against the Database interfaceclient/ — implement any outbound clients against their interfacesservice/ — implement business logic using only the interfaces from types/handler/ — implement the transport layer using only the Service interface from types/main.go — wire all concrete implementations together and start the serverDepend on interfaces, not implementations. Every layer holds its dependencies as interfaces from types/. Nothing is imported directly from sibling packages except in main.go.
Inject all dependencies through constructors. All dependencies are passed into New* functions. Nothing is instantiated inside the service or handler.
One responsibility per layer. Transport logic does not touch the database. Business logic does not build queries. Queries do not make outbound calls.
Crash on bad config, log on recoverable errors. Missing environment variables are fatal at startup. Cache failures and best-effort side effects are logged and swallowed. Anything that prevents a correct response is returned as an error.
Shared infrastructure lives in services/common. Reusable clients, interfaces, and domain models are defined there and imported. Never copy infrastructure code into a service.
types/types.goDefine the interfaces that every other layer in the service depends on. This file is the contract layer — it describes what each dependency must provide without specifying how.
Service, Database, SomeClient, etc.Service interface must exactly match the public methods implemented on the service struct — the handler depends on itservices/common and are imported here as neededhandler, service, database, client) — only from services/common and standard librariespackage types
import (
"context"
commontypes "github.com/kabradshaw1/story/services/common"
"github.com/kabradshaw1/story/services/common/genproto/<name>"
)
type Service interface {
MethodOne(ctx context.Context, req *proto.Request) (*proto.Response, error)
MethodTwo(ctx context.Context, req *proto.Request) (*proto.Response, error)
}
type Database interface {
FindRecord(ctx context.Context, id int) (*commontypes.SomeModel, error)
SaveRecord(ctx context.Context, record commontypes.SomeModel) error
}
type SomeClient interface {
Notify(ctx context.Context, payload SomePayload) error
}
Service interface matches the service struct's public methods exactlyservices/common, not redefineddatabase/database.goExecute all data access operations. This is the only layer that communicates with the database.
Connect function (or equivalent) accepts a connection string, establishes the connection, and returns a typed *DB struct; it calls log.Fatal on failurecontext.Context as its first argument and passes it through to the underlying querypackage database
type DB struct {
client *SomeClient
}
func Connect(connectionString string) *DB {
// connect, fatal on error
return &DB{client: client}
}
func (d *DB) FindRecord(ctx context.Context, id int) (*commontypes.SomeModel, error) {
// query using ctx
}
func (d *DB) SaveRecord(ctx context.Context, record commontypes.SomeModel) error {
// insert using ctx
}
For any mutation scoped to a user, always verify ownership before modifying data:
record.User == requestingUser — return a descriptive error if notAfter an update query, do not re-fetch the record to return it. Instead, update the already-fetched struct in memory:
// fetch → verify → update → return modified struct
record.Field = newValue
return &record, nil
fmt.Errorf("failed to find record: %w", err)Connect accepts a connection string and calls log.Fatal on failurecontext.Context as its first parameter