15 min read · Days 16–35 · Notion
Goal: Learn Go as your primary backend language, design APIs at a professional level, and build secure auth systems including DPoP — a technique most senior engineers don’t know.
Backend engineering is not just writing functions that run on a server. It’s designing contracts between systems. REST, gRPC, GraphQL are different contract languages — each with different tradeoffs. Auth is not a feature, it’s an infrastructure layer. This phase teaches you to design, not just code.
select for multiplexing channelscontext.Context: cancellation propagation across goroutinesdefer, panic, recover — and when NOT to use them/v1/), header, query param — tradeoffsctx.WithDeadline() propagated across service boundariesgrpc_health_v1author { posts { comments } } can fire 100+ SQL queriesSecure, HttpOnly, SameSite=Lax/Strict/Nonealg: none attack — why you must validate the algorithmcode_challenge, code_verifier.jtiStack: Go, gin, PostgreSQL, Redis, JWT, zap logger
Hexagonal architecture (ports and adapters). Domain layer has no framework imports. JWT auth with 15-minute access token and 7-day refresh token stored in Redis. Rate limiting per user ID (100 req/min). Structured JSON logging with request IDs. Graceful shutdown with signal.NotifyContext. Full error handling with RFC 7807 responses.
Deliverable: A fully deployable Go service with Dockerfile, proper error handling, and a Postman collection proving all endpoints work.
Stack: Go, grpc-go, protobuf, self-signed TLS certs
User service with 4 endpoints: CreateUser, GetUser, ListUsers, DeleteUser. Chain of interceptors: (1) logging with request duration, (2) auth token validation, (3) retry on UNAVAILABLE. Client uses grpcurl for manual testing. Server uses reflection for discovery.
Deliverable: grpcurl -plaintext localhost:50051 describe shows your service schema. All 4 methods work with proper status code responses.
Stack: Go, Redis, RSA/EC key generation, RFC 9449
Implement the Auth Code + PKCE flow end-to-end. Issue access tokens that are DPoP-bound: the token contains a confirmation (cnf) claim with the public key thumbprint. On the resource server, validate the DPoP proof JWT on every request: check HTTP method, URL, iat (timestamp), jti (unique ID, stored in Redis to prevent replay).
Deliverable: A Postman collection that demonstrates: (1) standard bearer token gets rejected by resource server, (2) DPoP-bound token only works with the matching private key.
❌ Using goroutines as if they’re free.
Each goroutine starts at 2KB and can grow to gigabytes. A goroutine that never exits is a memory leak. A server that spawns a goroutine per request without bounding them will OOM under load.
✅ Mental model: Use context.Context for cancellation. Use sync.WaitGroup to wait for completion. Use worker pools to bound concurrency. Use goleak in tests to catch goroutine leaks.
❌ Storing JWT signing secrets in config files or environment variables.
Config files end up in git. Env vars appear in process dumps and CI logs. A leaked signing secret means every token ever issued is compromised.
✅ Mental model: JWT keys should be managed by a secrets system (HashiCorp Vault, AWS Secrets Manager) and rotated regularly. Prefer asymmetric keys (RS256/ES256) so the public key can be published via JWKS without exposing the signing key.
❌ Using bearer tokens without DPoP for financial or high-security APIs.
A stolen bearer token works from any machine, any IP, any client. Bearer tokens stolen from logs, proxies, or MITM attacks can be replayed.
✅ Mental model: DPoP binds the token to the holder’s private key. The token becomes a “proof-of-possession” credential. Stealing the token without the private key is useless. This is why OAuth 2.0 FAPI 2.0 mandates DPoP.
❌ Designing REST endpoints as RPC calls.
/getUser, /createOrder, /deleteProduct is RPC masquerading as REST. You lose all the semantic benefits of HTTP: caching, idempotency semantics, method-based routing.
✅ Mental model: REST models resources, not actions. GET /users/123, POST /orders, DELETE /products/456. Actions that don't fit become state transitions: POST /orders/456/cancel (transitioning to cancelled state).
Stripe: Their API versioning strategy (date-based versions pinned per API key) is the industry gold standard. Every breaking change gets a new YYYY-MM-DD version. Existing integrations never break. This requires maintaining multiple API response formats simultaneously — a significant engineering investment that they view as a product moat.
Uber: Migrated from REST to gRPC across 1,000+ microservices. The key ROI: strongly typed Protobuf contracts eliminated entire classes of production bugs caused by undocumented REST API changes. They saw 30% reduction in serialization overhead.
Auth0 / Okta: DPoP adoption is now required for financial-grade APIs (FAPI 2.0). Auth0 shipped DPoP support and documented the implementation in detail. If you’re building fintech auth, DPoP is mandatory, not optional.
GitHub: Migrated from REST to GraphQL for their public API v4. They solved N+1 with DataLoader. Then found that introspection queries were expensive at scale and introduced query complexity limits and persisted queries. The lesson: GraphQL requires active performance management.
Core mental model: Go is built for network services: cheap concurrency, fast startup, simple deployment, and explicit error handling. Learn the runtime because production bugs often happen at the boundary between goroutines, memory, cancellation, and I/O.
G-M-P scheduler: Goroutines (G) are scheduled onto OS threads (M) through logical processors (P). A P owns runnable goroutine queues and enables Go to multiplex many goroutines over fewer OS threads. This is why Go can handle many concurrent requests without creating one expensive OS thread per request.
Goroutines: They start with small stacks that grow as needed, but they are not free. Goroutines blocked forever on channels, timers, network calls, or missing cancellation are memory leaks. Every goroutine should have an exit path.
Channels and select: Channels coordinate ownership and communication. Use unbuffered channels for handoff and buffered channels for bounded queues. select lets one goroutine wait on multiple channel operations, especially cancellation through ctx.Done().
Context: context.Context carries cancellation, deadlines, and request-scoped values across goroutines and service boundaries. Do not store it in structs. Pass it as the first parameter to functions that do I/O or may block.
Interfaces: Interfaces are implicit contracts. Prefer small interfaces defined near the consumer. This keeps tests simple and prevents framework-style abstractions too early.
GC: Go uses concurrent mark-and-sweep GC. Lower allocation rates usually matter more than tuning. Tune GOGC only after profiling.
Practice: Build a worker pool with context cancellation, a timeout, graceful shutdown, and tests that prove all goroutines exit.
Core mental model: Language choice is an operational decision, not only a syntax preference.
Node.js: Excellent for I/O-heavy workloads and frontend-adjacent teams. The event loop is efficient, but CPU-heavy work blocks unless moved to workers or separate services. Package and runtime complexity can become operational overhead.
Java: Strong ecosystem, mature observability, excellent performance after warmup, and great for large enterprise systems. JVM startup, memory overhead, and GC tuning are the common costs.
Go: Small static binaries, fast startup, simple deployment, good concurrency, and predictable performance. Tradeoffs include less expressive type-system features than Java/Scala and manual care around error handling and shared memory.
Decision frame: Choose Go for API services, proxies, CLIs, infrastructure tools, and network-heavy systems. Choose Java when ecosystem maturity, JVM tooling, or existing enterprise architecture dominates. Choose Node when product velocity and JavaScript ecosystem alignment matter most.
Core mental model: REST is about resources, representations, and HTTP semantics. Good API design makes retries, caching, pagination, errors, and versioning predictable.
Resource design: Use nouns, not verbs: GET /users/123, POST /orders, DELETE /sessions/current. Actions that do not map cleanly can be modeled as state transitions, such as POST /orders/123/cancel.
Idempotency: Idempotent operations can be retried safely because repeated execution has the same effect. GET, PUT, and DELETE should be idempotent. POST is normally not, so payment/order APIs use idempotency keys.
Status codes: Use 201 for created, 204 for successful no-body responses, 400 for malformed requests, 401 unauthenticated, 403 unauthorized, 409 conflict, 422 semantically invalid data, 429 rate limited, and 503 unavailable.
Pagination: Offset pagination becomes slow and inconsistent at scale. Cursor pagination uses stable ordering and a cursor from the last item seen.
Errors: RFC 7807 Problem Details gives clients a consistent structure: type, title, status, detail, and optional fields.
Practice: Design a users/orders/payments API and write retry behavior for each endpoint.
Core mental model: gRPC is contract-first RPC over HTTP/2 with Protobuf. It shines for internal service-to-service communication where strong typing and performance matter.
Protobuf: Field numbers are the wire contract. Never reuse removed field numbers. Add fields as optional-compatible changes. Renaming a field is usually safe on the wire, but changing field meaning is dangerous.
Streaming: Unary is request-response. Server streaming sends many responses. Client streaming sends many requests. Bidirectional streaming lets both sides send independently.
Deadlines: Every RPC should have a deadline. Deadlines propagate downstream so a slow dependency does not continue work after the caller has given up.
Interceptors: Use for cross-cutting behavior such as auth, logging, metrics, tracing, validation, and retries.
Status codes: Return typed gRPC status codes instead of random strings. Map domain errors intentionally.
Practice: Build one service with unary and streaming endpoints, then inspect it with grpcurl.
Core mental model: GraphQL gives clients flexible querying, but the server pays the complexity cost.
Resolver tree: Each field can trigger a resolver. Nested queries can explode into many backend/database calls.
N+1 problem: If each parent row triggers a child lookup, one query becomes hundreds. DataLoader batches and caches lookups within a request.
Operational controls: Use persisted queries, depth limits, complexity scoring, timeouts, and auth checks at resolver boundaries.
When to avoid: Do not use GraphQL just for CRUD or public APIs where caching, simplicity, and HTTP semantics are more valuable.
Core mental model: Authentication proves who the user is. Authorization decides what they can do. Session design is about state, revocation, and compromise impact.
Cookies: HttpOnly blocks JavaScript access. Secure requires HTTPS. SameSite reduces CSRF. Cookie-based auth must still handle CSRF correctly.
Sessions: Server stores state; client holds opaque session ID. Easy revocation and rotation. Horizontal scaling needs shared storage such as Redis or sticky sessions, with Redis preferred.
JWT: Signed claims carried by the client. Good for distributed validation, but revocation is hard. JWT payload is not encrypted unless using JWE. Always validate signature, issuer, audience, expiry, and algorithm.
Safe pattern: Short-lived access token, refresh token rotation, Redis/session store for refresh tokens, and JWKS for public-key validation.
Core mental model: OAuth is delegated authorization. It lets a client obtain limited access without handling the user’s password.
Authorization Code + PKCE: Best default for user-facing apps. PKCE prevents intercepted authorization codes from being exchanged by attackers.
Client Credentials: Server-to-server auth where the client itself is the principal.
Device Flow: Useful for CLIs, TVs, and limited-input devices.
Refresh rotation: Each refresh use returns a new refresh token and invalidates the old one. Reuse of an old token signals theft.
Token introspection: Resource server asks authorization server whether a token is active and what it represents. Useful for opaque tokens.
Core mental model: Bearer tokens work like cash. Whoever has the token can use it. DPoP turns the access token into proof-of-possession by binding it to a private key.
How it works: Client generates a key pair. During token issuance, the access token includes a confirmation claim tied to the client public key thumbprint. For every API request, the client signs a DPoP proof JWT containing method, URL, timestamp, and unique jti.
Validation: The resource server verifies the proof signature, checks htm, htu, iat, jti, optional nonce, and confirms the proof key matches the access token cnf claim.
Replay defense: Store jti values temporarily and reject reuse. Use server-provided nonces for stronger replay protection.
Core mental model: Security belongs at every boundary: request parsing, authentication, authorization, outbound calls, database access, logs, and secrets.
Rate limiting: Apply different limits per IP, user, token, route, and organization. Auth endpoints need stricter limits than normal reads.
CORS: CORS is browser protection, not backend auth. Wildcard origins with credentials are dangerous.
SSRF: Validate outbound URLs by scheme, host, resolved IP, redirects, and private network ranges.
SQL injection: Always use parameterized queries. Do not build SQL with string concatenation.
Secrets: Prefer secret managers and short-lived credentials. Avoid leaking tokens in logs, errors, metrics, and crash reports.
grpc-go examples on GitHub