Request Lifecycles Through the Backend

LESSON

Backend and API Architecture

011 30 min intermediate

Request Lifecycles Through the Backend

The core idea: A backend request lifecycle is a sequence of trust upgrades, shape changes, decisions, and exits; each stage should leave clearer assumptions for the next one.

Core Insight

When someone says "the endpoint handles the request," it is easy to picture one function: the route runs, some code executes, and a response comes back. That picture is too small for a real backend. The handler is only one stage in a longer path.

Use the same course review example from the previous lessons. A learner sends POST /courses/42/reviews with an authorization token and a JSON body. At the network edge, the system has bytes, headers, and uncertainty. By the time the use case runs, the backend should have a known caller, a parsed route, validated input, trace context, and a command that means something in the application.

That is the useful mental model: a request lifecycle is not a chain of arbitrary framework hooks. It is a sequence of transformations. Each stage narrows uncertainty, adds context, changes shape, or chooses an exit. Parsing turns bytes into an HTTP request. Authentication turns a token into caller context. Validation turns raw input into accepted input. Mapping turns transport data into an application command. The use case turns the command into a business result or business failure.

The misconception to correct is that lifecycle design is just middleware order. Middleware order matters, but the deeper issue is responsibility. Early stages are good for cross-cutting and boundary concerns. Middle stages translate between transport and application meaning. Inner stages make business decisions. Outward stages translate results and failures back into the public API contract.

Once you see the lifecycle this way, backend bugs become easier to locate. Instead of asking "why did the endpoint fail?", you can ask, "Which stage had the wrong assumption about the request?"

The Request Timeline

Here is a healthy review submission path:

1. Network/framework receives bytes.
2. HTTP layer parses method, path, headers, and body.
3. Trace and request-id middleware attaches correlation context.
4. Auth middleware validates the token and creates caller context.
5. Rate or abuse controls may reject obvious excess traffic.
6. Schema validation accepts rating and comment shape.
7. Handler maps request DTO + auth context into a command.
8. Use case checks enrollment, course state, and duplicate review rules.
9. Repository/unit of work persists the review.
10. Mapper turns internal result into public response DTO.
11. Serializer writes status, headers, and response body.

The exact names vary by framework, but the responsibilities recur. The request is not the same object at every step. It accumulates meaning.

bytes
  -> HTTP request
  -> authenticated request
  -> validated DTO
  -> application command
  -> domain result
  -> public response

This helps explain why placement matters. If a handler receives raw JSON and a raw token, too much uncertainty has leaked inward. If middleware tries to decide whether the learner is enrolled in a course, too much domain knowledge has leaked outward.

The trade-off is explicitness versus convenience. A flat handler that parses, authenticates, validates, checks business rules, writes the database, and formats the response is quick to write. A staged lifecycle is easier to reason about when the endpoint grows, fails, or needs to be observed in production.

Worked Lifecycle: Create a Review

The handler should be a boundary adapter, not the owner of every decision. A useful shape looks like this:

def create_review_endpoint(http_request, auth_context):
    dto = parse_and_validate_body(http_request.body)

    command = CreateReviewCommand(
        user_id=auth_context.user_id,
        course_id=http_request.path_params["course_id"],
        rating=dto.rating,
        comment=dto.comment,
    )

    result = create_review(command)
    return review_to_response(result)

This endpoint does not decide whether Alice may review course 42. It adapts HTTP facts into application input. The use case owns the workflow:

def create_review(command):
    course = courses.get(command.course_id)
    enrollment = enrollments.find(command.user_id, command.course_id)

    if not enrollment:
        raise ReviewRejected("not_enrolled")
    if course.reviews_closed:
        raise ReviewRejected("reviews_closed")

    review = reviews.create_once(command)
    return review

Now the lifecycle has clean stages. The endpoint speaks HTTP and DTOs. The use case speaks product meaning. The repository and unit of work speak persistence. The response mapper speaks public API shape.

The same use case could later be called from an admin tool, a queue consumer, or a batch import with a different adapter. That is the benefit of keeping transport details out of the center. The use case needs a command, not an Express request, a Django request, or an ASP.NET controller object.

Failure Exits Are Part of the Lifecycle

A lifecycle is incomplete if it only describes the happy path. Many of the most important backend behaviors happen on exits:

bad JSON                  -> schema boundary exits with 400
missing token             -> auth boundary exits with 401
not enrolled              -> use case exits with domain rejection
duplicate review race     -> persistence boundary exits with conflict
database timeout          -> dependency failure exits through error mapper

Each failure should be translated at the boundary that can preserve its meaning for the next audience. The use case can raise ReviewRejected("not_enrolled"); the HTTP boundary can translate that into the API's chosen status and problem payload. The database adapter can catch a unique constraint violation and turn it into a duplicate-review conflict. Observability can keep the trace, request id, dependency timings, and internal cause.

That is why the next lesson focuses on error semantics. For now, the lifecycle lesson should make one point clear: errors do not live outside the request path. They are named exits from the same path.

Good observability follows the same shape. A useful trace might show:

http.receive
  auth.validate
  request.validate
  review.create
  db.insert_review
  response.map

When latency spikes, the team can ask which stage expanded. When a request is rejected, the team can ask which boundary rejected it and whether that boundary had the right knowledge.

Operational Failure Modes

Issue: Treating the controller as the real start of the request.

Clarification / Fix: Draw the path from socket to response. Include parsing, correlation, authentication, rate controls, validation, error mapping, and serialization. Many bugs live before or after the handler.

Issue: Putting domain decisions into early middleware.

Clarification / Fix: Keep early middleware focused on cross-cutting and boundary concerns. Domain rules that need product state belong in the application or domain workflow.

Issue: Passing framework objects deep into the application.

Clarification / Fix: Translate transport-specific data into an application command at the boundary. Inner code should not depend on web framework request or response classes.

Issue: Observing only the use case while ignoring lifecycle stages around it.

Clarification / Fix: Instrument the path as stages. Auth, validation, dependency calls, and response mapping can each be the source of correctness, latency, or rejection behavior.

Close the lesson and reconstruct the review request lifecycle from memory. For each stage, name the input shape, the output shape, and the assumption it gives the next stage. If you cannot say what changed at a stage, that stage is probably unclear in the design.

Connections

The previous lesson explained why request DTOs, commands, internal models, and response DTOs should not collapse into one object. This lesson places those shapes on the full request timeline.

The next lesson goes deeper on error handling. It will take the failure exits from this lifecycle and ask how their meaning should be classified, translated, and observed.

Request lifecycle thinking also connects back to validation. Validation is not a single location; it is one stage in a larger path that turns raw input into trusted application input.

Resources

Key Takeaways

PREVIOUS DTOs, Entities, and Data Mapping NEXT Backend Error Handling and Failure Semantics