Architecture through real-world projects / Article

Trace a request before choosing a service boundary

A small report service makes responsibilities, waiting, and failure visible before you split the code.

Shared case study: A report service

Start with the work, then name the parts

Imagine a dashboard that produces a downloadable report. A user asks for a date range, waits for processing, and downloads a file. The first useful design question is not how many services to create. It is what the system promises after it accepts that request.

For this example, acceptance means the system has durably recorded a job. Completion means a result is available. These are different moments, and a diagram gives the team a place to discuss the gap between them.

  1. The client submits a date range and receives a job identifier.
  2. The API records the request before acknowledging it.
  3. A worker reads the job, generates a file, and records its location.
  4. The client asks the API for the job status and, when ready, the result.

Inspect the live sequence at three checkpoints

Open the shared Report Service model and select Accept, generate and retrieve. Stop first at the Job Store commit. An accepted response sent before that commit would promise work that a crash could erase. The model places the response after durable recording; the implementation must establish the same order.

Stop again where the worker writes to Result Store. The file may exist while the job remains unfinished. A stable output key helps a retry address the same result, but it does not settle concurrent writes or guarantee identical output. Decide whether the input is a captured snapshot, whether generation is deterministic, and which conditional update can finalize this claim. Those are requirements to verify, rather than properties created by the drawing.

Finally, follow retrieval through Report API. A job identifier identifies work; it is not an authorization credential. The API checks the caller's access before reading status or returning report content. Neither a leaked identifier nor a guessed storage key should grant access to another user's report.

Live model · Sequence view

Inspect durable acceptance and authorized retrieval

This sequence follows the same Report Service responsibilities through acceptance, generation and retrieval. The committed job precedes acceptance; the output write and guarded completion remain distinct; result retrieval returns through the authorized API.

Find the job commit before the accepted response, then the gap between writing the output and marking completion. Follow the final access check before report content returns. Open the public diagram ↗

Open this viewer on its own ↗ if the frame is unavailable. The explanation remains readable without the viewer.

Write a boundary decision you can revisit

Begin with API and worker responsibilities inside one owned service. Record a concrete reason before separating their deployments: independent scaling, an expensive runtime dependency, or a different failure budget may justify it. Then record the cost: durable coordination, compatible job formats, and operational visibility across processes. More boxes alone do not improve the design.

As an exercise, close the client after acceptance, interrupt the worker after writing the file, and request the result as a different user. For each case, name the expected visible state and the component that enforces it. A useful system map leaves you with these testable decisions.

Look for decisions that need one owner

The API owns acceptance and authorization. The worker owns generation. The job record owns the shared status. Those responsibilities may live in one codebase and one deployment; drawing separate boxes does not require separate services.

Draw a boundary only when you can explain what crosses it. In this map, a job identifier crosses the client boundary. A durable job crosses the processing boundary. A file location should not quietly become a public download permission.

Live model · Architecture view

Follow the report request

A client submits a report request to an API. The API records a job and returns its identifier. A worker builds the report, stores the result, and updates the job. The client retrieves the result through the API. The job record is the shared agreement between acceptance, processing, and retrieval.

Inspect the Report API, worker and owned stores, then switch to “Accept, generate and retrieve”. Find the durable job commit before acceptance and the access check before returning a result. Open the public diagram ↗
Explore scenarios in this model

These links open the same public model in a selected view. They do not change its source.

Open this viewer on its own ↗ if the frame is unavailable. The explanation remains readable without the viewer.

Exploration is local to your viewer. Reload or Reset view returns to the source layout.

Use a failure to test the map

Suppose a worker crashes after writing the file but before updating the job. A retry can generate the same report again. The diagram alone cannot solve this, but it exposes the question: which operation makes completion observable, and is repeating generation safe?

For a first implementation, you might use a stable output key derived from the job identifier and make the final status update conditional. Document that choice beside the flow. An implementation still needs tests for concurrent retries, storage failures, and authorization; a tidy diagram is not evidence that those cases work.

Apply this to your system

Choose one real request. Write down what acceptance means, what completion means, and who can observe each state. Map only the participants needed to explain those promises. Then ask whether a proposed service boundary makes ownership clearer or adds coordination that your constraints do not need.

The next material in this path uses the same report flow to review a change proposed by an AI agent. Keeping the example constant lets you check the change against an existing agreement.