⚙️ B4 · Service Layer: where business rules live

B4 · Service Layer: where business: The service layer is the API's **decision-making manager**: the repository does raw storage, the controller receives the request — but **busin

The service layer is the API's **decision-making manager**: the repository does raw storage, the controller receives the request — but **business rules** like "can a CLOSED bug be reopened?", "can two bugs be created with the same title?" live here. But why not put these rules in the controller or the repository — wouldn't they work there too? Because if a rule enters the controller, every new entry point (REST, message queue, scheduled job) must re-implement it and one silently diverges; if it enters the repository, storage tech and business logic get glued together. The service is the **single source of truth** for rules. In Java the equivalent is business logic gathered in a `@Service` class and guarded with `@Transactional`; the controller just says "do this" without knowing how. In QA the service layer is where the most valuable defects live: not a field validation but a **business-rule violation** (reopening a closed bug, duplicate records) is often invisible from the UI yet silently corrupts data — the tester must test these rules scenario-based (state transitions).

Business Rules in the Service

**🐞 Defect Birth — if the "already CLOSED" rule is forgotten** **Code:** the `if (bug.getStatus() == Status.CLOSED) throw ...` check inside `closeBug` is MISSING; the method directly sets status to CLOSED and saves. **What happens:** sending `PATCH /api/v1/bugs/42/status {"status":"CLOSED"}` again to an already-closed bug returns 200 instead of 409/400. It looks fine, but if closing increments a counter, sends a notification, or stops an SLA timer, those side effects fire a SECOND time — duplicate notifications, wrong metrics. **Why sneaky:** in a single request nothing shows; the record was already CLOSED and is CLOSED again. The side effects (notification, metric) repeat silently and are noticed only when reports become inconsistent. **Where the tester catches it:** in a state-transition test — close a bug, then send the SAME close again; expect an error (409 Conflict) on the second request. Without the business rule the second close passes silently.

🎬 Closing Twice: How an Invisible Side Effect Fires Twice

A bug is closed: PATCH .../status {CLOSED}. Closing does not just change status — it sends a notification and stops the SLA timer.

The same close request arrives again (double click, retry, race). The record is already CLOSED. Now the business rule must step in.

If the rule EXISTS: the "already CLOSED" check rejects the second request with 409 Conflict. The side effect runs once. The system stays consistent.

If the rule is MISSING: the second close passes silently (200), the notification goes a SECOND time, the metric increments twice — a silent but spreading corruption.

The lesson — Business rules live in the service layer and guard state transitions. The tester tests them scenario-based: "close, close again, expect 409".

Why the Rule Belongs in the Service, Not the Controller

Controller is just a door…

The controller receives the request and delegates to the service; it knows no rule. Different entry points use the same service.

Service is the single source…

With the rule in one place in the service, REST, queue, scheduled job — all apply the same rule.