Suppose you and three classmates are building "SchoolConnect" for your school's 1,200 students — one app that handles login, marking attendance, searching the library catalogue, and ordering snacks from the canteen. You start the way almost every programmer starts: one project, one running program, one database. It works. You demo it in class, the principal is impressed, and the school decides to actually deploy it. Three weeks later, at 12:30 pm on a Tuesday, the canteen-ordering feature has a bug — an unhandled case where a student tries to order an item that's out of stock — and the whole app crashes. Not just canteen ordering. A student trying to mark attendance at that exact moment gets an error too. So does someone quietly browsing the library catalogue in a free period. One broken feature took down three working ones. That single incident is the entire motivation for this chapter, and by the end of it you'll know exactly why it happened and how professional systems are architected so that it doesn't.
Building SchoolConnect: One Program to Rule Them All
When you write SchoolConnect the "obvious" way, you get what's called a monolithic architecture. The word monolith literally means "one stone" — a single, indivisible block. In software terms: a monolithic architecture is a design where every feature of an application lives inside one codebase, runs as one process, and is deployed as one unit, usually sharing a single database. Your login code, attendance code, library code, and canteen code all sit in the same project, get compiled or started together, and if you want to update the canteen menu, you redeploy the entire application — login, attendance, and library included, even though nothing about them changed.
This isn't a bad choice at first. For a four-person team building a first version, a monolith is genuinely the right call: one codebase is easy to reason about, you don't need to coordinate across separate deployments, and a function in the attendance module can call a function in the login module directly, in the same process, with no network involved at all. The trouble starts as the app grows in three specific ways: more students using it at once, more features being built by more people, and more variation in how much load each feature needs at different times of day. SchoolConnect hit all three within a month of launch.
When One Program Becomes One Point of Failure
Here is precisely what went wrong at 12:30 pm. Because all four features run inside the same process, they share the same fate. When the canteen-ordering code hit an unhandled error — trying to read the price of an item that had just sold out — that error crashed the single process the entire application was running in. It didn't matter that the attendance-marking code itself had no bug. It was sharing a house with a feature that did, and when that house caught fire, everyone inside was affected, not just the room where the fire started.
This is the core structural weakness of a monolith at scale: fault isolation is impossible by default, because "isolation" would require the features to not share a process, a memory space, or a deployment — and in a monolith, they share all three, on purpose. The second weakness is about scaling, which we'll work through with real numbers shortly. The third is about team coordination: once you have, say, four different teams each owning a feature, they're all editing the same codebase and all have to test and release together, so one team's unfinished, buggy work-in-progress can block or break everyone else's finished feature from shipping.
What Is a Microservice, Really?
The fix professional engineering teams reach for is microservices architecture: instead of one program containing four features, you build four small, separate programs — each one runs as its own process, on its own server (or its own slice of a server), with its own database, and each is deployed independently of the others. A "Login Service" only knows how to authenticate students. An "Attendance Service" only knows how to record and fetch attendance. They don't share code, they don't share a process, and critically, they don't share a database. When one needs information from another — say, the Attendance Service needs to confirm a student ID is valid before recording attendance — it doesn't call a function directly. It sends a request over the network to the other service and waits for a response, exactly the way your browser sends a request to a website and waits for the page to come back.
That request usually takes the form of an API call. An API (Application Programming Interface) is a defined contract: a set of URLs (called endpoints), the inputs each one expects, and the output it promises to return — without exposing how it computes that output internally. The most common style for microservices is REST over HTTP: each endpoint is a URL, and the request uses an HTTP method such as GET (fetch data, no side effects) or POST (create or change something), with data usually formatted as JSON. So instead of your Order code calling checkStock(item) as a local function, it sends GET /inventory/check?items=samosa,juice to the Inventory Service, over the network, and gets back a JSON response like {"samosa": 12, "juice": 30}.
This single change — network call instead of function call, separate database instead of shared database — is the entire definition of the architecture. Everything else (independent scaling, independent deployment, fault isolation) is a consequence of that one design decision, not a separate feature you bolt on.
Redesigning SchoolConnect as Microservices
If we rebuild SchoolConnect as microservices, we'd split it into (at least) a Login Service, an Attendance Service, a Library Service, and a Canteen Service, each with its own database, sitting behind a single API Gateway — a front door that the student's phone app talks to, which then routes each request to the correct backend service. The phone app never needs to know there are four separate services running on four separate machines; it just talks to the gateway, and the gateway does the routing. This is exactly analogous to a shopping complex with independent shops versus one giant department store: if the bakery shuts its shutters because of an electrical fault, the vegetable shop next door keeps trading completely normally, because they were never wired to the same circuit in the first place.
How Services Talk to Each Other: Tracing a Real Request
Splitting an app into services only works if they can cooperate to complete tasks that touch more than one of them. Placing a canteen order is a perfect example, because it genuinely needs two services: the Inventory Service (does the school still have a samosa and a juice in stock?) and the Payment Service (does the student have enough balance, and can we deduct it?). Below is a simplified simulation of that flow in Python. In a real deployment, each function marked "service" would actually be a separate program on a separate server, and the calls between them would be HTTP requests, not direct function calls — but simulating them as functions lets us trace the exact logic and data each service is responsible for, which is the part that matters for understanding the architecture.
inventory_db = {"samosa": 12, "juice": 30}
student_balance_db = {"9B047": 85}
menu_prices = {"samosa": 25, "juice": 20}
def inventory_service_check(items):
return {item: inventory_db.get(item, 0) for item in items}
def inventory_service_decrement(items):
for item in items:
inventory_db[item] -= 1
return inventory_db
def payment_service_balance(student_id):
return student_balance_db.get(student_id, 0)
def payment_service_debit(student_id, amount):
student_balance_db[student_id] -= amount
return student_balance_db[student_id]
def order_service_place_order(student_id, items):
stock = inventory_service_check(items)
for item in items:
if stock[item] < 1:
return {"status": "failed", "reason": item + " out of stock"}
total_cost = sum(menu_prices[item] for item in items)
balance = payment_service_balance(student_id)
if balance < total_cost:
return {"status": "failed", "reason": "insufficient balance"}
new_balance = payment_service_debit(student_id, total_cost)
inventory_service_decrement(items)
return {"status": "confirmed", "cost": total_cost, "new_balance": new_balance}
result = order_service_place_order("9B047", ["samosa", "juice"])
print(result)
Trace it exactly as the interpreter would. order_service_place_order is called with student_id="9B047" and items=["samosa","juice"]. First it calls inventory_service_check, which builds a dictionary by looking up each item in inventory_db: {"samosa": 12, "juice": 30}. The loop checks each item's stock is not below 1 — 12 and 30 both pass, so we don't return early. total_cost is computed as menu_prices["samosa"] + menu_prices["juice"] = 25 + 20 = 45. Next it calls payment_service_balance("9B047"), which looks up the student in student_balance_db and returns 85. Since 85 is not less than 45, we proceed instead of failing. payment_service_debit is called with amount=45: it subtracts 45 from the stored balance, so student_balance_db["9B047"] becomes 40, and that value is returned as new_balance. Then inventory_service_decrement reduces inventory_db["samosa"] to 11 and inventory_db["juice"] to 29. Finally the function returns {"status": "confirmed", "cost": 45, "new_balance": 40}, which is exactly what print(result) displays: {'status': 'confirmed', 'cost': 45, 'new_balance': 40}.
Notice the order of operations matters for correctness: stock is checked before money is deducted, and money is deducted before stock is decremented, so a student is never charged for an item that turns out to be unavailable. In a real network deployment, each of those four calls — inventory check, balance check, debit, decrement — is a separate HTTP round-trip, for example GET /inventory/check?items=samosa,juice, GET /students/9B047/balance, POST /students/9B047/debit, and POST /inventory/decrement. If each round-trip takes roughly 50 milliseconds, this single order takes at least 200 milliseconds just in network time, before any actual processing — time a monolith's equivalent function calls would spend in microseconds, because they never leave the process. That's the real cost of splitting a system apart, and it's worth remembering precisely because the rest of this chapter is about why it's still often worth paying.
Scaling Only What Needs Scaling: The Lunch Rush Problem
Now let's return to the scaling problem with numbers you can actually work through. Suppose each server SchoolConnect runs on can reliably handle 50 requests per minute before it starts slowing down — a simplified capacity figure for this example, not a real benchmark. During an ordinary class period, all four features combined generate about 100 requests per minute (attendance checks, the occasional library search, students glancing at their login status). At 100 ÷ 50 = 2, you need 2 servers. In the monolith, "2 servers" means two complete, identical copies of the entire application — login code, attendance code, library code, and canteen code all duplicated on both machines, because you cannot deploy a fraction of a monolith.
Now consider the 30-minute lunch window, 12:30 to 1:00 pm, when all 1,200 students try to order food. Each order involves roughly four separate actions — browsing the menu, adding an item, checking out, and confirming payment — so that's 1200 × 4 = 4800 requests inside 30 minutes, or 4800 ÷ 30 = 160 requests per minute from canteen ordering alone. Meanwhile, because classes are paused, login/attendance/library traffic drops to almost nothing, say 10 requests per minute. Total load during lunch: 160 + 10 = 170 requests per minute.
In the monolith, 170 ÷ 50 = 3.4, rounded up to 4 servers — and again, each of those 4 servers is running a full copy of the entire application, including login and library code that almost nobody is touching during those 30 minutes. For the rest of the six-hour school day, you either keep paying for 4 idle full copies, or you scale back down to 2 and scale up again tomorrow at 12:30 — extra engineering work either way, to solve a problem caused by exactly one feature.
In microservices, the same 170 requests per minute are handled completely differently, because each service scales on its own. The Canteen Service alone needs 160 ÷ 50 = 3.2, rounded up to 4 instances — but each instance is a small, single-purpose program that only knows how to handle orders, nothing else. The Login, Attendance, and Library Services stay at 1 instance each, comfortably covering their combined 10 requests per minute. You've scaled precisely the part of the system under real load, and every instance you're running and paying for is doing genuinely necessary work — none of it is idle login or library code along for the ride.
Two Misconceptions Worth Correcting
The first misconception is that microservices are just "organizing your code into separate files or folders." They're not — that's called a modular monolith, and it's a perfectly reasonable intermediate step, but it is still one process, one deployment, and usually one database, so it still shares the exact failure mode we started this chapter with: a bug anywhere can crash the whole thing, because everything still runs together. What makes something a genuine microservice is independent deployability (you can update or restart the Canteen Service without touching the others) combined with independent data ownership (the Canteen Service has its own database that no other service reads or writes directly) and network-based communication (services call each other's APIs, not each other's functions). Folder structure has nothing to do with it.
The second misconception is that splitting into services makes crashes disappear entirely. It doesn't — it changes which crashes propagate. Recall the order-placing trace: the Order Service directly depends on the Inventory Service and the Payment Service. If the Payment Service goes down, every order attempt fails, even though the Order Service's own code is fine — the failure has simply moved one hop along the dependency chain instead of vanishing. What genuinely stops propagating are failures in services that aren't in the dependency path: if the Library Service crashes, it cannot affect canteen ordering, because Canteen never calls Library. Fault isolation in microservices is real, but it follows the shape of your dependency graph — it isn't a blanket guarantee, and drawing that dependency graph correctly is a real design skill, not an automatic side effect of using the architecture.
The Price You Pay: Trade-offs of Microservices
Weigh this honestly, because it's the part students most often skip. Microservices genuinely give you independent scaling (the lunch-rush numbers above), fault isolation along the dependency graph, independent deployment (the canteen team can ship a menu update on a Tuesday without waiting for the library team's unrelated feature to be ready), and the freedom to let different services use different technology suited to their own job — an inventory service doing simple key lookups doesn't need the same database engine as an attendance service doing structured reporting.
Against that, you're paying real costs. Every cross-service action now involves network calls that are slower than function calls, and can fail in ways a function call never does — a request can time out, arrive twice, or get no response at all, and your code has to handle every one of those cases explicitly. Data that used to live in one shared database, joinable with a single query, now lives in several separate databases that have to be kept in sync deliberately — if the Canteen Service records a sale but the Analytics Service that reports daily revenue hasn't been told yet, the two will briefly disagree, a state engineers call eventual consistency. And you now have several small programs to build, deploy, and monitor instead of one, which is real operational overhead that a two-person team building a single school project usually cannot justify. For a small app with one team and steady, low traffic, a monolith remains the correct engineering choice — microservices earn their cost only once you have the scale, team size, or load variation to actually need independent scaling and independent deployment.
Where This Fits in Your CBSE Syllabus
Your Computer Science / Informatics Practices coursework introduces client-server communication — a client sending a request to a server and receiving a response — as the foundation for how any networked application works. Microservices architecture is that same idea applied recursively: instead of one client talking to one server, you have a client talking to a gateway, which itself becomes a client making requests to several servers (the individual services) behind it. If you can trace a client-server request correctly, you already have the core mental model needed to trace the four-hop order-placing flow worked through above — the only new idea is that "the server" is no longer a single program.
Check Your Understanding
- A different app has a baseline load of 220 requests per minute, and each server handles 40 requests per minute reliably. How many servers does a monolithic deployment need at baseline? Show the division and round correctly.
- In the SchoolConnect order-placing code, list the four service calls made by
order_service_place_order, in the exact order they execute, and explain in one sentence why the order matters. - At 2:00 pm, the Library Service crashes. Of these three actions — (a) a student marking attendance, (b) a student searching the library catalogue, (c) a student topping up their canteen balance — which ones fail, and which keep working? Justify your answer using the dependency-graph idea from the misconceptions section, not just intuition.
- A classmate is building a three-page personal blog (home page, about page, contact form) as a school project and asks whether they should use microservices. Give your recommendation and justify it using at least two of the specific trade-offs discussed above.
- Explain, in your own words, why "splitting code into separate files" is not sufficient to call something a microservice. Name the three properties that actually define one.
Summary
A monolithic architecture bundles every feature of an application into one process, one deployment, and typically one shared database — simple to build first, but structurally unable to isolate failures or scale one feature independently of the rest, because everything shares the same runtime. A microservices architecture splits an application into small, independently deployable services, each owning its own database and communicating with the others over the network through defined APIs, commonly REST over HTTP with JSON payloads, routed through a shared API Gateway. This buys you targeted scaling (only the overloaded service gets more instances, as SchoolConnect's canteen rush demonstrated with real numbers), fault isolation along the actual dependency graph (not universally — a service still fails if something it depends on fails), and independent deployment for separate teams. It costs you network latency and failure handling on every cross-service call, harder cross-service data consistency, and meaningfully more operational complexity — costs that only pay for themselves once an application has genuinely outgrown what a single well-organized codebase can handle.
Think About It
Think about this: How would you explain microservices architecture: building scalable apps to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.