Part 01 · Backend Mindset

Data, State, Logic and Computation

DataStateLogic

যে চারটা idea দিয়ে সব backend বানানো, আর real code এ এদের আলাদা করে চেনার উপায়।

⏱ 1 ঘণ্টা · Station 4 / 153

Total দেখালো 65। হওয়ার কথা 75।

একটা café র ছোট একটা ordering app আছে। একজন customer cart এ দুই কাপ চা আর একটা cake নিলো। Total: 90 টাকা। এর মধ্যে café চায়ের দাম 20 থেকে 25 করে দিলো। Customer একটা চা বাদ দিলো, আর cart এ এখন দেখাচ্ছে 65 টাকা।

নিজে যোগ করে দেখো: একটা চা 25, একটা cake 50, মোট 75। App 10 টাকা কম দেখাচ্ছে, অথচ যোগ করার code এ কেউ হাতই দেয়নি।

Bug টা যোগ-বিয়োগে না। Bug টা অনেক আগের একটা সিদ্ধান্তে: কোনটা save করবো, আর কোনটা দরকারের সময় হিসাব করে নেবো। এই lesson এ চারটা শব্দ শিখবো, যেগুলো জানা থাকলে এই সিদ্ধান্ত নেওয়া সহজ হয়ে যায়: data, state, logic আর computation।

সমস্যা: সবকিছুই দেখতে "variable"

যেকোনো backend খুললেই দেখবে value আর value: দাম, cart, user এর নাম, total, limit, flag। নতুন অবস্থায় সবগুলোকে একই রকম লাগে, কিছু একটা ধরে রাখা variable।

কিন্তু এদের আচরণ একদম আলাদা:

Valueকে বদলায়?কত ঘন ঘন?হারিয়ে গেলে কী হয়?
চায়ের দামCafé র মালিকবছরে কয়েকবারদোকান কিছুই বেচতে পারবে না
আমার cart এ কী আছেআমি, প্রতিটা tap এমিনিটে কয়েকবারবিরক্ত হয়ে আবার শুরু করবো
"একটা item সর্বোচ্চ 5টা"Developer, release এর সময়কালেভদ্রে, জেনেবুঝেকিছুই না: এটা code এ আছে
Cart এর totalসরাসরি কেউ নাদাম বা cart বদলালেইকিছুই না: আবার হিসাব করে নাও

সবগুলোকে এক রকম ধরলে 65 টাকার cart এর মতো bug হয়: যে value টা হিসাব করে নেওয়ার কথা, সেটা save করে রাখা হয়েছে, আর পরে যে value গুলো থেকে ওটা এসেছিল তাদের সাথে আর মেলেনি।

রান্নাঘরের কথা ভাবো

বাসার রান্নাঘরে চার রকম জিনিস থাকে।

  • ভাঁড়ারে কী আছে তার list: চাল 2 kg, তেল 1 litre, ডিম 6টা। বাইরের জগতের fact। (data)
  • এই মুহূর্তে চুলায় কী চলছে: ভাত আধা সেদ্ধ, পেঁয়াজ ভাজা হচ্ছে। মিনিটে মিনিটে বদলায়, আর শুধু এই রান্নার জন্যই দরকারি। (state)
  • Recipe র নিয়ম: "পানি ফুটলে তারপর লবণ", "3টার বেশি মরিচ না"। (logic)
  • হিসাব: "6 জনের জন্য সব ডাবল: 4 কাপ চাল"। (computation)

ভাঁড়ারের list এ কেউ "4 কাপ" লিখে রাখে না। দরকার হলে কয়জন খাবে সেটা দেখে হিসাব করে নেয়, তাই ওটা কখনো ভুল হয় না।

store the facts, compute the restdata: pricestea 20 → 25state: carttea x2, cake x1computationqty × pricecomputed totalalways freshstored totalsaved once90 → 100still 90: stale9025
Cart আর দাম মিলে একটা হিসাব হয়। চায়ের দাম বদলালে হিসাব করা total ও সাথে সাথে বদলায়। আগে save করে রাখা total জানেই না কিছু বদলেছে, তাই চুপচাপ পুরনো হয়ে যায়।

