Chapter 15 · Views, Routines, and Database Automation

Choosing Between Database Logic and Application Logic

The database and application are not competing locations for all business logic. Each is strongest at different responsibilities. A maintainable system assigns every rule an explicit owner and keeps the enforcement path testable.

Intermediate145–180 minutesArchitecture decisions + capstoneLast reviewed: August 2026

Learning outcomes

Assign each rule to an explicit and testable owner

01

Compare constraints, views, routines, triggers, and application services by responsibility.

02

Place atomic data invariants close to the data without overloading the database with orchestration.

03

Identify portability, deployment, observability, and team-ownership tradeoffs.

04

Design a hybrid workflow with database enforcement and application coordination.

05

Create a governance checklist for database-resident logic.

Start with the property, not the technology

Ask what must remain true, who owns the rule, which resources it touches, and what must happen atomically. Only then choose the implementation location.

Business requirement
Atomicity boundary
Data and external resources
Ownership + portability
Observability + deployment
Mechanism decision

The goal is a clear enforcement path, not maximizing either database logic or application logic.

Decision matrix

Requirement characteristicDatabase is usually strongerApplication is usually stronger
Must hold for every writerConstraints, foreign keys, unique indexesOnly if all writes are guaranteed through one service
Atomic with row changesTransaction, trigger, stored routineApplication transaction when using one database connection
External API, email, or queueOutbox row onlyActual network call, retry, timeout, circuit breaker
Reusable relational projectionView or materialized viewAPI response composition across many systems
Complex workflow and user interactionPersist state and invariantsOrchestrate steps, approvals, and compensation
Cross-database portabilityPortable constraints and SQL subsetDomain code behind repository interfaces
High data localitySet-based SQL close to dataRow-by-row logic is usually worse
Rich telemetry and debuggingDatabase logs and catalog inspectionTracing, structured logs, feature flags, and debuggers

A practical placement hierarchy

  1. Use declarative schema features first: types, NOT NULL, CHECK, keys, foreign keys, and exclusion/unique rules.
  2. Use views for stable read contracts: centralize relational shape without implying stored results.
  3. Use materialization for measured read bottlenecks: add a freshness and refresh contract.
  4. Use routines for cohesive data-local operations: type the interface and define side effects.
  5. Use triggers only for narrow automatic behavior: audit, transition guards, and view adaptation.
  6. Use application services for orchestration: external resources, user workflows, policy variation, and broad observability.

Hybrid capstone: cancelling an order

The application authenticates the caller, authorizes the action, chooses a request identifier, and coordinates external notifications. The database atomically enforces the state transition, records the command receipt, updates the order, and writes an outbox event.

API validates request
BEGIN transaction
Insert idempotency receipt
Conditional order UPDATE
Insert audit + outbox
COMMIT
Worker publishes event

Only the durable outbox row is part of the database transaction; the network call happens after commit.

Database foundation

sqlite · durable workflow tables
DROP TABLE IF EXISTS command_receipt;DROP TABLE IF EXISTS outbox_event;DROP TABLE IF EXISTS cancellation_audit;CREATE TABLE command_receipt (    request_id    TEXT PRIMARY KEY,    command_name  TEXT NOT NULL,    aggregate_id  INTEGER NOT NULL,    completed_at  TEXT NOT NULL) STRICT;CREATE TABLE cancellation_audit (    audit_id      INTEGER PRIMARY KEY,    order_id      INTEGER NOT NULL,    previous_status TEXT NOT NULL,    request_id    TEXT NOT NULL UNIQUE,    actor_id      TEXT NOT NULL,    reason        TEXT NOT NULL,    recorded_at   TEXT NOT NULL) STRICT;CREATE TABLE outbox_event (    event_id       INTEGER PRIMARY KEY,    event_type     TEXT NOT NULL,    aggregate_id   INTEGER NOT NULL,    request_id     TEXT NOT NULL UNIQUE,    payload_json   TEXT NOT NULL CHECK (json_valid(payload_json)),    created_at     TEXT NOT NULL,    published_at   TEXT) STRICT;

Explicit atomic command

This transaction is intentionally explicit rather than hidden in a trigger. It needs actor, request, and reason context from the application, and the caller must distinguish “duplicate request” from “invalid state.”

