Tinker AI
Read reviews

Outcome

MVP shipped in 3 weeks vs estimated 6; protobuf-first workflow plus aider produced clean separation of generated and hand-written code

8 min read

I built a Go gRPC service over three weeks using Aider as my primary AI assistant. The service: an inventory API for a small retail backend. About 5000 lines of Go, ~30 RPC methods, gRPC + grpc-gateway for the REST shim.

The interesting finding: protobuf-first development is a really good fit for AI-assisted coding. The proto files act as a specification that aider works against, producing implementation that satisfies the spec. The pattern was tighter than I’d seen on freeform code work.

The setup

Tools used:

  • Aider with Claude 3.5 Sonnet
  • Buf for protobuf compilation
  • protoc-gen-go-grpc and protoc-gen-go for code generation
  • grpc-gateway for the REST translation
  • pgx for Postgres
  • Wire for dependency injection
  • Go 1.22

The repo structure I started with:

.
├── proto/
│   └── inventory/
│       └── v1/
│           └── inventory.proto
├── gen/                       # generated code, gitignored
├── internal/
│   ├── server/                # gRPC handlers
│   ├── repository/            # database access
│   ├── service/               # business logic
│   └── domain/                # types
├── cmd/
│   └── server/
│       └── main.go
└── buf.yaml / buf.gen.yaml

This isn’t unusual for Go services. The interesting thing about it for AI work: the proto/ directory is the source of truth. Everything else is downstream.

The workflow

The pattern that emerged after the first few days:

  1. Define the RPC method in the proto file (manually, by me)
  2. Run buf generate to produce the Go interface
  3. Open aider with the proto file, generated interface, and the relevant repository/service files
  4. Ask aider to implement the method following the existing patterns
  5. Review, test, commit

The proto file was the specification. The generated Go interface was the type signature aider had to satisfy. The existing files were the patterns aider should follow.

This is more constrained than typical “write a function” prompts. The constraints helped — aider had less room to invent things, and the things it produced fit the project better.

A specific example

A method for adjusting inventory quantity. The proto:

rpc AdjustQuantity(AdjustQuantityRequest) returns (AdjustQuantityResponse) {
  option (google.api.http) = {
    post: "/v1/items/{item_id}/adjust"
    body: "*"
  };
}

message AdjustQuantityRequest {
  string item_id = 1;
  string warehouse_id = 2;
  sint32 delta = 3;
  string reason = 4;
  string idempotency_key = 5;
}

message AdjustQuantityResponse {
  string item_id = 1;
  string warehouse_id = 2;
  int64 new_quantity = 3;
  google.protobuf.Timestamp adjusted_at = 4;
}

After buf generate, I had a Go interface I needed to implement. I opened aider:

> /add proto/inventory/v1/inventory.proto
> /add gen/proto/inventory/v1/inventory_grpc.pb.go
> /add internal/server/items.go
> /add internal/service/items.go
> /add internal/repository/items.go
> implement the AdjustQuantity RPC. Follow the patterns in this file —
> handler validates input and calls service, service handles business
> logic and calls repository, repository does the SQL. Use the existing
> idempotency_key tracking pattern from CreateItem. Tests should follow
> the testify pattern from items_test.go.

Aider produced:

  • The handler in internal/server/items.go (input validation, service call, response shaping)
  • The service method in internal/service/items.go (business logic, idempotency check)
  • The repository method in internal/repository/items.go (SQL with pgx)
  • Tests in three matching test files

About 200 lines of new code total. Aider got it right on the first attempt, which surprised me. Reviewing for correctness took ~10 minutes. Total time: ~15 minutes for an RPC method.

Pre-AI estimate for the same method: 60-75 minutes.

Why this worked

A few things I think made the protobuf-first workflow effective:

The interface is unambiguous. Aider knew exactly what types to use, what fields to populate, what method signature to match. There was no inventing — the proto specified it.

The architecture was already decided. Three layers (handler, service, repository) with clear responsibilities. Aider’s job was to fill in each layer, not to design the architecture.

The patterns were visible. With existing implementations as reference, aider could produce matching code. It wasn’t generating a generic Go service; it was generating one that fit my project’s conventions.

Idempotency was a known pattern. I’d implemented idempotency once (in CreateItem) and aider could see the pattern. New methods inherited it correctly.

Tests were straightforward. Each method had a clear contract. Test cases came from the proto’s documentation comments and the obvious cases (valid, invalid, edge cases).

What aider got wrong

Not everything was smooth. Across the three weeks:

Generated code awareness. Aider sometimes tried to “improve” things in gen/. The generated code is regenerated on every build; manual changes are lost. I added gen/ to .aiderignore after this.

SQL with sqlc patterns. I wasn’t using sqlc but aider sometimes wrote queries in sqlc style. They worked but didn’t match the project. Adding “use pgx, not sqlc” to .aider.conf.yml fixed this.

Complex business logic. A few methods had business rules that weren’t in the proto comments. Aider produced implementations that satisfied the proto but missed the implicit rules. Caught in review; required iteration.

Wire dependency injection. Aider’s understanding of Wire is uneven. When I added a new dependency, aider sometimes forgot to update the wire config. Manual fix.

Streaming RPCs. I had three streaming RPCs (server-side streaming for change feeds). Aider’s first implementations had subtle bugs with stream lifecycles and context cancellation. Required careful review.

The protobuf advantage

The protobuf-first pattern has properties that compound across the project:

Documentation lives with the spec. Each RPC method has comments in the proto. These comments are the source of truth for what the method does. Aider reads them. New methods get correct implementations because the comments are there.

Schema evolution is structured. Adding a field to a request type is well-understood — set up backward compatibility, generate code, update implementations. Aider handles this competently because the protobuf docs are clear about the patterns.

Cross-language clients are free. I generated TypeScript clients for the frontend and Python clients for an internal tool. Aider had nothing to do with this; protobuf generation handled it. But the result was that my AI-assisted Go work produced consistent multi-language artifacts.

What I’d do differently

A few things I’d change next time:

Define more methods upfront in protobuf. I defined methods incrementally as I built features. Defining all 30 methods upfront in the proto file (even with TODO bodies) would have made the work more parallelizable. I could have asked aider to implement multiple methods in one session.

Test fixtures from protobuf. I hand-rolled test fixtures. Generating fixtures from the protobuf message types (fake data that matches the schema) would have saved time on tests.

More aggressive use of buf lint. Buf has lint rules for protobuf style. I had them on but loose. Stricter rules would have caught some inconsistencies aider introduced (variable casing, comment style).

The cost

Total aider API spend across three weeks: $43.

Compared to my pre-AI estimate of 6 weeks for the project, the savings is roughly 3 weeks of focused work. That’s worth far more than $43.

The savings broke down roughly as:

  • 50% on RPC method implementation
  • 30% on tests
  • 20% on supporting code (config, observability, deployment)

The RPC method numbers are the biggest. The protobuf-first workflow is what made aider’s contribution there so high.

Recommendation

For new Go gRPC services, the protobuf-first + aider workflow is a strong default. The constraints from protobuf give aider clear specifications to satisfy. The architectural patterns from a few hand-built methods give it patterns to follow. The result is code that fits the project on first attempt at a higher rate than I’d seen with freeform Go work.

I’d extend this recommendation to other code-generation-friendly stacks: TypeScript with tRPC, Rust with tonic (though Rust’s other challenges complicate this), Python with grpcio + betterproto. The shape is similar — define the spec, generate the interfaces, fill in the implementations with AI help.

For Go services without gRPC (REST-only), the workflow is less structured. Aider works fine there too, but the productivity gain is smaller because there’s less specification to ground the model in.