Analogy টা কোথায় খাটে না। রান্নাঘরে একজন মানুষই চারটা জিনিস মাথায় রাখে। Backend এ এরা ইচ্ছা করেই আলাদা জায়গায় থাকে: data থাকে database এ, state থাকে প্রতিটা user বা request এর সাথে, logic থাকে code এ, আর computation হয় দরকারের সময়, হয়তো প্রতিবার আলাদা machine এ। এই ভাগাভাগির জন্যই backend scale করতে পারে, আবার এই কারণেই 65 টাকার bug টা হওয়া সম্ভব।

মূল জিনিসগুলো

  1. Data: যে fact গুলো system রেখে দেয়। Product আর দাম, user আর তার email, আগের order। Data বদলায় কেউ জেনেবুঝে কিছু করলে (admin দাম বদলালো, user sign up করলো), browse করতে গিয়ে আপনা আপনি না। Restart এর পরেও থাকতে হবে, তাই এটা থাকে storage এ। (Module 6।)
  2. State: এই মুহূর্তে কী অবস্থা। একটা cart, অর্ধেক পূরণ করা একটা form, চলতে থাকা একটা game, checkout এর কোন ধাপে আছো, একটা খোলা connection। প্রতিটা request এ state বদলায়, আর সাধারণত এটা একজন user বা একটা process এর। State নিয়ে বড় প্রশ্নটা হলো এটা কোথায় থাকবে, lesson 5 পুরোটাই এটা নিয়ে।
  3. Logic: কী করা যাবে আর যাবে না তার নিয়ম। "একটা item সর্বোচ্চ 5টা", "free plan এ 3টা note", "1,000 টাকার বেশি হলে delivery free"। Logic থাকে code এ। কোনো নির্দিষ্ট user এর value সে ধরে রাখে না; user দের সাথে কী হবে সেটা ঠিক করে। (Lesson 8।)
  4. Computation: বাকি তিনটা থেকে হিসাব করে পাওয়া value। Total, গোনা, গড়, search এর ফল, delivery কবে পৌঁছাবে তার আন্দাজ। হিসাব করা value এর একটা উৎস থাকে: data আর state জানা থাকলে এটা যখন খুশি আবার বের করা যায়।

যেকোনো value কে ঠিক বাক্সে ফেলার একটা প্রশ্ন:

এই value টা কি অন্য value থেকে আবার পাওয়া যায়? গেলে এটা computed: হিসাব করে নাও, save করো না (মেপে দেখা কোনো কারণ না থাকলে)। না গেলে এটা data বা state: যত্ন করে save করো, কারণ হারালে আর ফেরত আনা যাবে না।

আগে অনুমান করো

একজন user এর profile এ লেখা 'Age: 27'। Database এ একটা column আছে age = 27, তিন বছর আগে sign up এর সময় save করা। সমস্যাটা কী?

ভেতরে কী হয়: cart টা কীভাবে ভুল হলো

Café র cart টা ধাপে ধাপে দেখি। App দুটো total রাখতো: একটা save করা, প্রতিটা বদলে হাতে update হয়; আরেকটা cart আর দাম থেকে হিসাব করা।

ধাপে ধাপে দেখো

দুটো total, একটা cart

CustomerLogicCart (state)Prices (data)Stored total

ধাপ 1/6 2টা চা যোগ

Logic নিয়ম দেখে (একটা item সর্বোচ্চ 5টা): ঠিক আছে। State বদলায়: tea x2। Save করা total এ যোগ হয় 2 × 20 = 40। হিসাব করা: 2 × 20 = 40। দুটো মিলে যায়।

cart
tea x2
stored
40
computed
40

মাথায় রাখার ছবি

চারটা বাক্স আঁকো:

  • একদম নিচে Data, storage এ: ধীরে বদলানো fact গুলো।
  • পাশে State: প্রতিটা user এর দ্রুত বদলানো "এখনকার অবস্থা"।
  • দুটোর সামনে একটা গেট হিসেবে Logic: নিয়ম পার না হয়ে data বা state কিছুই বদলায় না।
  • সবার ওপরে Computation, data আর state থেকে বের হওয়া তীর হিসেবে, দরকারের সময় উত্তর বানায়।