sqlite · cancellation transaction
BEGIN IMMEDIATE;INSERT INTO command_receipt    (request_id, command_name, aggregate_id, completed_at)VALUES    (:request_id, 'cancel_order', :order_id, datetime('now'))ON CONFLICT (request_id) DO NOTHING;-- Continue only when the receipt was inserted.UPDATE sales_orderSET status = 'cancelled',    updated_at = datetime('now'),    version = version + 1WHERE order_id = :order_id  AND status IN ('draft', 'submitted')  AND changes() = 1;INSERT INTO cancellation_audit    (order_id, previous_status, request_id, actor_id, reason, recorded_at)SELECT    :order_id, :previous_status, :request_id, :actor_id, :reason, datetime('now')WHERE changes() = 1;INSERT INTO outbox_event    (event_type, aggregate_id, request_id, payload_json, created_at)SELECT    'order.cancelled', :order_id, :request_id,    json_object('order_id', :order_id, 'reason', :reason),    datetime('now')WHERE changes() = 1;COMMIT;

In production, check affected-row counts after each step and roll back on an invalid transition. The compact SQLite example demonstrates placement, but application code should not rely on a long chain of changes() calls without explicit verification.

Application responsibilities

Before transactionInside transactionAfter commit
Authenticate actorSupply actor/request contextReturn stable command result
Authorize cancellation policyExecute conditional state transitionPublish outbox events asynchronously
Validate request shapeWrite receipt, audit, and outbox atomicallyRetry publication with backoff
Select idempotency keyDetect duplicate requestRecord metrics and traces
Avoid external callsNever wait on network servicesNotify users and downstream systems

Anti-patterns

DB

Everything in stored code

Workflow, external calls, and product policy become hard to deploy, observe, and port.

APP

All rules in one service

Other tools, migrations, or direct writers can violate invariants; concurrency bugs reappear.

Trigger surprise

A simple update performs undocumented cascading work and changes latency unpredictably.

Duplicated rule

The same rule exists in several services and a trigger with slightly different semantics.

🌐

Network in transaction

Locks remain held while an unreliable dependency is contacted.

?

No owner

Database objects persist without tests, monitoring, migration policy, or responsible team.

Govern database logic like code

ControlRequired practice
Source controlEvery view, routine, trigger, and grant is represented in migrations.
ReviewDatabase and application owners review semantics, performance, and rollout.
TestingContract, failure, concurrency, privilege, and rollback tests run automatically.
VersioningBreaking interfaces use expand-and-contract or versioned names.
ObservabilityTrack routine duration/errors, trigger amplification, refresh lag, and outbox backlog.
InventoryCatalog object owner, consumers, purpose, dependencies, and retirement date.
RecoveryDocument rollback or forward-fix strategy before production deployment.

Architecture exercise

Place each requirement in the most appropriate mechanism, then justify the atomicity boundary:

  1. Quantity must be positive.
  2. Every order must reference an existing customer.
  3. A dashboard needs daily totals that may be ten minutes old.
  4. Changing an order status must create an audit row in the same commit.
  5. A cancellation sends email and informs a warehouse API.
  6. Five applications need the same customer-order read shape.

Review the placement

  1. Where should the two row-integrity requirements live?
  2. What mechanism fits the bounded-staleness dashboard?
  3. How can audit remain atomic without sending email inside the transaction?
  4. What should expose the shared read shape?
Review the answers

Use CHECK and foreign-key constraints. Use a materialized summary with a ten-minute freshness SLO. Write audit and an outbox row atomically; publish email/API events after commit. Use a documented view, or a materialized view if measurement proves necessary.

Chapter summary

  • Place universal data invariants in declarative database constraints.
  • Use views and materialized views as explicit read contracts with performance and freshness semantics.
  • Use routines for cohesive typed operations, and triggers only for narrow automatic behavior.
  • Keep external orchestration, retries, user workflow, and rich telemetry in application services.
  • Govern database-resident logic with ownership, tests, versioning, observability, and recovery plans.

Chapter 16 continues with security, reliability, and governance: roles and least privilege, SQL injection prevention, backup and recovery objectives, encryption and auditing, retention, privacy, lineage, and data quality.

References

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.