Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

From Backend Change Request to Acceptance Criteria and Implementation Steps

Hello! In the previous lesson, you traced a sync request from its Express route to a simulated downstream adapter. You also found a risky assumption: the service calls recordId.trim() without first checking whether recordId is a string.

A request to “fix bad record IDs” sounds straightforward, but it leaves important decisions open. This lesson shows how to turn that request into testable acceptance criteria—statements of what the service must do—and then into implementation steps grounded in the code you traced. The result is a plan you could review before changing the service.


Start with the change request, not the solution

Imagine this request arrives for the scratch service:

“Sync jobs should handle bad record IDs instead of crashing. Strip stray spaces, and don’t bother the downstream system with bad requests.”

You already have useful evidence from the request trace. A string such as " C42 " is trimmed before it reaches the simulated adapter. But a missing or non-string recordId causes .trim() to throw, and the current error handler turns that failure into HTTP 500. A whitespace-only string becomes empty after trimming, yet the simulated adapter still accepts it.

That evidence describes current behavior, not necessarily the desired contract. The request does not define “bad,” specify a response, or say whether a downstream call must be prevented for every invalid case. Writing code now would mean making those decisions on the requester’s behalf.

Watch the following short sections for the distinction between a broad requirement and the conditions used to verify it.

User Stories and Acceptance Criteria EXAMPLE (Agile Story Tutorial)

Watch “User Stories and Acceptance Criteria EXAMPLE,” from The Business Analysis Doctor, for a concise explanation of how criteria establish scope and make a requirement verifiable.

Watch scope and verification for what acceptance criteria establish. Then watch scenario format for Given, When, and Then, and criteria quality for the emphasis on observable, pass-or-fail outcomes. Keep the format in mind; the video's example is not a specification for our API.

Before writing criteria, separate observations from questions:

What the trace establishes What the request still needs to decide
Strings are trimmed in syncJob. Is a string containing only spaces invalid?
A missing or non-string value can produce HTTP 500. Which values count as invalid, and what should the caller receive?
The adapter runs after trimming. Must invalid input be rejected before the adapter is called?
"fail" triggers a simulated downstream failure. Should that existing failure behavior remain unchanged?

For the rest of this lesson, assume the requester confirms the following contract. These are illustrative decisions, not facts hidden in the original request:

  • recordId must be a string containing at least one non-whitespace character after trimming.
  • Missing values—including a request with no body—and non-string values are invalid.
  • Invalid input receives HTTP 400 with {"error":"INVALID_RECORD_ID"}. The downstream adapter must not be called.
  • Valid strings are trimmed before the adapter receives them. The existing success response remains unchanged.
  • An actual downstream failure retains the existing HTTP 502 response. Handling malformed JSON syntax is outside this particular change.

This clarification makes the work bounded. It also prevents a common mistake: silently treating every client or downstream problem as the same error.


Write criteria at the service boundary

Acceptance criteria describe required behavior; implementation steps describe how you intend to produce it. A criterion such as “add a validation helper” is really a task. It does not say what a caller should observe, and another implementation might satisfy the requirement without that helper.

Given–When–Then is useful here because it separates the relevant starting condition, the single trigger, and the expected result. Read the discussion below before looking at the sync scenarios; it explains why mixing conditions and actions makes criteria harder to test.

Applying BDD acceptance criteria in user stories | Thoughtworks United States

Read Dennis Hee’s Thoughtworks article for its practical guidance on writing scenarios that have clear preconditions and one trigger.

In “Common mistakes anti-patterns in BDD,” read “Not a matter of if, but when,” starting at precondition versus trigger. Continue to “When When When”; focus on one clear trigger. Finally, in “Precondition vs sequence of events,” read relevant context only. Notice how each revision removes detail that does not change the expected outcome.

For this API, the trigger in each scenario is sending one sync request. The value supplied in that request belongs in the Given condition; it is not a second action to perform. The Then statement should make the result distinguishable from a pass or failure.

Here is a compact set of criteria for the confirmed contract:

AC1 — Valid input with stray spaces

Given a sync request for job J17 with JSON body {"recordId":" C42 "} and query parameter tag=manual,
When the request is sent to POST /api/jobs/J17/sync,
Then the adapter receives recordId: "C42" and externalJobId: "J17", and the caller receives HTTP 200 with jobId: "J17", downstreamId: "ds-C42", accepted: true, and traceTag: "manual".

AC2 — Empty after trimming

Given a sync request whose recordId is " ",
When the request is sent to the sync endpoint,
Then the caller receives HTTP 400 with {"error":"INVALID_RECORD_ID"}, and the adapter is not called.