Value যায় ওপরের দিকে, data আর state থেকে computation এ। Save করা copy হয়ে আবার নিচে নামা উচিত না, যদি না তুমি জেনেবুঝে ঠিক করো copy টা রাখার মতো, আর সেটা কীভাবে ঠিক রাখবে তারও একটা plan থাকে।

চেষ্টা করো: কোনটা কী?

প্রতিটা value কে ভাগ করো। প্রশ্নটা মনে রাখো: এটা কি অন্য value থেকে আবার পাওয়া যায়?

প্রতিটা case চালিয়ে দেখো

Data, state, logic নাকি computation?

1996-03-14
কাজ করে: Data। কোনোভাবে হিসাব করে বের করা যায় না; হারালে শেষ। Save করো।

গোড়া থেকে code

Café র cart, code এর ভেতরে চার রকম জিনিসের গায়ে নাম লাগানো:

  • prices হলো data।
  • maxPerItem হলো logic।
  • cart হলো state।
  • computedTotal() হলো computation।

আরেকটা আছে storedTotal: হিসাব করা value, কিন্তু state এর মতো save করে রাখা, আর প্রতিটা বদলে হাতে update করা। Bug টা এটাই, ইচ্ছা করে রেখে দিয়েছি যাতে চোখের সামনে ঘটতে দেখো।

package main

import (
	"fmt"
	"slices"
	"strings"
)

// Data: facts the business keeps. They change rarely, and not because of this user.
var prices = map[string]int{"tea": 20, "cake": 50}

const maxPerItem = 5 // logic: a rule about what is allowed

// State: this user's cart right now. Every request may change it.
type line struct {
	item string
	qty  int
}

var (
	cart        []line
	storedTotal int // a second copy of the total, kept up to date by hand
)

// Computation: derive the total from the state and the data, every time it's needed.
func computedTotal() int {
	total := 0
	for _, l := range cart {
		total += l.qty * prices[l.item]
	}
	return total
}

// add applies the logic, then changes the state (and tries to keep storedTotal in step).
// A negative qty removes items.
func add(item string, qty int) string {
	i := slices.IndexFunc(cart, func(l line) bool { return l.item == item })
	have := 0
	if i >= 0 {
		have = cart[i].qty
	}
	if have+qty > maxPerItem {
		return fmt.Sprintf("refused: at most %d per item", maxPerItem)
	}
	switch {
	case i < 0:
		cart = append(cart, line{item, qty})
	case have+qty == 0:
		cart = slices.Delete(cart, i, i+1)
	default:
		cart[i].qty += qty
	}
	storedTotal += qty * prices[item]
	return ""
}

func show(step, note string) {
	parts := []string{}
	for _, l := range cart {
		parts = append(parts, fmt.Sprintf("%s x%d", l.item, l.qty))
	}
	row := fmt.Sprintf("%-22s %-15s %6d %9d", step, strings.Join(parts, ", "), storedTotal, computedTotal())
	if note != "" {
		row += "  " + note
	}
	fmt.Println(row)
}

func main() {
	fmt.Printf("%-22s %-15s %6s %9s\n", "step", "cart", "stored", "computed")
	show("add 2 tea", add("tea", 2))
	show("add 1 cake", add("cake", 1))
	prices["tea"] = 25 // the shop changes a price: data changes, the cart doesn't
	show("tea price 20 -> 25", "")
	show("remove 1 tea", add("tea", -1))
	show("add 9 cake", add("cake", 9))
	if storedTotal != computedTotal() {
		fmt.Printf("the stored total is off by %d taka\n", computedTotal()-storedTotal)
	}
}

Output (পাঁচটা language এই একই):

