Prompts for Go and Rust: Assembling a High-Load Backend Without an Army of Seniors

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 httpx async task, compare JSON bodies with a diff rate < 0.1%.
  • Phase 2: route on X-Canary: 1 header; 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:

  1. 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.
  2. Benchmark before believing. "This is faster" is a hypothesis until benchstat or criterion says otherwise.
  3. 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.

← All posts

Comments