Part 01 · The Backend Mindset
Understanding Business Logic
The rules that make your product your product, and why they belong on the server.
⏱ 1 hours · Station 8 of 153
Two apps, two different prices
A food delivery company has a mobile app and a website. A customer orders the same biryani on both, a minute apart. The app charges 380 taka. The website charges 440.
Both are "correct". Each was built by a different team, and each team wrote its own version of the rules: the minimum order, the delivery fee, the first-order discount. One team rounded the discount differently. One forgot that delivery is free above 500 on weekends.
The code that makes a product this product, its prices, limits, permissions and promises, is called business logic. This lesson is about recognising it, keeping it in one place, and keeping it separate from everything else.
The problem: the rules end up everywhere
Look at any growing backend and you'll find the same rule written several times:
| Where the rule hides | Why it got there |
|---|---|
| The mobile app | "To show the total before checkout" |
| The website | Same reason, different team |
| The HTTP handler | "It was the quickest place to add an if" |
| A SQL query | WHERE subtotal >= 200, buried in a report |
| A nightly script | Someone needed the discount for a refund |
Each copy is a chance to drift. When marketing changes "20% off, max 100" to "25% off, max 120", someone has to find every copy, and they won't.
There's a second problem: when the rules are tangled with plumbing (reading HTTP, talking to the database, formatting JSON), you can't test the rules without starting a server and a database, so nobody tests them properly.
Think of a cricket rulebook
A cricket match has many moving parts: the pitch, the scoreboard, the TV broadcast, the ticket gates. None of that is cricket. Cricket is the rulebook: what counts as out, how many balls in an over, when a no-ball is called.
The rulebook is written once, by one body, and every match follows it. The TV channel doesn't get its own version of the LBW rule. And you can understand and check the rules without a stadium: you can read them, discuss them, and test situations on paper ("ball pitched outside leg, not out").
Business logic is your product's rulebook. HTTP, the database and the user interface are the stadium.
Where the analogy stops working. Cricket's rules change rarely and slowly. A business changes its rules constantly: new discounts, new cities, new payment methods, sometimes weekly. That's exactly why the rules must live in one clearly marked place: the more often something changes, the more it hurts to have copies.
The building blocks
- Business logic is the code that encodes decisions the business makes: who may do what, what things cost, what's allowed, what happens next. If a non-programmer in the company would have an opinion about it ("free delivery from 500"), it's business logic.
- Plumbing (also called infrastructure) is everything else: parsing HTTP, reading and writing the database, sending email, logging, retries. It's essential, but it isn't your product. Another company's backend has nearly the same plumbing.
- Keep the rules pure where you can. A function that takes plain values (subtotal, hour, is this a new user) and returns a decision, without touching the network, the clock or the database, is easy to read, test and reuse. The plumbing gathers the inputs, calls the rules, and acts on the answer.
- One home, many callers. The mobile app, the website, the admin tool and the nightly script all ask the backend; the backend asks the rules. A client may show an estimate, but the number that's charged comes from the one home (lesson 2).
- Rules have reasons. A good rule returns not just "no" but why ("minimum order is 200 taka"), so the client can show a helpful message and support staff can explain it.
A new user orders 400 taka of food at 13:00 and pays cash. The rules: minimum 200; delivery 60, free from 500; new users get 20% off, at most 100 taka; cash only up to 3000. What do they pay?
Behind the scenes: one order, many rules
An order passes the rulebook
Step 1/5 The app sends the order
The app sends what the user chose: the items, the payment method. It may show an estimated total, but it doesn't send one.
- items
- biryani x2
- payment
- cash
The picture to keep in your head
Draw three rings:
- The core: business rules. Plain functions and types:
quote(order),canCancel(order, now),Plan.noteLimit. No HTTP, no SQL. - Around it: the application, which loads what the rules need, calls them, and saves the result.
- The outside: HTTP, databases, queues, email providers, the clock.
Dependencies point inward: the outside knows about the rules, the rules know nothing about the outside. That's what lets you change the database, add a new client, or test every rule in milliseconds, without touching the rulebook. Module 5 builds this structure properly.
Try it: business logic or plumbing?
Is this a business rule?
if subtotal < 200 { refuse }Code from first principles
A food delivery shop's rules in one pure function, quote. It takes plain values (the subtotal, the hour, whether it's a first order, the payment method) and returns either the amount to pay or the reason the order is refused. No HTTP, no database, no clock: the caller passes the hour in.
The program runs seven orders through it, like a table of test cases.
package main
import "fmt"
type order struct {
subtotal int // taka, already worked out from the menu prices
hour int // when it was placed, 0-23
newUser bool // first order ever?
payment string // "card" or "cash"
}
// quote holds the shop's business rules, and nothing else: no HTTP, no database.
// It returns the amount to pay, or the reason the order can't be placed.
func quote(o order) (int, string) {
if o.hour < 10 || o.hour >= 22 {
return 0, "we deliver 10:00-22:00 only"
}
if o.subtotal < 200 {
return 0, "minimum order is 200 taka"
}
if o.payment == "cash" && o.subtotal > 3000 {
return 0, "cash on delivery only up to 3000"
}
delivery := 60
if o.subtotal >= 500 {
delivery = 0 // free delivery from 500
}
discount := 0
if o.newUser {
discount = min(o.subtotal*20/100, 100) // 20% off the first order, at most 100
}
return o.subtotal - discount + delivery, ""
}
func main() {
cases := []struct {
name string
o order
}{
{"small lunch", order{150, 13, false, "card"}},
{"regular lunch", order{350, 13, false, "card"}},
{"family dinner", order{600, 20, false, "cash"}},
{"first order, big", order{600, 20, true, "card"}},
{"first order, small", order{400, 13, true, "cash"}},
{"office party", order{3500, 12, false, "cash"}},
{"midnight snack", order{300, 23, false, "card"}},
}
fmt.Printf("%-19s %8s %5s %-4s %-5s %s\n", "order", "subtotal", "time", "new", "pay", "result")
for _, c := range cases {
total, reason := quote(c.o)
result := fmt.Sprintf("pay %d", total)
if reason != "" {
result = "refused: " + reason
}
isNew := map[bool]string{true: "yes", false: "no"}[c.o.newUser]
fmt.Printf("%-19s %8d %02d:00 %-4s %-5s %s\n", c.name, c.o.subtotal, c.o.hour, isNew, c.o.payment, result)
}
}// quote holds the shop's business rules, and nothing else: no HTTP, no database.
// It returns [amount to pay, ""] or [0, the reason the order can't be placed].
function quote({ subtotal, hour, newUser, payment }) {
if (hour < 10 || hour >= 22) return [0, "we deliver 10:00-22:00 only"];
if (subtotal < 200) return [0, "minimum order is 200 taka"];
if (payment === "cash" && subtotal > 3000) return [0, "cash on delivery only up to 3000"];
const delivery = subtotal >= 500 ? 0 : 60; // free delivery from 500
const discount = newUser ? Math.min(Math.floor((subtotal * 20) / 100), 100) : 0; // 20% off the first order, at most 100
return [subtotal - discount + delivery, ""];
}
const cases = [
["small lunch", { subtotal: 150, hour: 13, newUser: false, payment: "card" }],
["regular lunch", { subtotal: 350, hour: 13, newUser: false, payment: "card" }],
["family dinner", { subtotal: 600, hour: 20, newUser: false, payment: "cash" }],
["first order, big", { subtotal: 600, hour: 20, newUser: true, payment: "card" }],
["first order, small", { subtotal: 400, hour: 13, newUser: true, payment: "cash" }],
["office party", { subtotal: 3500, hour: 12, newUser: false, payment: "cash" }],
["midnight snack", { subtotal: 300, hour: 23, newUser: false, payment: "card" }],
];
console.log(`${"order".padEnd(19)} ${"subtotal".padStart(8)} ${"time".padStart(5)} ${"new".padEnd(4)} ${"pay".padEnd(5)} result`);
for (const [name, o] of cases) {
const [total, reason] = quote(o);
const result = reason ? `refused: ${reason}` : `pay ${total}`;
const time = `${String(o.hour).padStart(2, "0")}:00`;
console.log(`${name.padEnd(19)} ${String(o.subtotal).padStart(8)} ${time} ${(o.newUser ? "yes" : "no").padEnd(4)} ${o.payment.padEnd(5)} ${result}`);
}struct Order {
subtotal: i64, // taka, already worked out from the menu prices
hour: u32, // when it was placed, 0-23
new_user: bool, // first order ever?
payment: &'static str, // "card" or "cash"
}
// quote holds the shop's business rules, and nothing else: no HTTP, no database.
// It returns the amount to pay, or the reason the order can't be placed.
fn quote(o: &Order) -> Result<i64, &'static str> {
if o.hour < 10 || o.hour >= 22 {
return Err("we deliver 10:00-22:00 only");
}
if o.subtotal < 200 {
return Err("minimum order is 200 taka");
}
if o.payment == "cash" && o.subtotal > 3000 {
return Err("cash on delivery only up to 3000");
}
let delivery = if o.subtotal >= 500 { 0 } else { 60 }; // free delivery from 500
let discount = if o.new_user { (o.subtotal * 20 / 100).min(100) } else { 0 }; // 20% off the first order, at most 100
Ok(o.subtotal - discount + delivery)
}
fn main() {
let cases = [
("small lunch", Order { subtotal: 150, hour: 13, new_user: false, payment: "card" }),
("regular lunch", Order { subtotal: 350, hour: 13, new_user: false, payment: "card" }),
("family dinner", Order { subtotal: 600, hour: 20, new_user: false, payment: "cash" }),
("first order, big", Order { subtotal: 600, hour: 20, new_user: true, payment: "card" }),
("first order, small", Order { subtotal: 400, hour: 13, new_user: true, payment: "cash" }),
("office party", Order { subtotal: 3500, hour: 12, new_user: false, payment: "cash" }),
("midnight snack", Order { subtotal: 300, hour: 23, new_user: false, payment: "card" }),
];
println!("{:<19} {:>8} {:>5} {:<4} {:<5} result", "order", "subtotal", "time", "new", "pay");
for (name, o) in &cases {
let result = match quote(o) {
Ok(total) => format!("pay {total}"),
Err(reason) => format!("refused: {reason}"),
};
let new = if o.new_user { "yes" } else { "no" };
println!("{:<19} {:>8} {:02}:00 {:<4} {:<5} {}", name, o.subtotal, o.hour, new, o.payment, result);
}
}from dataclasses import dataclass
@dataclass
class Order:
subtotal: int # taka, already worked out from the menu prices
hour: int # when it was placed, 0-23
new_user: bool # first order ever?
payment: str # "card" or "cash"
def quote(o):
"""The shop's business rules, and nothing else: no HTTP, no database.
Returns (amount to pay, "") or (0, the reason the order can't be placed)."""
if o.hour < 10 or o.hour >= 22:
return 0, "we deliver 10:00-22:00 only"
if o.subtotal < 200:
return 0, "minimum order is 200 taka"
if o.payment == "cash" and o.subtotal > 3000:
return 0, "cash on delivery only up to 3000"
delivery = 0 if o.subtotal >= 500 else 60 # free delivery from 500
discount = min(o.subtotal * 20 // 100, 100) if o.new_user else 0 # 20% off the first order, at most 100
return o.subtotal - discount + delivery, ""
cases = [
("small lunch", Order(150, 13, False, "card")),
("regular lunch", Order(350, 13, False, "card")),
("family dinner", Order(600, 20, False, "cash")),
("first order, big", Order(600, 20, True, "card")),
("first order, small", Order(400, 13, True, "cash")),
("office party", Order(3500, 12, False, "cash")),
("midnight snack", Order(300, 23, False, "card")),
]
print(f"{'order':<19} {'subtotal':>8} {'time':>5} {'new':<4} {'pay':<5} result")
for name, o in cases:
total, reason = quote(o)
result = f"refused: {reason}" if reason else f"pay {total}"
new = "yes" if o.new_user else "no"
print(f"{name:<19} {o.subtotal:>8} {o.hour:02d}:00 {new:<4} {o.payment:<5} {result}")public class Main {
// subtotal in taka (already worked out from the menu prices), hour 0-23,
// newUser = first order ever, payment = "card" or "cash".
record Order(int subtotal, int hour, boolean newUser, String payment) {}
// The answer: the amount to pay, or the reason the order can't be placed.
record Quote(int total, String reason) {}
// quote holds the shop's business rules, and nothing else: no HTTP, no database.
static Quote quote(Order o) {
if (o.hour() < 10 || o.hour() >= 22) return new Quote(0, "we deliver 10:00-22:00 only");
if (o.subtotal() < 200) return new Quote(0, "minimum order is 200 taka");
if (o.payment().equals("cash") && o.subtotal() > 3000) return new Quote(0, "cash on delivery only up to 3000");
int delivery = o.subtotal() >= 500 ? 0 : 60; // free delivery from 500
int discount = o.newUser() ? Math.min(o.subtotal() * 20 / 100, 100) : 0; // 20% off the first order, at most 100
return new Quote(o.subtotal() - discount + delivery, "");
}
public static void main(String[] args) {
Object[][] cases = {
{"small lunch", new Order(150, 13, false, "card")},
{"regular lunch", new Order(350, 13, false, "card")},
{"family dinner", new Order(600, 20, false, "cash")},
{"first order, big", new Order(600, 20, true, "card")},
{"first order, small", new Order(400, 13, true, "cash")},
{"office party", new Order(3500, 12, false, "cash")},
{"midnight snack", new Order(300, 23, false, "card")},
};
System.out.printf("%-19s %8s %5s %-4s %-5s %s%n", "order", "subtotal", "time", "new", "pay", "result");
for (Object[] c : cases) {
Order o = (Order) c[1];
Quote q = quote(o);
String result = q.reason().isEmpty() ? "pay " + q.total() : "refused: " + q.reason();
System.out.printf("%-19s %8d %02d:00 %-4s %-5s %s%n", c[0], o.subtotal(), o.hour(), o.newUser() ? "yes" : "no", o.payment(), result);
}
}
}Output (the same in all five languages):
order subtotal time new pay result
small lunch 150 13:00 no card refused: minimum order is 200 taka
regular lunch 350 13:00 no card pay 410
family dinner 600 20:00 no cash pay 600
first order, big 600 20:00 yes card pay 500
first order, small 400 13:00 yes cash pay 380
office party 3500 12:00 no cash refused: cash on delivery only up to 3000
midnight snack 300 23:00 no card refused: we deliver 10:00-22:00 only
Reading the output:
- small lunch / midnight snack / office party: each is refused by a different rule, with a reason a person can read. The rules are checked in a deliberate order: shop open first, then the minimum, then payment limits.
- regular lunch: 350 + 60 delivery = 410.
- family dinner: 600, delivery free because the subtotal is at least 500.
- first order, big: 20% of 600 is 120, capped at 100, so 500; delivery is free.
- first order, small: 400 − 80 + 60 = 380, the answer to the predict question.
- Notice what you didn't need: a server, a database or a real clock. Seven situations, checked in microseconds. That's the payoff of keeping the rules pure.
Real engineering
- Give the rules a home. Many teams use a
domainorcorepackage for business rules and keep handlers thin: parse, call the rules, respond. (Module 5, lessons 6 and 11.) - Test the rules like a table. Exactly like the output above: inputs in, expected answer out, dozens of rows. Rules are where bugs cost money, and pure functions are the cheapest code to test.
- Make time an input. Rules that call
now()inside are hard to test ("what happens at 21:59?"). Pass the time in, asquotedoes. - Store the result, not just the inputs. Save the total, discount and fee an order actually got. When the rules change, old orders must not change with them (lesson 4).
- Some rules become configuration. A threshold that changes every week (free delivery from 500) can move into a settings table that the rules read, so changing it doesn't need a deploy. The rule stays in code; the number becomes data.
When it breaks
Pick a failure
app: 380 taka website: 440 taka same cart
Think before you read on
The mobile team asks: "Can we keep a copy of the discount rules in the app, so we can show the total instantly without waiting for the server?" Is that allowed?
Reveal
Yes, as an estimate. Showing a quick total is good for users. But the app's copy must never be the one that decides what's charged: the backend recalculates with its own rules, and the app replaces its estimate with the backend's answer. And to keep the copies from drifting, many teams have the backend send the current rule values (or a quote endpoint) instead of hard-coding them in the app.
Check yourself
1. What's a quick test for whether some code is business logic?
Answer
Would someone in the business (sales, finance, support, a manager) have an opinion about it, or want to change it? Prices, limits, permissions and promises are business logic; parsing, retries and database access are plumbing.
2. Why is a pure quote(order) function easier to work with than one that reads the database and the clock itself?
Answer
It can be tested with plain values in microseconds, covering edge cases like 21:59 or exactly 500 taka, and it can be called from any client path without dragging I/O along.
3. Why should an order store the discount it received, even though the rules could recompute it?
Answer
Because the rules will change. The order is a historical fact; recomputing with next month's rules would rewrite what the customer actually paid.
4. The app shows an estimated total using its own copy of the rules. What must the backend still do?
Answer
Recalculate the total with its own rules and charge that amount, ignoring the client's number (lesson 2).
Your challenge: a weekend rule
The business adds a rule: on Fridays, delivery is free from 300 taka instead of 500.
- Add the day of the week as an input to
quote, the same way the hour is. - Add two rows to the table: a 350-taka order on a Friday and the same order on a Tuesday.
Before you run it, predict both totals.
You've succeeded when the Friday order pays 350 and the Tuesday order pays 410, every existing row prints exactly what it did before, and quote still doesn't read the clock itself.
The mental model
Business logic is your product's rulebook: prices, limits, permissions and promises. Plumbing is everything that moves data around it. Give the rules one home on the backend, make them plain functions of their inputs, and let every client and script go through them. Clients may estimate; the rules decide.
Station stamp
Finish the chapter
Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.
Go Pro to earn XP →