step                   cart            stored  computed
add 2 tea              tea x2              40        40
add 1 cake             tea x2, cake x1     90        90
tea price 20 -> 25     tea x2, cake x1     90       100
remove 1 tea           tea x1, cake x1     65        75
add 9 cake             tea x1, cake x1     65        75  refused: at most 5 per item
the stored total is off by 10 taka

Output টা পড়ি:

  • প্রথম দুই সারি: save করা আর হিসাব করা total মিলে যায়। Test এর সময় এরকম bug চোখে পড়ে না, কারণ ছোট একটা test এর মধ্যে কেউ data বদলায় না।
  • tea price 20 → 25: cart বদলায়নি, কিন্তু হিসাব করা total 100 হয়ে গেছে, কারণ নিচের data বদলেছে। Save করা total এর জানার কোনো উপায় ছিল না।
  • remove 1 tea: save করা total এখন দুটো দাম মিশিয়ে ফেলেছে (যোগ হয়েছিল 20 এ, বাদ গেছে 25 এ), তাই হয়েছে 65। হিসাব করা total 75, customer এর আসলে এটাই দেওয়ার কথা।
  • add 9 cake: logic না করে দেয়, কোনো total ই নড়ে না। কোন total রাখছো, নিয়মের তাতে কিছু যায় আসে না।
  • শেষ লাইন: 10 টাকা কম। দাম বদলানোর সময় যতগুলো cart খোলা ছিল সবগুলোর সাথে গুণ করো, support team এর জন্য বড় ঝামেলা।

আসল engineering

  • Default হলো হিসাব করা; save করো কারণ থাকলে। বের করা গেলে বের করো। হিসাব করতে মেপে দেখা গেছে অনেক ধীর, শুধু তখনই হিসাব করা value save করো, আর তখন ওটা ঠিক রাখার একটা ব্যবস্থাও লাগবে। এটাকেই cache বলে, আর cache ঠিক রাখা যে কত ঝামেলার, সেটা সবাই জানে। (Module 6, lesson 17 আর 18।)
  • যেটা আটকে রাখতে চাও, তার snapshot নাও। Order এ save হবে কেনার সময়কার দাম: ওই সংখ্যা এখন ইতিহাস, তাই data। Cart এর টাকা এখনো দেওয়া হয়নি, তাই সেখানে আজকের দাম। একই সংখ্যা, মানে আলাদা।
  • Logic বদলায় release এ, data বদলায় request এ। Marketing প্রতি সপ্তাহে যে free shipping এর সীমা বদলায়, তার জন্য deploy লাগা উচিত না; team গুলো ওটাকে data তে (একটা config table এ) সরিয়ে নেয়, আর logic সেখান থেকে পড়ে।
  • State এর একটা ঠিকানা লাগে। Cart কোথায় রাখবে (browser এ? server এর memory তে? database এ?) backend এর সবচেয়ে জরুরি সিদ্ধান্তগুলোর একটা। এটাই lesson 5।
  • নাম দেখেই যেন ধরন বোঝা যায়। subtotal() পড়লে বোঝা যায় হিসাব করা; subtotal নামে একটা field থাকলে কেউ না কেউ ওটা set করতে চাইবে। যে code এ চার রকম জিনিস পরিষ্কার আলাদা, সেটা ঠিক রাখাও সহজ।

যখন ভাঙে

"হিসাব মিলছে না" ধরনের বেশিরভাগ bug এর মধ্যে কোনো একটা।

প্রতিটা case চালিয়ে দেখো

একটা failure বাছো

cart দেখায় 65, item গুলো যোগ করলে 75
ভাঙে: হিসাব করা একটা value save করা হয়েছিল, আর উৎস বদলালেও update হয়নি। ধরার উপায়: উৎস থেকে আবার হিসাব করে মিলিয়ে দেখা। সমাধান: দরকারের সময় হিসাব করা, বা একই transaction এ সব copy update করা।

পড়ার আগে ভাবো

একটা e-commerce site এর product page এ লেখা "In stock: 12"। এই সংখ্যাটা data, state, নাকি computation? একসাথে 3,000 জন page টা দেখছে, তাহলে কি উত্তর বদলায়?

উত্তর দেখো

