I created this blog by pulling together my notes on Context propagation. Before deep diving into it, I just want to share the story behind this blog, because the "how I got here" part is probably the most relatable thing I've written.
2 months ago, I was trying to contribute to an open-source project that is under heavy maintenance, and they usually use the Golang programming language. The issue I picked up was "Adding Context propagation for the REST API" and I've just realized the term "Context propagation" is very familiar to me but I had no idea why and how to implement this thing.
So, I've just done tons of research about Context Propagation for three weeks and unfortunately, I missed the PR and someone solved the problem.
But all this research really worked when I decided to create this blog.
What does "Propagation" mean?
If you are not a native English speaker just like me, then you might wonder what "Propagation" actually means.
Propagation is the act of spreading, multiplying, or transmitting something, such as plants, organisms, ideas, or physical waves, to a broader area or higher number.
To make it clearer, just imagine light, sound, or radio waves traveling through a medium; that journey is propagation.
Context in Go
context.Context is a standard way to manage the lifecycle of an operation in Go.
It acts as a "carrier" that moves through your functions, telling them when they should stop working
(cancellation / timeouts) or providing them with specific metadata they need (request-scoped
values).
Imagine a user sends an HTTP request, but somehow cancels it halfway. Without context, your database query is still running in the background because it doesn't have any idea whether the process is canceled or not. Context lets you propagate that cancellation signal all the way down.
The Golang documentation says: "The Context should be the first parameter, typically named ctx."
func GetUser(ctx context.Context, id string) (*User, error) {
// ctx carries the cancellation signal
}
Also there are multiple ways to create a context and it depends on your needs:
context.Background(): The root context. Use this when you don't have a parent context yet, typically at the entry point likemain()or an HTTP handler.context.WithCancel(ctx): Returns a child context and acancel()function. When you callcancel(), it signals all downstream operations to stop. Perfect for manual control.context.WithTimeout(ctx, duration): Automatically cancels the context after the given duration. Great for database queries or external API calls where you don't want to wait forever.context.WithValue(ctx, key, value): Attaches a key-value pair to the context. Useful for passing request-scoped data (like a user ID or trace ID) without adding extra function parameters.
Context Propagation in a REST API
Now that we know what context is, let's talk about how propagation actually works in a real REST API.
Here's the thing: every incoming HTTP request in Go already carries a context. You don't need to
create one from scratch. It lives right inside the request object and you can grab it with
r.Context().
So the idea is simple: take that context from the HTTP handler and pass it all the way down through your layers.
HTTP Handler
The entry point. Grabs the context from the incoming request via r.Context() and
passes it to the service layer.
Service Layer
Business logic lives here. Receives ctx as its first parameter and passes it
straight down to the repository.
Repository Layer
Where ctx does the real work, passed directly into database calls like
QueryRowContext.
Each layer just receives the context as its first parameter and passes it to the next one. That's it. The context travels like a signal through your entire application stack.
Here's what that looks like in code:
// Handler layer - this is where the journey starts
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // grab the context from the request
id := chi.URLParam(r, "id")
user, err := h.userService.GetUser(ctx, id)
// ...
}
// Service layer - receives ctx and passes it down
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
return s.userRepo.FindByID(ctx, id)
}
// Repository layer - this is where ctx actually does the work
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
row := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
// ...
}
QueryRowContext instead of
QueryRow. That one small change means if the user cancels their HTTP request, the
database query gets cancelled too, no wasted resources running in the background.
Before & After
I think the best way to really see the impact is a side-by-side comparison. This is essentially what the open-source project needed, the functions existed, they just weren't context-aware.
Before (no context):
func (s *UserService) GetUser(id string) (*User, error) {
return s.userRepo.FindByID(id)
}
func (r *UserRepository) FindByID(id string) (*User, error) {
row := r.db.QueryRow("SELECT * FROM users WHERE id = $1", id)
// ...
}
After (with context):
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
return s.userRepo.FindByID(ctx, id)
}
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
row := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
// ...
}
The changes are minimal: ctx context.Context as the first parameter, and
QueryRow
becomes QueryRowContext. But the impact is huge. Your application is now
cancellation-aware from top to bottom.
What I Learned
Honestly, missing that PR hurt a little. But looking back, I think I got the better deal.
If I had just submitted the fix without truly understanding it, I would have moved on. Instead, I spent three weeks digging into how context actually works and now I feel like I genuinely own this knowledge rather than just copying a pattern.
If you're working on a Go project and your service or repository functions don't take a
ctx
parameter, that's worth fixing. It's a small change with a big safety net.
And if you miss a PR because of it, well, at least you'll have a blog post.