Логотип Workflow

Article

Updated at:

Stage 12: Transactions in the Service Layer

Stage 12 - Transactions in the Service Layer

Backend code becomes hard to maintain when important behavior is hidden in random controller methods. This article focuses on service-layer @Transactional boundary. The topic looks technical, but the real question is practical: what happens when a real client sends a real request and something is incomplete, too large, forbidden, slow, or inconsistent?

A transaction should wrap a complete business operation, not a random repository call. Creating an order may reserve stock, save the order, and write an audit event; those changes must commit or roll back together.

Stage 12 - Transactions in the Service Layer

The picture to keep in your head

Think about a small online shop. A user opens the frontend, clicks a button, and the browser calls the backend. The backend receives HTTP data, converts it into Java objects, checks rules, touches the database, and returns JSON. If each step has a clear owner, the system is understandable. If every step is mixed in one method, the first production incident becomes painful.

For this topic the sequence is:

  1. Controller calls service.
  2. transaction opens.
  3. repositories work.
  4. exception rolls back or success commits.
PartResponsibility
EntityPersistence object managed by Hibernate.
TransactionBoundary where database changes commit or roll back together.
MapperConverts persistence shape into API shape.
MigrationVersioned database change stored with application code.

Concrete Spring example

@Service
class OrderService {
    @Transactional
    OrderResponse create(CreateOrderRequest request) {
        productService.reserve(request.productId(), request.quantity());
        Order order = orderRepository.save(Order.create(request));
        auditRepository.save(AuditEvent.orderCreated(order.id()));
        return mapper.toResponse(order);
    }
}

The code is intentionally small because the important part is the boundary. The controller should not quietly decide business rules that belong in the service. The service should not depend on servlet objects. The repository should not know which JSON field name the frontend expects. When the boundary is clean, the same behavior can be documented, tested, and changed without touching every layer.

Why it matters

In a demo project, many shortcuts look harmless. Returning all records is fine when there are five rows. Returning an entity is fine until it contains a hidden field. Logging without a request id is fine until several users fail at the same time. Running against a manually created database is fine until staging has a different schema. Production work is mostly about removing these hidden assumptions before they become incidents.

The simplest useful rule is this: make the behavior explicit at the edge, enforce it in the right layer, and keep the public contract stable. If the frontend knows the request format, response format, and error behavior, it can work confidently. If tests exercise the same contract, refactoring becomes safer. If logs and configuration reflect the same design, operations become less dependent on guesswork.

Common mistakes

  • Putting the whole use case into a controller method.
  • Letting persistence details leak into the API contract.
  • Handling the happy path but leaving failure behavior undefined.
  • Using a local shortcut that cannot work in staging or production.

Understanding checklist

  • I can draw the sequence for this topic from request to response.
  • I can explain which layer owns the decision.
  • I can name the production problem this topic prevents.

Self-check questions

  1. What happens if this rule is implemented differently in every endpoint?
  2. Which part of the behavior should be documented for API clients?
  3. Which test would prove the rule works, not only that the happy path works?