Business Logic: Where the Real Work Happens
Business logic is the rules and processes specific to your business. Not framework code, not database queries, but the logic that makes your application do what it does. Inventory checks, price calculations, workflow state machines, validation rules—these are business logic.
Where you put business logic determines how easy it is to test, reuse, and maintain. The wrong place couples your business logic to framework details and database implementation. The right place keeps it independent and testable.
Where Business Logic Should Live
Business logic should live in the back-end, never the front-end. Front-end logic can be bypassed. Back-end logic is enforced.
Within the back-end, business logic should live in a service layer or domain model, not in:
- API routes: Routes should be thin—validate input, call service, return response. Not calculate, validate, and modify.
- Database queries: Queries should retrieve data, not enforce business rules.
- Middleware: Middleware should handle cross-cutting concerns (logging, auth), not business logic.
The Service Layer Pattern
Business logic lives in service classes/modules. A UserService, OrderService, PaymentService. These services encapsulate business logic and are called by API routes.
Example structure:
// In route handler
const order = await OrderService.create({
user_id: req.user.id,
items: req.body.items,
shipping_address: req.body.address
});
// OrderService.create does:
// - Validate inputs
// - Check inventory
// - Calculate totals
// - Create order record
// - Process payment
// - Send notification
The route is thin. All the complexity is in the service. Services are easy to test (pass in parameters, assert results). Services are easy to reuse (called from multiple routes).
Domain Models: The OOP Approach
An alternative to service layers is domain models. Business concepts become objects with behaviour. An Order object knows how to validate itself, calculate totals, process payment.
This is more sophisticated than services but also more complex. It's appropriate for complex domains with many rules. For simpler applications, services are sufficient.
The Fat Controller Anti-Pattern
A common mistake: all logic lives in the route handler. The route handler validates input, checks business rules, modifies data, sends notifications. It becomes hundreds of lines of spaghetti code.
This is unmaintainable. It's hard to test (you'd need to test the entire route). It's not reusable (if two routes need the same logic, you duplicate code). It violates separation of concerns.
Extract business logic into services. Keep routes thin. This is foundational for maintainability.
Testing Business Logic
Business logic in services is easy to unit test. You don't need the full framework or database. You pass in parameters, assert the result.
Business logic embedded in routes is hard to test. You need to set up HTTP requests, mock middleware, deal with framework concerns. Testing is tedious and slow.
This is another reason to extract business logic into services: testability.
The Complexity Spectrum
Not all applications need sophisticated architecture. The complexity of your business logic dictates the architecture:
- Simple CRUD: Create, read, update, delete records with minimal validation. Route handlers can include logic.
- Standard business app: Multiple entities, business rules (inventory, pricing), validations. Use service layer.
- Complex domain: Many rules, state machines, complex workflows, integration with external systems. Use domain models or sophisticated service architecture.
Architecture should match complexity. Overarchitecting a simple CRUD app is waste. Underarchitecting a complex domain leads to unmaintainable code.
Transactional Integrity
Business operations often span multiple database writes. Create an order, deduct inventory, process payment. Either all succeed or all fail. This is a transaction.
Transactions ensure data consistency. Without them, you could create an order without deducting inventory (inventory and order are inconsistent).
Business logic must be transaction-aware. If something fails mid-operation, the transaction rolls back and the database is consistent.
State Machines for Complex Workflows
Many business processes are state machines. An order starts as "pending", moves to "paid" after payment, then "shipped", then "delivered". Not all transitions are valid (can't go from "pending" to "delivered" directly).
Implementing this in business logic prevents invalid states. A service method for "ship order" checks if the order is "paid" before allowing transition to "shipped".
State machines prevent bugs and enforce business rules at the code level, not just in your head.
The Principle
Business logic is the heart of your application. How you structure it determines how easy the application is to understand, test, modify, and scale.
Thin routes, extracted business logic, clear service boundaries. This makes code maintainable and testable. It's the right structure for professional software.