Prompts for Go and Rust: Assembling a High-Load Backend Without an Army of Seniors
Hiring a senior Go or Rust engineer in 2026 is still a bottleneck: the talent pool is small, salaries are high, and onboarding takes months. Meanwhile, the demand for high-load backends — payment gateways, real-time APIs, stream processors — keeps growing. The pragmatic answer is not to replace engineers, but to amplify them with precise AI prompts. A well-crafted prompt turns a general-purpose LLM into a focused pair-programmer that knows Go's scheduler internals, Rust's borrow checker, and the exact shape of a gRPC service definition.
In this article I'll share a set of prompts I've used and refined while building production backends: REST and gRPC services, concurrency-heavy workers, profiling sessions, and migrations from legacy monoliths. Each prompt is a complete, copy-pasteable block with a real example of input and a plausible output. The goal is not to generate code blindly, but to get a structured, reviewable answer that a mid-level engineer can validate and ship.
A quick note on tooling: LLMs in 2026 are good at generating idiomatic Go and Rust, but they still hallucinate APIs. Always cross-check generated code against official documentation — go.dev, doc.rust-lang.org, grpc.io, and the relevant RFCs when it comes to HTTP semantics (RFC 9110).
1. Service Decomposition: From Monolith to Bounded Contexts
Task: Break a legacy monolith into microservices without creating a distributed mess.
Prompt:
You are a systems architect. Given the following monolith description, propose a decomposition into 3–5 microservices.
For each service: name, responsibility, data ownership (which tables it owns), and the API it exposes.
Use Domain-Driven Design bounded contexts. Avoid shared databases.
Monolith: an e-commerce backend with a single PostgreSQL DB. Modules: users, catalog, cart, orders, payments, notifications, admin.
Output as a Markdown table with columns: Service
| Responsibility | Owned tables | API style (REST/gRPC) | Rationale.
Example result (excerpt):
| Service | Responsibility | Owned tables | API style | Rationale |
|---|---|---|---|---|
| Identity | Auth, profiles | users, sessions | REST + JWT | Low write volume, cacheable |
| Catalog | Products, search | products, categories | gRPC (internal) | High read QPS, needs streaming |
| Orders | Cart, checkout, order state | carts, orders | REST + gRPC | Transactional boundary |
| Payments | PSP integration, ledger | payments, refunds | gRPC | Strict consistency |
| Notifications | Email, push | outbox | Async (Kafka/NATS) | Eventual consistency is fine |
The prompt forces the model to assign data ownership, which is the single most common mistake in naive decompositions.
2. Designing a gRPC Contract Before Writing Code
Task: Get a reviewable .proto file that matches your domain.
Prompt:
Write a proto3 file for an Orders service.
Requirements:
- CreateOrder, GetOrder, ListOrders (server streaming), CancelOrder, StreamOrderEvents (bidirectional).
- Use google.protobuf.Timestamp for times, int64 for money in minor units, string UUID for IDs.
- Add field-level comments and reserve field numbers 100–199 for future internal use.
- Include a message OrderEvent with a oneof for event types: created, paid, shipped, cancelled.
Follow the Google API Design Guide naming conventions.
Example result (excerpt):
syntax = "proto3";
package orders.v1;
import "google/protobuf/timestamp.proto";
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (Order);
rpc GetOrder(GetOrderRequest) returns (Order);
rpc ListOrders(ListOrdersRequest) returns (stream Order);
rpc CancelOrder(CancelOrderRequest) returns (Order);
rpc StreamOrderEvents(stream OrderEventAck) returns (stream OrderEvent);
}
message Order {
string id = 1; // UUID v4
string user_id = 2;
int64 total_minor = 3; // amount in minor units
google.protobuf.Timestamp created_at = 4;
reserved 100 to 199; // internal fields
}
message OrderEvent {
oneof kind {
OrderCreated created = 1;
OrderPaid paid = 2;
OrderShipped shipped = 3;
OrderCancelled cancelled = 4;
}
}
Run buf lint and buf breaking against this file before generating stubs — that's how you catch issues the LLM cannot.
3. Go Concurrency: Worker Pools and Backpressure
Task: Turn a naive goroutine-per-request pattern into a bounded worker pool.
Prompt:
Refactor the following Go function to use a bounded worker pool with backpressure.
Constraints:
- Do not spawn unbounded goroutines.
- Use context.Context for cancellation.
- Return the first error, cancel remaining work, and drain the channel safely.
- Use only the standard library (no errgroup, no semaphore package).
Function:
func Process(items []Item) error {
for _, it := range items {
go func(i Item) { _ = handle(i) }(it)
}
return nil
}
Example result (excerpt):
func Process(ctx context.Context, items []Item, workers int) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
jobs := make(chan Item)
errCh := make(chan error, 1)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for it := range jobs {
if err := handle(ctx, it); err != nil {
select {
case errCh <- err:
cancel()
default:
}
return
}
}
}()
}
go func() {
defer close(jobs)
for _, it := range items {
select {
case jobs <- it:
case <-ctx.Done():
return
}
}
}()
wg.Wait()
select {
case err := <-errCh:
return err
default:
return ctx.Err()
}
}
This pattern is the backbone of high-load ingestion pipelines. The default in the error channel prevents goroutine leaks if multiple workers fail simultaneously.
4. Rust: Async vs Sync for a CPU-Bound Endpoint
Task: Decide whether a workload should live in Tokio or in spawn_blocking.
Prompt:
I have a Rust service using Tokio. One endpoint performs JSON parsing plus HMAC-SHA256 signature verification over a 2 MB payload.
Analyze whether this should run on the async runtime or via tokio::task::spawn_blocking.
Explain the trade-offs in terms of Tokio's cooperative scheduling, and show the recommended code with proper error handling using anyhow.
Reference the Tokio documentation on blocking operations.
Example result (excerpt):
use anyhow::Result;
use tokio::task;
pub async fn verify(payload: Vec<u8>, key: Vec<u8>) -> Result<bool> {
// CPU-bound work must not block the async reactor.
let ok = task::spawn_blocking(move || {
let mac = hmac_sha256(&key, &payload);
// constant-time compare
subtle::ConstantTimeEq::ct_eq(&mac, &expected(&key, &payload)).into()
})
.await?;
Ok(ok)
}
The key rule: any operation longer than ~10–100 µs of CPU work belongs in spawn_blocking. The Tokio docs call this out explicitly in the "CPU-bound tasks and blocking code" section.
5. REST vs gRPC: A Decision Matrix
Task: Justify the protocol choice for each service boundary.
Prompt:
Build a decision matrix for choosing REST (HTTP/JSON) vs gRPC for internal and external service boundaries.
Rows: external clients (browsers), mobile, service-to-service, streaming, low-latency internal RPC.
Columns: protocol, transport, typical payload, browser support, tooling maturity, recommended use.
Then give a one-paragraph recommendation for a payments platform with a public API and internal microservices.
Cite RFC 9110 for HTTP semantics and grpc.io for gRPC.
Example result (excerpt):
| Boundary | Protocol | Transport | Recommended |
|---|---|---|---|
| Browser | REST/JSON | HTTP/1.1 or HTTP/2 | ✔ |
| Mobile app | gRPC-web or REST | HTTP/2 | gRPC-web |
| Service-to-service | gRPC | HTTP/2 | ✔ |
| Streaming telemetry | gRPC streaming | HTTP/2 | ✔ |
| Public partner API | REST/JSON | HTTP/1.1 | ✔ |
For a payments platform: expose REST to the public (browsers, partners, RFC 9110 semantics, cache-friendly), and use gRPC internally (strict contracts, streaming, codegen).
6. Profiling a Go Service with pprof
Task: Find the hot path in a slow endpoint.
Prompt:
My Go HTTP handler /api/search takes 400 ms p99. I suspect allocations.
Give me a step-by-step pprof workflow:
1. How to enable net/http/pprof safely in production.
2. Exact commands to capture CPU and heap profiles with curl.
3. How to read top, list, and web output.
4. What to look for: alloc_objects, inuse_space, flat vs cum.
Use only standard Go tooling.
Example result (excerpt):
# Capture 30s CPU profile
curl -o cpu.pprof 'http://localhost:6060/debug/pprof/profile?seconds=30'
go tool pprof -http=:8080 cpu.pprof
# Heap profile
curl -o heap.pprof 'http://localhost:6060/debug/pprof/heap'
go tool pprof -top -sample_index=alloc_space heap.pprof
Typical finding: strings.Split inside a hot loop allocating gigabytes. Fix: preallocate with make([]string, 0, n) or use bytes.IndexByte. In one real case, replacing fmt.Sprintf in a logging path with slog structured fields cut allocations by an order of magnitude.
7. Rust Memory Profiling with DHAT and Flamegraph
Task: Diagnose a Rust service that grows RSS over time.
Prompt:
A long-running Rust service's RSS grows steadily. Guide me through diagnosing it.
Cover: cargo flamegraph, DHAT (dhat crate), heaptrack, and how to interpret allocation stacks.
Show a minimal main.rs that instruments DHAT with the default feature.
Explain the difference between a true leak (Rc cycle) and unbounded growth (a Vec that never shrinks).
Example result (excerpt):
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
fn main() {
let _profiler = dhat::Profiler::new_heap();
run_service();
// dhat-heap.json is written on drop
}
Common culprits: Rc<RefCell<T>> cycles (use Weak), or a metrics HashMap keyed by unbounded label combinations. DHAT's "Total bytes" vs "At t-gmax" columns separate the two.
8. Database Access: Connection Pool Sizing
Task: Avoid the classic "pool too small, latency too high" trap.
Prompt:
For a Go service using database/sql with PostgreSQL, explain how to size SetMaxOpenConns, SetMaxIdleConns, and SetConnMaxLifetime.
Give the formula based on: number of app instances, DB max_connections, and average query time.
Show a realistic configuration and explain the failure mode of setting MaxOpenConns too high.
Reference the PostgreSQL docs on max_connections.
Example result (excerpt):
db.SetMaxOpenConns(20) // per instance
db.SetMaxIdleConns(20) // == MaxOpen, avoid churn
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)
Rule of thumb: total open connections across all instances must stay below PostgreSQL's max_connections minus a reserve for admin tasks. Setting MaxOpenConns to hundreds per instance causes context-switch storms and FATAL: sorry, too many clients already.
9. Migrating a Legacy Endpoint to Rust
Task: Incremental, zero-downtime migration.
Prompt:
I have a Python Flask endpoint /v1/report that takes 2s p95. I want to rewrite it in Rust with Axum, but I cannot do a big-bang cutover.
Design a strangler-fig migration:
- Phase 1: shadow traffic (mirror requests, compare responses).
- Phase 2: canary 5% -> 50% -> 100%.
- Phase 3: decommission.
For each phase: metrics to watch, rollback trigger, and the code hook in the Python app to mirror requests.
Output as a checklist.
Example result (excerpt):
- Phase 1: mirror via an
httpxasync task, compare JSON bodies with a diff rate < 0.1%. - Phase 2: route on
X-Canary: 1header; rollback if p99 of Rust path > Python path. - Phase 3: keep the Python code behind a feature flag for 2 weeks.
This is exactly the pattern described in Martin Fowler's "StranglerFigApplication" article — worth citing when you pitch it to stakeholders.
10. Debugging a Go Data Race
Task: Reproduce and fix a race reported by the race detector.
Prompt:
Here is a race detector report:
WARNING: DATA RACE
Write at 0x00c0000b4010 by goroutine 7:
main.(*Cache).Set()
Read at 0x00c0000b4010 by goroutine 9:
main.(*Cache).Get()
Explain the report line by line, then provide three fixes: mutex, sync.Map, and sharded map.
Compare them on read-heavy workloads. Include a benchmark snippet using testing.B.
Example result (excerpt):
type Cache struct {
mu sync.RWMutex
m map[string][]byte
}
func (c *Cache) Get(k string) ([]byte, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.m[k]
return v, ok
}
For read-heavy workloads with low contention, sync.RWMutex wins. For high-cardinality keys, sharded maps (e.g., 256 shards by fnv hash) scale better. sync.Map is best when keys are written once and read many times.
11. Observability: OpenTelemetry in Go and Rust
Task: Get consistent traces across polyglot services.
Prompt:
Show how to instrument a Go service and a Rust service with OpenTelemetry so that traces propagate across a gRPC call.
For Go: use otelhttp and otelgrpc interceptors.
For Rust: use tracing-opentelemetry and tonic.
Include the W3C Trace Context header propagation and a sample span.
Reference the OpenTelemetry specification and the W3C Trace Context recommendation.
Example result (excerpt):
// Go server
handler := otelhttp.NewHandler(mux, "api")
// gRPC client
conn, _ := grpc.Dial(addr, grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()))
// Rust client
let channel = tonic::transport::Channel::from_static(addr)
.connect().await?;
let mut client = OrderClient::with_interceptor(channel, tracing_interceptor);
Both propagate traceparent (W3C Trace Context, w3.org/TR/trace-context), so a single trace spans both languages.
12. Load Testing with k6 and Vegeta
Task: Produce a realistic load test, not a synthetic one.
Prompt:
Write a k6 script to load-test a REST endpoint with:
- Ramp-up 0 -> 500 VUs over 2 min, hold 5 min, ramp-down 1 min.
- Mixed read/write ratio 9:1.
- Thresholds: p95 < 200 ms, error rate < 0.5%.
Also give a Vegeta one-liner for a quick smoke test.
Explain how to avoid the "coordinated omission" trap.
Example result (excerpt):
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 500 },
{ duration: '5m', target: 500 },
{ duration: '1m', target: 0 },
],
thresholds: { http_req_duration: ['p(95)<200'], http_req_failed: ['rate<0.005'] },
};
echo "GET https://api.example.com/v1/orders"
| vegeta attack -rate=200 -duration=30s | vegeta report
Coordinated omission is avoided by k6's constant-arrival-rate executor, which schedules requests regardless of response time.
13. Error Handling: Go's errors.Is vs Rust's thiserror
Task: Standardize error handling across the codebase.
Prompt:
Compare idiomatic error handling in Go (errors.Is/As, wrapping with %w) and Rust (thiserror, anyhow).
For each, show how to define a domain error, wrap it, and match on it at the API boundary.
Then give a Go function that maps a domain error to an HTTP status code, and the Rust equivalent with Axum's IntoResponse.
Example result (excerpt):
var ErrNotFound = errors.New("not found")
func toHTTP(err error) int {
switch {
case errors.Is(err, ErrNotFound):
return http.StatusNotFound
default:
return http.StatusInternalServerError
}
}
#[derive(thiserror::Error, Debug)]
pub enum DomainError {
#[error("not found")] NotFound,
#[error(transparent)] Other(#[from] anyhow::Error),
}
impl IntoResponse for DomainError {
fn into_response(self) -> Response {
match self {
DomainError::NotFound => StatusCode::NOT_FOUND.into_response(),
DomainError::Other(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
}
Go's %w and errors.Is mirror Rust's #[from] and ? operator — both preserve the causal chain, which is what you actually need in production logs.
14. Graceful Shutdown and Zero-Downtime Deploys
Task: Stop dropping in-flight requests during rolling updates.
Prompt:
Show a production-ready graceful shutdown for:
1. Go net/http server with signal.NotifyContext and Shutdown.
2. Rust Axum with tokio::signal and graceful shutdown.
Include: draining connections, a readiness probe that flips to unhealthy before shutdown, and a shutdown timeout.
Explain how Kubernetes terminationGracePeriodSeconds interacts with this.
Example result (excerpt):
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM)
defer stop()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)
axum::serve(listener, app)
.with_graceful_shutdown(async {
tokio::signal::ctrl_c().await.ok();
})
.await?;
Set terminationGracePeriodSeconds higher than your shutdown timeout, and flip /readyz to 503 a few seconds before SIGTERM so the load balancer stops routing.
15. Code Review Prompt: Catch Idioms and Anti-Patterns
Task: Turn the LLM into a reviewer that knows both languages' idioms.
Prompt:
Review the following Go code as a senior engineer. Flag:
- Unnecessary allocations in hot paths.
- Missing context propagation.
- Ignored errors.
- Goroutine leaks.
- Non-idiomatic naming (Go uses short names, no Get prefixes on getters).
For each issue: line, severity (blocker/major/minor), and a concrete fix.
Example result (excerpt):
| Line | Severity | Issue | Fix |
|---|---|---|---|
| 12 | blocker | go handle(x) without context |
pass ctx, add errgroup |
| 27 | major | fmt.Sprintf in loop |
use strconv |
| 41 | minor | GetUser() name |
rename to User() |
This prompt is the cheapest senior-review substitute for a junior-heavy team, but it's a first pass — humans still approve the merge.
16. Benchmarking: Go testing.B vs Rust criterion
Task: Get comparable, statistically sound benchmarks.
Prompt:
Write equivalent microbenchmarks in Go (testing.B) and Rust (criterion) for a function that hashes a 1 KB buffer.
Explain: warm-up, b.N calibration, and how criterion reports confidence intervals vs Go's single-number ns/op.
Show how to avoid the compiler optimizing away the call (black_box in Rust, sink variable in Go).
Example result (excerpt):
func BenchmarkHash(b *testing.B) {
buf := make([]byte, 1024)
var sink uint64
b.ReportAllocs()
for i := 0; i < b.N; i++ {
sink = xxhash.Sum64(buf)
}
_ = sink
}
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn bench(c: &mut Criterion) {
let buf = vec![0u8; 1024];
c.bench_function("hash_1kb",
|b| b.iter(|| xxhash_rust::xxh3::xxh3_64(black_box(&buf))));
}
Criterion runs multiple rounds and reports outliers; Go's b.N calibration is simpler but noisier. For CI gates, use benchstat to compare two Go runs statistically.
17. Security: Input Validation and SQL Injection
Task: Harden a data-access layer.
Prompt:
Show secure parameterized queries in Go (database/sql) and Rust (sqlx) for a search endpoint with dynamic filters.
Explain why string concatenation is unsafe even with escaping.
Show a safe dynamic filter builder using placeholders only.
Then add input validation rules: max length, allowed characters, and a note on OWASP SQL Injection prevention.
Example result (excerpt):
q := "SELECT id FROM orders WHERE user_id = $1"
args := []any{userID}
if status != "" {
q += " AND status = $2"
args = append(args, status)
}
rows, _ := db.QueryContext(ctx, q, args...)
let rows = sqlx::query("SELECT id FROM orders WHERE user_id = $1 AND status = $2")
.bind(user_id)
.bind(status)
.fetch_all(&pool).await?;
Never interpolate user input into SQL. The OWASP SQL Injection Prevention Cheat Sheet is the canonical reference here.
18. The Prompt That Writes Your ADR
Task: Document architectural decisions so the next hire (or the AI) has context.
Prompt:
Write an Architecture Decision Record (ADR) in the Michael Nygard format for the decision: "Use gRPC for internal service-to-service communication, REST for public API."
Sections: Title, Status, Context, Decision, Consequences (positive and negative), Alternatives considered.
Keep it under 400 words. Output in Markdown.
Example result (excerpt):
# ADR-007: gRPC internally, REST externally
## Status
Accepted (2026-10)
## Context
Our 7 services communicate over JSON/REST. Schemas drift; streaming is awkward.
## Decision
Use gRPC for internal calls; expose REST to browsers and partners.
## Consequences
+ Strong contracts via .proto, codegen in Go and Rust.
+ Native streaming.
- Added tooling (buf, grpc-gateway).
- Browser clients need gRPC-web or a REST facade.
## Alternatives
- All-REST: rejected, no streaming, weaker contracts.
- All-gRPC: rejected, browser friction.
ADRs are the cheapest way to keep a distributed system comprehensible. They also feed future prompts with context the LLM would otherwise lack.
How to Actually Use These Prompts
Treat each prompt as a specification, not a magic wand. The output is a draft that a mid-level engineer reviews, tests, and adapts. Three rules keep this safe:
- Verify against primary sources. Go's docs, Rust's std docs, gRPC docs, and the relevant RFCs. If the LLM cites an API, check it exists.
- Benchmark before believing. "This is faster" is a hypothesis until
benchstatorcriterionsays otherwise. - Keep humans in the merge loop. AI can draft the
.proto, the worker pool, and the ADR — but the approval is yours.
The talent shortage in Go and Rust is real, but the bottleneck is often not headcount — it's the time seniors spend on boilerplate, reviews, and documentation. These prompts don't replace that expertise; they multiply it. Start with one prompt, apply it to a real service you own, and measure the delta in review time. Then iterate.
If you want to go deeper into structured AI-assisted engineering, explore the courses on asibiont.com — the catalogue covers backend, DevOps, and prompt engineering for developers. And if you have a prompt that saved your team a sprint, share it: the best patterns in this space are still being discovered in production, not in spec sheets.
Comments