How to separate business logic from infrastructure code
hexagonal architecture basics, business logic isolation, infrastructure dependencies, dependency injection, testability through separation
Separating Business Logic from Infrastructure
Business logic is the code that encodes what your application actually does — the rules, calculations, and decisions. Infrastructure is everything the application depends on to run: databases, HTTP clients, email services, file systems.
The rule: business logic should never directly import infrastructure code. This one rule makes business logic independently testable without a real database or network.
// Bad — business logic directly depends on infrastructure
function applyLoyaltyDiscount(userId) {
const user = db.query('SELECT * FROM users WHERE id = ?', userId);
if (user.orders_count > 10) {
db.query('UPDATE users SET discount = 0.15 WHERE id = ?', userId);
}
}Separate the logic from the data access:
// Pure business logic — no infrastructure
function calculateLoyaltyDiscount(ordersCount) {
return ordersCount > 10 ? 0.15 : 0;
}
// Infrastructure layer calls the logic
async function applyLoyaltyDiscount(userId) {
const user = await userRepository.findById(userId);
const discount = calculateLoyaltyDiscount(user.ordersCount);
await userRepository.updateDiscount(userId, discount);
}Now calculateLoyaltyDiscount is a pure function you can test with zero setup. The infrastructure layer handles all I/O and calls the pure logic with real data.
This pattern — often called hexagonal or ports-and-adapters architecture — scales from small functions to entire applications. You don't need a framework to apply it: just stop letting business logic directly call databases and HTTP clients.
