Where Does Business Logic Live? A DBA’s Field Guide
Ask three experienced engineers where business logic belongs and you will get four answers, two raised voices, and at least one person quoting a blog post from 2008. I have sat on both sides of this argument. I have written 3,000-line stored procedures that ran a business, and I have supported applications where the database was treated as a passive bucket of rows and every rule lived in C# or Java. Both shipped. Both made money. Both had production incidents at 2 AM that traced straight back to where someone decided the logic should live.
This is the first post in a series about one of the longest-running architecture debates in our field: should business logic live inside SQL Server, in the application layers above it, or somewhere in between? I am writing it as a DBA who has had to operate, troubleshoot, and tune both kinds of systems – not as an advocate for one camp.
In this opening post I want to do the unglamorous groundwork: where this debate came from, why it refuses to die, and – most importantly – what we actually mean when we say “business logic,” because half of every argument about this topic is two people using the same words for different things.

A Short History of the Database in the Middle
For a long stretch of commercial computing, the database was not a component of the application. It was the application’s center of gravity.
In the client-server era of the 1990s, the standard pattern was a thick desktop client – PowerBuilder, Visual Basic, Delphi, Access – talking directly to a relational database over the network. The client rendered forms and the database did the work. Stored procedures were not a stylistic choice; they were the application tier. Validation, pricing, posting to the ledger, inventory adjustments: all of it lived in T-SQL or PL/SQL because that was the one place all the clients had in common. If you wanted a rule enforced consistently across the VB client, the Excel macro, and the nightly batch job, you put it in a stored procedure and granted execute.
This was not naive. It was a rational response to the constraints of the time. The network was the bottleneck, the database was the single shared resource, and centralizing logic next to the data was the cheapest way to keep behavior consistent. A great deal of software that still runs your bank, your insurance policy, and your municipal land registry was built exactly this way, and a meaningful fraction of it still works.
Then the constraints changed.
The Tilt Toward the Application Tier
Several forces pulled logic up and out of the database over the following two decades.
The web happened. A browser cannot hold a database connection and run a stored procedure directly – and you would not want it to. Something had to sit between the browser and the data, and that something was an application server. Once you have a middle tier, it is a short walk to start putting rules there.
Object-oriented programming and the domain model matured. Martin Fowler’s Domain Model and Transaction Script patterns gave teams a vocabulary for expressing business behavior in application code, and the broader industry increasingly preferred to model the business in the language the developers lived in all day.
Object-relational mappers – Hibernate, Entity Framework, ActiveRecord – made it possible to treat the database as a persistence detail. The promise was that you could write your domain in objects and let the ORM worry about SQL. That promise was real, and it was also the source of an enormous amount of the friction this series is about.[1]
Service-oriented architecture, and later microservices, formalized the idea that a business capability is a service with an API, not a set of tables. When each service owns its own data and exposes behavior through an interface, the natural home for “what the business does” is the service, not a shared database that multiple services would have to couple themselves to.
And the cloud changed the economics. Application tiers scale horizontally and cheaply – add another stateless container behind a load balancer. The relational database is the part you scale vertically, carefully, and expensively. When compute in the app tier is the elastic resource and the database is the precious one, there is real pressure to keep CPU-heavy work off the database.
By the 2010s, the default posture in a lot of greenfield development had inverted. Where the 1990s default was “logic in the database unless you have a reason,” the new default in many shops became “logic in the application, and please keep it out of the database.”
Why This Debate Won’t Die
If the industry had genuinely settled this, I would not be writing a six-part series about it. The debate persists for a few durable reasons.
The first is that both approaches demonstrably work at enormous scale. Stack Overflow famously served a staggering amount of traffic on SQL Server[2] using hand-written queries through Dapper, a deliberately thin micro-ORM,[3] precisely because they wanted to see and control the SQL. Shopify[4] and GitHub[5] run two of the largest Ruby on Rails / ActiveRecord deployments on the planet with a full ORM and a great deal of application-layer logic. Stripe builds correctness-critical financial systems where many concerns a 1995 architect would have handled with stored procedures are instead handled through application architecture, idempotency keys,[6] and rigorous engineering discipline. When you can point to wildly successful systems on both ends of the spectrum, neither side can claim the other is simply wrong.
The second is that the two camps often are not measuring the same thing. A DBA who has spent the week untangling an execution plan generated by an ORM is optimizing for query predictability and operational stability. An application architect who has spent the week wiring a new payment provider into a workflow is optimizing for change velocity and testability. Both are right about their own pain. Neither is automatically right about the other’s.
The third reason is the uncomfortable one: this debate is frequently about identity and ownership rather than evidence. “Logic belongs in the database” and “logic belongs in the application” are sometimes technical positions, and sometimes they are really “my team should own this” wearing a technical costume. Where the logic lives determines who gets paged, who controls the deployment, and whose budget pays for the next server. Those are organizational questions dressed up as architectural ones, and they are a big part of why the conversation gets heated out of proportion to the actual technical stakes.
Here is something I find genuinely clarifying, and we will come back to it in the final post. If you read the public engineering writing from Stack Overflow, GitHub, Shopify, Stripe, Uber, Brent Ozar’s team, and Erik Darling side by side,[7] you will find surprisingly little disagreement about the technical realities. Everyone agrees that execution plans matter, that indexing matters, that transaction isolation matters, that data integrity matters, and that an ORM does not free you from understanding the database underneath it. The disagreement is almost never about whether those things matter. It is about where to draw the line between database responsibilities and application responsibilities. That is a much narrower – and much more answerable – question than the shouting suggests.
The fights that look like “database versus application” are usually fights about where one specific responsibility should sit. Reframing the argument from “which side wins” to “where does this particular rule belong” defuses most of it.
So What Is “Business Logic,” Exactly?
The single biggest source of confusion in this whole debate is that “business logic” is a bucket term. People throw five genuinely different kinds of logic into it and then argue about the bucket as if it were one thing. Before we can have a useful conversation about where logic should live, we have to separate the contents of the bucket.
Here is the taxonomy I use. It is not the only valid breakdown, but it maps cleanly onto the placement decisions we will make in the rest of the series.
| Logic type | What it is | Example |
|---|---|---|
| Data integrity | Rules that keep the data internally consistent and well-formed, independent of any one business process | An order line cannot reference a product that does not exist; a quantity cannot be negative; an email column must be unique |
| Validation | Checking that a specific input is acceptable before acting on it | “This discount code is still valid,” “this date is not in the past,” “this field is required on this form” |
| Business rules | Domain decisions that encode how the business actually behaves | “Gold-tier customers get free shipping over $50,” “an invoice over $10,000 requires manager approval,” “tax is calculated using the destination province’s rate” |
| Workflow orchestration | Coordinating a multi-step process, often across systems, over time | “On checkout: reserve inventory, charge the card, send the confirmation email, schedule the shipment, and if the charge fails, release the reservation” |
| Presentation logic | Rules about how things are shown or formatted for a user | Sort order on a screen, which fields are visible to which role in the UI, currency formatting, “show a warning banner if the cart is empty” |
These are not academic distinctions. They behave completely differently when you ask “where should this live?”
Data integrity wants to be close to the data
Data integrity is the category with the strongest gravitational pull toward the database, and for a reason that is hard to argue with: the database is the one component that sees every write. Your web app might enforce that a quantity is non-negative. So might your mobile app. But the nightly batch job, the data-fix script someone runs in an incident, the second service that was added last year, and the DBA doing an emergency UPDATE at 3 AM – those do not go through your application’s validation layer. A CHECK constraint, a foreign key, or a unique index does not care which path the write came from. It is the last line of defense, and it is the only line that holds when an unexpected writer shows up.
This is why even teams that are aggressively “logic in the application” usually keep integrity rules – primary keys, foreign keys, unique constraints, check constraints, not-null – in the database. It is the least controversial placement decision in the entire debate, and we will treat it as close to settled.
Presentation logic wants to be close to the user
At the opposite end, presentation logic has an equally strong pull toward the client and the application tier. How you sort a grid, which columns a manager sees versus a clerk, whether to show a red banner – none of that has any business being expressed in a stored procedure. The database should not know or care that there is a screen. Putting presentation concerns in the data layer couples your storage to your UI and guarantees pain the first time a second UI shows up.
Everything in the middle is where the war is fought
Validation, business rules, and workflow orchestration are the contested territory. These are the categories where reasonable, experienced people genuinely disagree, and where the right answer depends on your data volumes, your team, your compliance environment, your scaling needs, and how many different applications touch the same data.
A few observations to carry into the rest of the series:
Validation often needs to happen in more than one place, and that is fine, not a failure. A required-field check in the browser gives the user instant feedback. The same check in the service protects against a malicious client that bypasses the browser. A NOT NULL constraint in the database protects against every writer that bypasses the service. These are not redundant in the wasteful sense – they are defense in depth, each layer covering a different failure mode.
Business rules are where the “set-based versus object-based” tension is sharpest. A rule like “apply this pricing adjustment to every order line matching these criteria” is a natural fit for a single set-based UPDATE in SQL Server. The same rule expressed as “load each order into an object, apply the rule, save it back” through an ORM can turn into thousands of round trips. We will spend real time on this in the performance post, because it is one of the places where placement has dramatic, measurable consequences.
Workflow orchestration – especially when it spans multiple systems, calls external APIs, or has to wait for things – is where the application and service tiers have a genuine, structural advantage. Coordinating “charge this card, then call the shipping provider, then notify the warehouse” is not something a stored procedure should be reaching out to do. Long-running, cross-system, externally-dependent coordination belongs in a service. We will make that case fully in the application-layer post.
The Same Rule, In Three Places
To make this concrete, take one ordinary business rule and watch how it decomposes across the layers:
“A customer cannot place an order that exceeds their available credit limit.”
Look at how many of our categories that single sentence touches:
- There is an integrity component: the order has to reference a real customer with a real credit limit. That is a foreign key, and it belongs in the database.
- There is a business rule component: the comparison of order total against available credit. This could live in a stored procedure that computes the available credit and rejects the order in the same transaction that would create it, or it could live in an application service that loads the customer, checks the number, and decides. This is a real choice with real tradeoffs, and it is exactly the kind of decision the rest of this series is about.
- There is a workflow component: what happens when the limit is exceeded – notify the sales rep, flag for manual review, offer a partial order? That orchestration is a strong fit for a service.
- There is a presentation component: showing the user a friendly “this would exceed your credit limit” message, with their remaining balance, before they even submit. That is client and application-tier work.
- And there is a validation echo: the browser can do a cheap pre-check so the user is not surprised, knowing full well the authoritative check happens deeper in the stack.
One sentence, five different kinds of logic, at least three sensible homes. Anyone who tells you the whole thing belongs in one layer is selling you a slogan, not an architecture.
The Posture I’m Taking in This Series
I want to be upfront about my biases, because everyone has them and pretending otherwise is how you end up writing dishonest comparisons.
I am a DBA. I have a professional fondness for the database doing what the database is uniquely good at, and a professional allergy to seeing it abused as a row-at-a-time object store while all its strengths go unused. I have also been on the operational end of giant stored-procedure monoliths that no one could safely change, and I will not pretend that pain is not real either.
So the position of this series is deliberately not “stored procedures good, ORMs bad,” and it is not the reverse. It is closer to the view you will find running through Erik Darling’s writing on ORMs and SQL Server[8]: these tools are productive, they are not the enemy, and they also do not absolve anyone of understanding execution plans, indexing, parameterization, and query design. The most sophisticated organizations I can find public evidence from do not argue that all logic belongs in one place. They converge, often independently, on a hybrid: integrity and the most performance-sensitive, data-heavy operations close to the data; workflow, orchestration, and external integration in the services; presentation at the edge.
Getting to that hybrid deliberately – rather than by accident, or by whoever won the last argument – is the whole point of what follows.
What’s Coming in This Series
Over the next five posts we will work through this methodically:
- Part 2 – The Case for Logic in Stored Procedures. Centralized enforcement, set-based processing, transaction consistency, least-privilege security, and the very real performance pathologies that ORM-generated SQL can produce – with SQL Server examples throughout.
- Part 3 – The Case for Logic in Application Layers. Separation of concerns, testability, CI/CD, independent scaling, and integration – with examples in ASP.NET Core, Java Spring, Node.js, and SPA frameworks. (And a necessary correction along the way: CI/CD is not an application-only privilege – database schema, procedures, and migrations belong in a pipeline too.)
- Part 4 – Performance and Operations. Execution plans, round trips, caching, ORM behavior, transaction scope, horizontal scaling, and the operational realities of change management, source control, monitoring, and the DBA/developer ownership boundary.
- Part 5 – Security, Misconceptions, and Hybrid Architectures. Permissions, injection surface, row-level security, compliance, the four myths everyone repeats, and the pragmatic hybrid patterns that actually work in production.
- Part 6 – A Decision Framework. A practical checklist for deciding what belongs in the database, what belongs in the services, and what belongs in the client – plus why these arguments turn ideological, and how to stop them from doing so.
If you have read the ALTER DBA ADD AGENT series, you already know I think the interesting questions in our field are rarely “tool X versus tool Y” and almost always “what is this specific job, and what is genuinely best at it?” This series applies that same lens to the oldest layering question we have.
If you have a war story – a stored-procedure monolith that saved you, or an ORM that brought a server to its knees, or a hybrid that got the balance just right – I would genuinely like to hear it. You can find me on Bluesky and LinkedIn.
References and Footnotes
- Erik Darling, “On Entity Framework And SQL Server” – a balanced critique of how developers trust the ORM abstraction without understanding the SQL it generates. erikdarling.com. ↩ back
- Nick Craver, “Stack Overflow: The Architecture – 2016 Edition” – documents roughly 209 million HTTP requests and 504 million SQL queries served per day from SQL Server. nickcraver.com. ↩ back
- “Dapper – a simple object mapper for .NET”: “Dapper was originally developed for and by Stack Overflow.” github.com/DapperLib/Dapper. ↩ back
- “Deconstructing the Monolith: Designing Software that Maximizes Developer Productivity,” Shopify Engineering: “Shopify is one of the largest Ruby on Rails codebases in existence.” shopify.engineering. ↩ back
- “Partitioning GitHub’s relational databases to scale,” GitHub Engineering – describes GitHub’s Ruby on Rails and MySQL architecture, read replicas, and database partitioning at scale. github.blog. ↩ back
- “Designing robust and predictable APIs with idempotency,” Stripe – explains Stripe’s use of idempotency keys to guarantee correctness across unreliable networks. stripe.com/blog/idempotency. ↩ back
- For the broader body of practitioner writing referenced here, see Brent Ozar Unlimited, brentozar.com, and Uber Engineering, uber.com/blog/engineering. ↩ back
- Erik Darling, “Signs It’s Time To Switch From ORMs To Stored Procedures In SQL Server Development” – catalogs recurring ORM-generated SQL problems (wide SELECT lists, long IN clauses, poor parameterization) and when a stored procedure becomes the better tool. erikdarling.com. ↩ back
Next up: The Case for Logic in Stored Procedures – centralized enforcement, set-based processing, and the ORM performance pathologies every DBA eventually meets.
Part of the “Where Does Business Logic Live?” series.