Decision in briefThe server owns a state change that affects other records.

A status field looks simple until another part of the system depends on it.

In Azaeron, moving an invoice to paid is guarded by a payment summary. The lifecycle service rejects a paid transition while an outstanding balance exists. It also records a journal event and an audit entry after changing state. The implementation treats a transition as a business operation, not a display update.

The SHAPES publishing service makes a similar choice for a different domain. Content moves through draft, review, approved, scheduled, published and archival states. The service validates each move and creates a revision record. Routes call that service so one workflow owns the rules.

Why it matters

When UI controls are the only guard, another API caller can skip them. When multiple routes each implement a slice of the same rules, they drift. A domain service gives the operation one explicit place to reject invalid state, record context, and keep related effects together.

A service boundary alone does not make every side effect atomic. Transaction scope and retries still matter, especially when payment and publishing flows touch more than one record.

See the projects behind this noteAzaeron case studySHAPES case study