# Organize service code by responsibility

Canonical page: https://yishto.com/en/learn/service-data/service-layers-and-frameworks/

Author: [Yishto](https://yishto.com/en/about/)

Organize a reservation service through HTTP adaptation, use cases, rules, and repositories, then verify transactions, cancellation, and dependency failures separately from framework integration.

Published: 2026-11-10
Updated: 2026-11-10

A document-reservation service parses input, checks identity and capacity, writes data, and sends notifications. Putting everything in an HTTP callback makes rules difficult to test independently and obscures rollback boundaries. Assign responsibilities so use cases coordinate external operations while rules have a clear interface.

The architecture case is reservation, while the small example uses synthetic integer prices to show separation. It starts no Express, Spring, or Go server and connects to no database.

## Derive boundaries from the use case

An HTTP adapter parses input, establishes trusted identity, bounds bodies, and maps responses. An application service coordinates a use case and transactions. Domain rules express capacity, prices, and states. Repositories access storage. These are dependency choices rather than a mandatory four-folder structure.

A reservation can accept user, document, and request IDs, obtain data, check capacity, and save the result. Rules need not know headers or connection strings. Avoid duplicating stock SQL across routes.

List inputs, outputs, failures, and effects before splitting modules. A direct function can suffice for a simple rule. Introduce abstractions for actual distinct responsibilities rather than imagined future complexity.

## Make rule inputs explicit

Integer minor units avoid some floating-point representation problems, but currency, bounds, and rounding still need definitions. Quantities need maxima as well as nonnegativity. In Python, bool subclasses int, so decide whether strict integer checks must reject booleans.

Rules return business results or explicit errors rather than HTTP responses. Web endpoints, jobs, and batch operations can then share them. Pure computation should not decide recovery by sending network requests itself.

Pure functions expose invariants but do not prove concurrency or authorization. Database conditions and constraints still protect capacity across writers. Application rules express intent while storage enforces shared invariants.

## Coordinate persistence and external work

A use case can read data, validate rules, commit changes, and arrange notifications. Keep necessary transactional reads and writes together without holding locks through long model calls or uploads. Database commit and message publication need separate coordination, such as outbox or compensation.

Repository operations should fit the use case. A universal CRUD wrapper can hide conditional-update outcomes. Reservation needs success, duplicate, conflict, and version information rather than an indistinguishable empty result.

Propagate timeout and cancellation conditions to dependencies that support them. If completion is uncertain, preserve recoverable state. Ending an interface wait does not automatically undo database changes.

## Framework adapters follow different conventions

Express composes routes and middleware, whose order affects parsing, authentication, and errors. Follow the documentation for the actual version. Spring commonly separates controllers, services, and repositories, but annotations do not guarantee correct transactions or rules.

Go net/http handlers can use request context to propagate cancellation and deadlines. Connections and dependency calls still need suitable timeouts. Async I/O, CPU parallelism, and synchronization are different concerns across languages.

Choose from team expertise, dependencies, deployment, and maintenance needs. A modular monolith can establish boundaries before microservices introduce networking, versioning, and distributed consistency requirements.

## Check a rule and its adapter offline

Total rejects negative values and values that are not strict integers. Handle validates shape and maps rule failure to 400 or success to 200 and a result. Prices 20 and 30 are synthetic minor units, not actual product prices.

```python
def total(items):
    if any(type(x) is not int or x < 0 for x in items):
        raise ValueError("invalid price")
    return sum(items)
def handle(payload):
    try:
        if not isinstance(payload, dict) or not isinstance(payload.get("prices"), list):
            return {"status": 400}
        return {"status": 200, "total": total(payload["prices"])}
    except ValueError:
        return {"status": 400}
assert handle({"prices": [20, 30]}) == {"status": 200, "total": 50}
assert handle({"prices": [-1]})["status"] == 400
print(handle({"prices": [20, 30]}))
```

The output is `{'status': 200, 'total': 50}`. The negative-input assertion verifies rejection. The model omits complete amount bounds, currency, authentication, and persistence and sends no real status code.

Making total access a global request would reintroduce an entry-point dependency. Extend pure checks with empty input, booleans, strings, and large values, then test adapter shape and mapping. Verify actual HTTP headers, serialization, and startup separately.

## Test boundaries rather than framework internals

Rule checks cover invariants. Use-case checks cover dependency failures, transactions, and duplicates. HTTP integration checks cover routes, authentication, parsing, and responses. Neither launching a server for every rule nor testing only functions provides the right evidence for every boundary.

Dependency substitutes should model exhausted capacity, save conflicts, and notification failure. Assert effect counts, resulting state, and safe errors rather than duplicate the framework's internal call sequence.

Shared module state needs concurrency and multi-instance analysis. A process-local lock does not replace cross-instance database rules. Log use cases and safe correlation IDs without collapsing all failures into one unexplained result.

Circular dependencies often reveal unclear responsibility. Pass a required field explicitly instead of making a rule import the HTTP layer. Shared helpers should not become global state that any use case may mutate.

## Exercise: implement a reservation use case

Define inputs, states, capacity, and repeated-request semantics. Specify pure rules, repository operations, application coordination, and HTTP adaptation with dependency directions. Use synthetic dependencies for exhausted capacity, duplicates, failed writes, and failed notifications.

Rules must run independently of the framework. Invalid input must not write; successful reservation must decrement once; a failed save must not publish success; notification failures must be recoverable; and deadlines must reach cancellable dependencies. Record actual framework integration results separately. Layer diagrams and pure assertions do not prove a usable deployment.

## Sources & further reading

- [Express routing](https://expressjs.com/en/guide/routing.html)
- [Building a RESTful web service](https://spring.io/guides/gs/rest-service)
- [Go HTTP package](https://pkg.go.dev/net/http)

Sources checked on 2026-11-10. Examples are teaching data, not production measurements.

## Related guides

- [Support code evolution with tests and decision records](https://yishto.com/en/learn/data-practice/testing-refactoring-and-decisions/)
- [Project: a product-description review workspace](https://yishto.com/en/learn/project-labs/lab-product-description-review/)
