gRPC Project Structure
A tidy folder layout keeps a gRPC project easy to read, test, and grow. Small demos fit in one file. Real services need clear homes for proto files, generated code, business rules, and startup logic. Good structure lets a new teammate find any piece within seconds.
The Office Building Analogy
Think of an office building. The reception desk greets visitors and sends them to the right department. The departments do the actual work. The archive room stores records. Each area has one job, and visitors never walk straight into the archive. A gRPC project follows the same plan with three layers.
Request arrives
│
▼
┌────────────┐ Reception: reads the request, returns the reply
│ Handler │
└─────┬──────┘
▼
┌────────────┐ Departments: business rules
│ Service │
└─────┬──────┘
▼
┌────────────┐ Archive: database or other storage
│ Repository │
└────────────┘
Recommended Folder Layout
shop-service/
├── proto/
│ └── shop/orders/v1/orders.proto
├── gen/
│ └── shop/orders/v1/ (generated code)
├── cmd/
│ └── server/main.go (startup only)
├── internal/
│ ├── handler/ (gRPC methods)
│ ├── service/ (business rules)
│ ├── repository/ (database access)
│ └── config/ (settings)
├── Makefile
└── go.mod
Folder Roles
| Folder | Contents | Rule |
|---|---|---|
| proto | Contract files | Mirror the package name in the path |
| gen | Generated code | Never edit by hand |
| cmd/server | Program entry point | Wire parts together and nothing more |
| internal/handler | RPC method bodies | Keep thin |
| internal/service | Business logic | No gRPC types inside |
| internal/repository | Storage code | Hide database details |
Keep Handlers Thin
A handler translates between gRPC and the rest of the program. It reads the request, calls the service layer, and converts errors into status codes. Business rules never live here. That separation lets you test rules without starting a server.
func (h *OrderHandler) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) {
order, err := h.service.Find(ctx, req.OrderId)
if err != nil {
return nil, toStatus(err)
}
return toProto(order), nil
}
Wire Everything in main
The main function builds each layer and connects them. This pattern is called dependency injection. Each layer receives what it needs through its constructor.
func main() {
cfg := config.Load()
db := repository.Connect(cfg.DatabaseURL)
repo := repository.NewOrderRepo(db)
svc := service.NewOrderService(repo)
handler := handler.NewOrderHandler(svc)
server := grpc.NewServer()
pb.RegisterOrderServiceServer(server, handler)
lis, _ := net.Listen("tcp", cfg.Address)
server.Serve(lis)
}
Automate Code Generation
A Makefile target gives every developer the same one command. Nobody needs to remember long protoc flags.
proto:
protoc -I proto --go_out=gen --go-grpc_out=gen proto/shop/orders/v1/orders.proto
Run make proto after every contract change. Teams that use buf replace this target with buf generate.
Share Contracts Across Teams
Several services often use the same proto files. Keep contracts in a dedicated repository. Each service pulls the contract and generates its own code. One source of truth prevents the drift that appears when teams copy files by hand.
contracts-repo ──▶ orders-service (generates code)
──▶ payments-service (generates code)
──▶ mobile-app (generates code)
Configuration
Read ports, database addresses, and secrets from environment variables. Never hard code them. The same build then runs on a laptop, a test server, and production without changes.
Testing Layout
Place test files beside the code they check. Service tests use fake repositories. Handler tests call methods directly with a plain context. An in memory gRPC test, using the bufconn package, covers the full path without opening a network port.
Common Mistakes
- Placing database queries inside handlers
- Editing generated files
- Importing generated types deep into business logic
- Putting every proto file in one giant file
- Skipping versioned package names
Summary
Separate proto files, generated code, handlers, services, and repositories. Keep handlers thin, wire parts in main, and automate code generation. A predictable structure makes every later topic, from interceptors to production releases, easier to apply.