AC3 — Missing or non-string input

Given a sync request with no body, a JSON object lacking recordId, or a recordId that is null, a number, a boolean, an array, or an object,
When that request is sent to the sync endpoint,
Then the caller receives HTTP 400 with {"error":"INVALID_RECORD_ID"}, and the adapter is not called.

AC4 — Existing downstream failure remains distinct

Given a sync request with {"recordId":"fail"},
When the request is sent to the sync endpoint,
Then the simulated adapter is reached and the caller receives HTTP 502 with {"error":"DOWNSTREAM_FAILED"}.

AC3 groups inputs because the confirmed contract gives them the same outcome; verification can run a separate case for each listed value. AC4 is a regression criterion: validation must not turn a valid string into a client error merely because the downstream operation fails.

Notice two kinds of observation in these criteria. The HTTP response checks what the caller sees. Checking the adapter call establishes that bad input did not cross the downstream boundary—something HTTP 400 alone cannot prove. In the scratch service, that boundary is a local simulation, not a real network request.

A criterion need not use Given–When–Then if a short rule is clearer. It does, however, need enough precision that two people can agree whether a particular request passes it. “Handles invalid input gracefully” fails that test; it leaves the input set, response, and downstream behavior unspecified.


Turn the criteria into an implementation plan

The Product Backlog Building Canvas makes a useful distinction: acceptance criteria form a checklist for deciding whether work is complete, while tasks describe the technical work needed to get there.

Product Backlog Building Canvas

Read the “Acceptance Criteria” and “Breaking User Stories into tasks” subsections of Product Backlog Building Canvas, published on Martin Fowler’s site. They show the difference between a condition of satisfaction and a developer task.

Under “Filling out the user story,” start at acceptance criteria; read the ATM examples as checks of behavior, not instructions for building it. Then read breaking into tasks. Stop before “User interface”; that subsection is not needed for this backend change.

Now use the request trace to place work in the right order. The previous controller reads req.body.recordId directly, so a missing body needs attention before the value is passed into the service. The service currently trims before checking the type, so its ordering also needs attention. The error handler recognizes 502 but otherwise responds with 500, so a planned 400 needs an explicit path to the caller.

A reviewable implementation plan could be:

  1. Confirm the contract against the existing route. Record the four criteria above and check that the current POST /api/jobs/:jobId/sync path is the one being changed. Keep malformed JSON syntax and unrelated fields outside this change.
  2. Handle absent input safely. At the HTTP boundary, avoid reading recordId from an absent body in a way that throws. Ensure the value passed onward can be classified as invalid rather than becoming an accidental server error. This supports AC3.
  3. Validate before downstream work. Check that recordId is a string, trim it, and reject an empty result before calling sendToDownstream. Pass the normalized value for valid requests. This supports AC1–AC3.
  4. Return the agreed client error. Make invalid input produce HTTP 400 and the exact error body, without allowing the existing generic error handling to convert it to 500. This supports AC2–AC3.
  5. Verify both response and boundary behavior. Check the valid response and adapter argument, then check every invalid input’s response and that the adapter was not called. Also check the existing simulated downstream-failure response. This covers AC1–AC4.

These steps are intentionally technical: they refer to the controller, service, adapter boundary, and error path identified in the trace. They do not belong inside the acceptance criteria. They also leave room for a design decision—such as where a small validation function should live—without changing the agreed behavior.

For a short project task, save the clarified contract, AC1–AC4, and the five steps in a change-plan.md file beside your scratch service. As you inspect jobController.js, syncJob.js, and the error handler, annotate each step with the exact place you expect to change. Do not treat the notes as proof that the code is fixed; they are a plan to review before implementation. Later, automated tests can verify the adapter calls without relying on a real downstream system.


A final check before coding

A good change plan lets you answer three different questions without mixing them up:

  • What do we know now? The traced service trims strings, can crash on missing or non-string values, and has a distinct simulated 502 path.
  • What has been agreed? The confirmed definition of invalid input, the exact HTTP 400 response, the no-downstream-call rule, and the behaviors to preserve.
  • How will we build and verify it? The implementation steps, each tied to one or more acceptance criteria.

You can now convert a vague backend request into observable scenarios and a code-aware plan without inventing a contract along the way. Next, you will practice implementing a JavaScript function directly from written business rules—a smaller setting in which the same discipline of clarifying inputs and expected outputs still applies.

Can't find a good explanation? Sign up and we'll make it for you

Sign up