A Decision Framework (and When to Stop Arguing)
This is the finale of the “Where Does Business Logic Live?” series. Over five posts we have looked at what business logic even is, the case for the database, the case for the application, the performance and operations reality, and the security, myths, and hybrids. Now let’s make it actionable.
This post does three things: it gives you a checklist you can run against a specific piece of logic to decide where it belongs, it looks at how organizations operating at genuine scale have made these calls, and it ends with the honest question underneath the whole series – why do these arguments turn ideological, and how do you keep yours from doing the same?

The Decision Framework
There is no formula that places logic for you. But there is a small set of questions that, asked in order, will get you to a defensible answer for almost any piece of logic. Run them against the specific rule in front of you, not against “business logic” as an abstraction – the abstraction is what makes this debate unwinnable.
Question 1: Is this an invariant the data must never violate, regardless of who writes it?
If the answer is yes – a foreign key must point at a real row, a balance can never go negative, an email must be unique – it belongs in the database, as a constraint or, where a constraint cannot express it, in the procedure that owns that write. The test is “if a second application, a data fix, or an ad-hoc script wrote this wrong, would it be a disaster?” If so, the database is the only tier that sees every write and can guarantee the invariant. This is the least negotiable rule in the framework.
Question 2: Does this operate over a large set of existing rows?
If the work is “recalculate every account’s balance,” “expire all sessions older than 30 days,” or “apply this price change to a million products,” it belongs close to the data, in a set-based statement or a stored procedure. Pulling a large set into the application to loop over it and write back row by row is the single most common performance mistake in this whole space. Data gravity wins: move the logic to the data, not the data to the logic.
Question 3: Is this a complex domain rule with lots of branching, or does it change frequently?
If the logic is “how we calculate a promotional discount given the customer tier, the cart contents, the season, and three active campaigns,” it usually belongs in an application service, where a general-purpose language, a real domain model, and a fast test-and-deploy loop serve it better than T-SQL. Logic that changes every sprint also leans application-side, because the application’s deployment cadence is built for frequent change. (With the standing reminder from Part 3: the database’s code can and should be under the same CI/CD discipline – this is about which language and cadence fit the rule, not about one tier being “real” engineering and the other not.)
Question 4: Does this orchestrate external systems or long-running workflow?
If the logic calls a payment provider, schedules a shipment, sends an email, or coordinates several services over seconds or minutes, it belongs in an application service. The database should not be making outbound calls or holding transactions open across them. Workflow, sagas, idempotency, and compensation are application-tier concerns – Stripe’s published work on idempotency keys is the canonical reference for doing this well.[1]
Question 5: Is this about who is asking and whether they are allowed?
Authentication, authorization, rate limiting, token validation, and input validation at the edge belong in the application and the API gateway. The database enforces what can be done to the data once a request gets through (including row-level security for per-row isolation), but identity-aware, request-level security lives above it.
Question 6: Is this only for user experience?
Instant feedback – “this field is required,” “passwords must match,” “you have unsaved changes” – belongs in the client/SPA because it is closest to the user. The iron rule: client-side validation is never the authoritative check. Every rule enforced in the browser for responsiveness must be enforced again at a tier the user cannot bypass.
The framework on one page
| If the logic is… | Put it in… |
|---|---|
| A data invariant true for every writer | Database (constraint or owning procedure) |
| A large set-based operation | Database (set-based statement / procedure) |
| A complex or frequently-changing domain rule | Application service |
| External orchestration or long-running workflow | Application service |
| Identity, authorization, rate limiting | Application / API gateway |
| Per-row access isolation | Database (row-level security) |
| Authoritative validation | Database and/or service (never only the client) |
| Fast user-experience feedback | Client / SPA (plus an authoritative re-check) |
Notice that several rows say “database” and several say “application,” and that is the point. A real system runs all of these rows at once. The framework does not pick a side; it places each rule where its specific properties are best served.
What Scale Actually Teaches
The most useful evidence in this debate comes from organizations that have operated successfully at scale with very different boundaries – because they show that more than one answer works, and they show why each answer worked for that team.
Stack Overflow ran one of the busiest sites on the internet on a small number of SQL Server instances, with a hand-written-SQL, thin-ORM approach (Dapper, which they built) precisely so they could see and control every query.[2] Their lesson is not “avoid ORMs”; it is “at our scale, query predictability was worth writing SQL by hand, so we built a tool that let us.”
GitHub and Shopify run enormous applications on Rails and ActiveRecord – heavy application-tier logic over MySQL – and both have invested deeply in database engineering to make that work: partitioning, sharding, read replicas, and relentless attention to the SQL their ORM generates.[3] Their lesson is that application-tier logic and serious database expertise are not alternatives; the application-heavy approach requires the database expertise to survive scale.
Stripe and Uber built service-oriented systems where data ownership is split across services, and they have written extensively about the consistency, transaction, and data-ownership problems that creates – problems they solved with careful engineering, not by pretending the database stopped mattering.[1]
Read side by side, these organizations disagree far less than the internet debates suggest. None of them thinks indexing, execution plans, isolation levels, or data integrity stopped mattering. None of them thinks the database is “just a bucket.” What differs is where each drew the boundary between database and application – and each drew it based on their workload, their team, and their constraints, then measured the results. The thread that connects every one of these successful systems is not a particular boundary; it is that they made the boundary a deliberate, measured decision.
Why These Debates Turn Ideological
So if the evidence is this consistent, why is the argument so heated?
Because it stops being about the data and starts being about identity and ownership. “Business logic belongs in the application” can mean “I am an application developer and I want to own this.” “Business logic belongs in the database” can mean “I am a DBA and I do not trust the application team.” Once the question is whose code it is rather than where the rule is best served, no amount of benchmarking settles it, because benchmarks do not answer questions about identity.
You can see this in the long-running discussion threads on these exact topics – the highly-voted questions about how much logic the database should hold, or ORM versus stored procedures, are highly-voted precisely because the answer is “it depends” and people keep arriving hoping for a verdict that will end the argument at their office.[4] The verdict never comes, because the honest answer is a framework, not a side.
The way out is the boring, professional one: argue about the specific rule, not the category. Measure instead of asserting. Ask “where is this logic best served?” and let the six questions above answer it. When someone says “logic never belongs in the database” or “the application layer is just plumbing,” they have left the evidence behind and started defending a team. The most senior engineers I have worked with, on both sides of the wire, are the ones least interested in winning this argument and most interested in placing each rule correctly and moving on.
Where We Landed
The whole series reduces to a few sentences. Data integrity and large set-based work belong close to the data, because the database is the only tier that sees every write and the data has gravity. Complex, fast-changing domain logic and external orchestration belong in application services, because a general-purpose language and a fast deployment loop serve them better. Identity and edge concerns belong at the application and gateway; user-experience feedback belongs at the client, but never as the authoritative check. And the database’s code deserves the same CI/CD rigour as the application’s – version control, automated validation, gated deployment – because professional engineering is not a property of a tier.
Architecture decisions should be driven by business requirements, operational realities, team skills, scalability needs, compliance requirements, and long-term maintainability – not by which camp you joined. Both approaches have produced systems that run the world. The teams that succeed with either are the ones that decided on purpose and measured the result.
Where This Series Goes Next
This series mapped the boundary. The next run of posts will go deeper into practical software design for data systems – the day-to-day craft underneath these decisions. Planned topics:
- Designing the database contract: views, procedures, and the public surface of your database – treating the database as an API with a deliberately versioned interface.
- Idempotency keys and exactly-once semantics from the database’s side – what the data layer must do so application retries are safe.
- Putting your database under CI/CD: SSDT, Flyway, and migration discipline – the concrete tooling for the directive that ran through this whole series.
- Temporal tables and change data capture for audit and history – choosing the right built-in mechanism for “what changed, when, and by whom.”
- Multi-tenant data isolation: row-level security, separate schemas, or separate databases – the tradeoffs of each isolation model at scale.
- Reading ORM-generated SQL: a DBA’s field guide – how to capture, read, and fix what the abstraction emits, without removing the abstraction.
- The transactional outbox pattern in SQL Server – reliable messaging without distributed transactions.
- Testing database code: unit tests, integration tests, and tSQLt – bringing the testability advantage of the application tier to the database.
If one of those is the post you most want next, tell me – I write the series partly from what readers ask about. You can find me on Bluesky and LinkedIn. Thanks for reading the whole series.
References and Footnotes
- “Designing robust and predictable APIs with idempotency,” Stripe – idempotency keys, retries, and consistency at a service boundary. stripe.com. Uber’s engineering blog covers related service-oriented data-ownership and distributed-transaction topics: uber.com/blog/engineering. ↩ back
- Nick Craver, “Stack Overflow: The Architecture – 2016 Edition” – hand-written SQL and a thin micro-ORM for query predictability at scale. nickcraver.com. Dapper, built at Stack Overflow: github.com/DapperLib/Dapper. ↩ back
- “Partitioning GitHub’s relational databases to handle scale,” GitHub Engineering – database engineering investment behind an application-heavy architecture. github.blog. Shopify’s scaling of one of the largest Rails codebases: shopify.engineering. ↩ back
- “Entity Framework VS LINQ to SQL VS ADO.NET with stored procedures?” – a heavily-voted, much-revisited question whose accepted answer is, in effect, “it depends on your priorities.” stackoverflow.com. See also “How much business logic should the database implement?” softwareengineering.stackexchange.com. ↩ back
That’s the series – Part 1, Part 2, Part 3, Part 4, Part 5, and this finale. Start at the series index.