ধারণার দিক থেকে এটা computation: যত এসেছে তা থেকে যত বিক্রি হয়েছে আর যত cart এ আটকে আছে সেগুলো বাদ। কিন্তু প্রতিটা page view তে সব order থেকে এটা হিসাব করা অনেক খরচের, তাই আসল system সাধারণত stock এর একটা সংখ্যা save করে রাখে (data), আর প্রতিটা বিক্রির একই transaction এ সেটা update করে। 3,000 জন দেখলে প্রত্যেকের screen এর সংখ্যাটা তার browser এ already পুরনো state: page load এর সময় সত্যি ছিল। এজন্যই checkout এর সময় server কে আবার stock check করতে হয় (lesson 2), আর শেষ item টা দুজন একসাথে কিনলে কী হয় সেটা আলাদা একটা lesson (lesson 6)।

নিজেকে check করো

1. কোন একটা প্রশ্ন দিয়ে বোঝা যায় একটা value save করা উচিত কিনা?

উত্তর

এটা কি অন্য value থেকে আবার পাওয়া যায়? গেলে এটা computed: save না করে হিসাব করো, যদি না মেপে দেখা কোনো কারণ থাকে আর copy টা ঠিক রাখার plan থাকে।

2. Order কেন বেচার সময়ের দাম save করবে, কিন্তু cart করবে না?

উত্তর

Order এর দাম অতীতের একটা fact: আজকের price list দেখে গত বছর কত দেওয়া হয়েছিল বলা যায় না, তাই এটা data। Cart এর টাকা এখনো দেওয়া হয়নি, তাই সেখানে আজকের দাম থাকা উচিত, মানে হিসাব করে নেওয়া।

3. "1,000 টাকার বেশি হলে delivery free" এটা data, state, logic নাকি computation?

উত্তর

Logic: কী হবে তার নিয়ম। 1,000 সংখ্যাটা হয়তো data (একটা setting) হিসেবে রাখা হতে পারে যাতে release ছাড়া বদলানো যায়, কিন্তু নিয়মটা নিজে logic।

4. Program এ প্রথম দুই ধাপে save করা আর হিসাব করা total মিললো কেন?

উত্তর

কারণ তখনো কোনো data বদলায়নি। Save করা copy ভুল হয় শুধু তখনই, যখন তার উৎস এমনভাবে বদলায় যেটা update করার code দেখতে পায় না, এখানে cart এর বাইরে দাম বদলানো।

তোমার challenge: একটা discount যোগ করো

Café র cart এ একটা coupon যোগ করো: TEA10 দিলে cart এর প্রতিটা চায়ে 10 টাকা কম।

  • নতুন প্রতিটা value কোন ধরনের ঠিক করো: customer যে coupon code দিলো, "প্রতি চায়ে 10 কম" নিয়মটা, আর discount এর পরিমাণ।
  • Discount কে computation বানাও: total দেখানোর প্রতিবার cart, দাম আর coupon থেকে হিসাব হবে।

চালানোর আগে আন্দাজ করো এই ধাপগুলোর পর total কত হবে: 2টা চা, 1টা cake, TEA10 দেওয়া, চায়ের দাম 20 → 25, 1টা চা বাদ।

সফল হয়েছো যখন তোমার program শেষে total 65 print করে (25 টাকার একটা চা, 10 কম, আর 50 টাকার একটা cake), আর coupon আর দাম বদল যে ক্রমেই করো, total ঠিক থাকে।

Mental model

Backend এর প্রতিটা value চার রকমের একটা। Data হলো যে fact গুলো রেখে দাও। State হলো এই মুহূর্তের অবস্থা। Logic হলো নিয়ম, code এ। Computation হলো বাকি তিনটা থেকে যা কিছু হিসাব করে বের করা যায়। Data আর state যত্ন করে save করো; বাকিটা হিসাব করো। হিসাব করা value save করলে সেটা একটা copy, আর copy পুরনো হয়ে যেতে পারে।

Station stamp

Chapter শেষ করো

পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।

Pro নিয়ে XP পাও →