AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Grade 9 AI & Computer Science Practice Questions — Set 10

20 questions from the Grade 9 bank, each with its answer and a full explanation. Set 10 of 11 · 221 questions in this grade.

Reading is revision; testing is practice. Take the same questions as a timed quiz →

Question 181 · Random Forests: Ensemble Learning Power · hard

A random forest built to flag suspicious UPI transactions consists of 5 decision trees, each trained on an independent bootstrap sample of the transaction data. Each tree, on its own, correctly classifies a given transaction with probability 0.6, and you may assume the trees' errors are independent of one another. The forest's final prediction is decided by majority vote: it is correct whenever 3, 4, or 5 of the 5 trees vote correctly. What is the probability that the forest's majority-vote prediction is correct?

  1. 60%, since combining several independent trees does not change the overall accuracy beyond what any single tree already achieves.
  2. 34.56%, the probability that exactly 3 of the 5 trees vote correctly, since that is the smallest majority possible.
  3. 68.26%, obtained by summing the binomial probabilities of exactly 3, 4, and 5 correct trees out of 5.
  4. 98.98%, the probability that at least one of the 5 trees classifies the transaction correctly.

Answer: C. 68.26%, obtained by summing the binomial probabilities of exactly 3, 4, and 5 correct trees out of 5.

ExplanationBecause the five trees are independent, the number of trees that vote correctly follows a binomial pattern with n = 5 and p = 0.6. Majority vote succeeds when at least 3 of the 5 trees are correct, so the required probability is P(X=3) + P(X=4) + P(X=5). P(X=3) = C(5,3) × 0.6³ × 0.4² = 10 × 0.216 × 0.16 = 0.3456 P(X=4) = C(5,4) × 0.6⁴ × 0.4¹ = 5 × 0.1296 × 0.4 = 0.2592 P(X=5) = C(5,5) × 0.6⁵ × 0.4⁰ = 1 × 0.07776 = 0.07776 Adding these: 0.3456 + 0.2592 + 0.07776 = 0.68256, i.e. 68.26% — noticeably higher than any single tree's 60% accuracy. This is exactly the "wisdom of crowds" effect that makes random forests powerful: even trees that are only modestly better than a coin flip combine into a strong ensemble, as long as their errors are reasonably independent — which bootstrap sampling (each tree sees a different random subset of transactions) and random feature selection at each split are specifically designed to encourage. Claiming the ensemble's accuracy stays at 60% ignores this averaging-out of independent errors completely — that averaging is the entire reason forests outperform individual trees. Stopping the calculation at exactly 3 correct trees (34.56%) undercounts the majority condition, since 4-correct and 5-correct outcomes are majorities too and must be added in. And computing the chance that at least one tree is correct (98.98%) answers a different question altogether — a random forest doesn't accept a single tree's vote as final; it requires agreement from more than half the trees, not just one.

Question 182 · Overfitting: Detecting and Solving the Problem · hard

A CBSE Class 9 project team in Pune trains a small neural network to flag suspicious UPI transactions as fraud or genuine. They checkpoint the model at four points during training and record both training and validation accuracy: | Epoch | Training Accuracy | Validation Accuracy | |-------|--------------------|-----------------------| | 5 | 82% | 81% | | 20 | 91% | 89% | | 50 | 99% | 84% | | 100 | 100% | 78% | Based on this table, at which checkpoint should the team stop training to get the model most likely to generalize well to new UPI transactions, and why?

  1. Stop at epoch 100, since 100% training accuracy shows the network has completely learned the true patterns that separate fraud from genuine transactions.
  2. Stop at epoch 20, since validation accuracy peaks there at 89% before falling in later epochs even as training accuracy keeps climbing — the widening gap after epoch 20 is the signature of overfitting.
  3. Stop at epoch 50, since averaging the 99% training accuracy and 84% validation accuracy gives the highest combined score of any checkpoint in the table.
  4. Stop at epoch 5, since the 1-point gap between 82% training and 81% validation accuracy is the smallest of any checkpoint, and a smaller gap always means better generalization.

Answer: B. Stop at epoch 20, since validation accuracy peaks there at 89% before falling in later epochs even as training accuracy keeps climbing — the widening gap after epoch 20 is the signature of overfitting.

ExplanationEpoch 20 is the last checkpoint where training and validation accuracy rise together (82%→91% training, 81%→89% validation). After that, training accuracy keeps climbing to 99% and then 100%, but validation accuracy falls to 84% and then 78% — the network is starting to memorize quirks of the specific training transactions rather than learning the general fraud pattern, which is exactly what overfitting looks like on a train/validation curve. Stopping at the checkpoint where validation accuracy is highest (89% at epoch 20) — a technique called early stopping — gives the model most likely to perform well on UPI transactions it has never seen. Training accuracy alone is not a reliable stopping signal: reaching 100% training accuracy at epoch 100 usually means the model has fit noise in the training set, not the underlying signal, which is why its validation accuracy is the worst of the four checkpoints (78%). Averaging training and validation accuracy, as at epoch 50, is not a meaningful metric — validation accuracy alone is what measures generalization, and 84% at epoch 50 is already lower than the 89% achieved at epoch 20. And while epoch 5 does have the smallest train-validation gap (82% vs 81%, a 1-point difference), both numbers are low compared to what the network can reach — the model is still underfitting at that point, so a small gap by itself doesn't mean the best choice. What matters is comparing actual validation accuracy across checkpoints, and that is highest at epoch 20.

Question 183 · Docker Compose: Orchestrating Multiple Containers · hard

A CBSE Class 9 student is containerizing a two-service college project — a Flask-based UPI payment simulator (`web`) that talks to a Postgres database (`db`) — using this `docker-compose.yml`: ```yaml version: "3.9" services: db: image: postgres:15 environment: POSTGRES_PASSWORD: secret web: build: . ports: - "8000:5000" depends_on: - db environment: DB_HOST: db ``` When she runs `docker compose up`, which statement about this setup is correct?

  1. Compose starts `db` before `web` because of `depends_on`, but it only sequences container start order — it does not wait for PostgreSQL to finish booting, so `web` can still fail its first connection attempt if the app doesn't retry.
  2. The default PostgreSQL image ships with a built-in healthcheck that `depends_on` automatically waits on, guaranteeing `web` never starts before the database is ready to accept connections.
  3. Visiting `http://localhost:5000` is how you'd reach the app from the host browser, since the right-hand number in a `"8000:5000"` ports mapping is always the host-side port.
  4. Without an explicit `links:` entry connecting `web` and `db`, the `DB_HOST: db` environment variable will fail to resolve, because Compose does not create a shared network between services automatically.

Answer: A. Compose starts `db` before `web` because of `depends_on`, but it only sequences container start order — it does not wait for PostgreSQL to finish booting, so `web` can still fail its first connection attempt if the app doesn't retry.

Explanationdepends_on only sequences the order in which Compose starts the containers — it tells Docker to launch `db` first and then `web`, but it has no idea whether PostgreSQL inside `db` has actually finished its startup routine and opened port 5432 for connections. That's why, without a `healthcheck` on `db` plus `depends_on: db: condition: service_healthy` on `web`, the Flask app can boot before Postgres is truly ready, and its very first connection attempt can throw a "connection refused" error — real code for a project like this needs a retry-with-backoff loop for exactly this reason. The official `postgres:15` image doesn't ship a built-in HEALTHCHECK either, so there's nothing for a plain `depends_on: - db` to wait on even if it wanted to — that rules out the healthcheck claim entirely. Separately, the ports mapping `"8000:5000"` follows Compose's `HOST:CONTAINER` convention, so the student's browser must hit `http://localhost:8000` on her own machine, not 5000 — 5000 is only the port Flask listens on inside the container. And Compose automatically creates a private bridge network for every project and registers each service's name in its embedded DNS, so `web` can already resolve `db` as a hostname purely from being defined in the same `docker-compose.yml` — the old `links:` directive has been unnecessary for this since Compose file version 2.

Question 184 · CI/CD Pipelines: Automating Build and Deployment · hard

A team of B.Tech students in Bengaluru is building a UPI-based expense-splitting app. Their CI/CD pipeline runs four stages in strict order for every push — Build, Test, Security Scan, Deploy — taking 2, 5, 3, and 1 minutes respectively when nothing is cached. They configure build caching so that the Build stage only reinstalls dependencies (the full 2 minutes) when package.json changes; if only application source files change, the cached dependencies are reused and Build finishes in just 30 seconds. On a Monday afternoon, three commits are pushed one after another, each automatically triggering its own complete pipeline run: Commit 1 modifies package.json to add a new payment-gateway library, Commit 2 only edits a React component file, and Commit 3 only edits a utility function file. Assuming every stage in all three runs passes without failure, what is the total time spent specifically in the Build stage across all three pipeline runs?

  1. 3 minutes — Commit 1 needs the full 2-minute build since package.json changed, while Commits 2 and 3 each reuse the cache for a 30-second build, giving 2 + 0.5 + 0.5.
  2. 6 minutes — every pipeline run performs a full, uncached build regardless of which files changed, so each of the three runs takes 2 minutes.
  3. 4.5 minutes — the dependency cache is only rebuilt one run after package.json changes, so Commit 2 still triggers a full 2-minute build and only Commit 3 gets the cached 30-second build.
  4. 1.5 minutes — since package.json was modified alongside source files in the same working session, the cache is treated as valid from the very first commit, giving three cached 30-second builds.

Answer: A. 3 minutes — Commit 1 needs the full 2-minute build since package.json changed, while Commits 2 and 3 each reuse the cache for a 30-second build, giving 2 + 0.5 + 0.5.

ExplanationBuild caching in a CI/CD pipeline is keyed on whether the dependency manifest changed, not on how recently it changed or how many files were touched in a session. Commit 1 modifies package.json to add the payment-gateway library, so that run must reinstall dependencies from scratch, taking the full 2 minutes. Commit 2 and Commit 3 each modify only source files (a component and a utility function) with no change to package.json, so both of those independent runs find a valid dependency cache and finish Build in 30 seconds (0.5 minutes) apiece. Summing the three separate pipeline runs gives 2 + 0.5 + 0.5 = 3 minutes total. Cache invalidation is immediate and evaluated fresh for each run: it never lingers into a later commit as a phantom full rebuild, and it can never apply to the very commit that changed package.json itself, since that commit is exactly what makes the existing cache stale.

Question 185 · Git Branching: Organizing Team Development · hard

A four-member team at an Indian ed-tech startup is building a UPI payment-reconciliation module in a shared Git repository. The `main` branch history looks like this before anyone branches: ``` main: M1 -- M2 -- M3 ``` At commit `M3`, Priya creates a branch `payment-fix` and adds two commits, `P1` and `P2`, to it. While she is still working, Arjun commits directly to `main` (based on `M3`), adding commit `M4`: ``` main: M1 -- M2 -- M3 -- M4 payment-fix: \-- P1 -- P2 ``` Priya finishes her work, checks out `main`, and runs `git merge payment-fix`. Neither branch has touched the same lines of code, so there are no conflicting changes. What does Git do to complete this merge, and why?

  1. Git fast-forwards `main` straight to `P2`, since a branch created from `M3` is assumed to already include any commits added later on `main`
  2. Git creates a new merge commit with two parents, `M4` and `P2`, because `main` advanced with a commit `payment-fix` doesn't have, so neither branch's tip is an ancestor of the other
  3. Git rejects the merge command and forces Priya to run `git rebase main` on `payment-fix` first, since the two branches diverged after `M3`
  4. Git creates a merge commit that records only one parent, `P2`, because `M4`'s changes are discarded in favor of the more recently branched work

Answer: B. Git creates a new merge commit with two parents, `M4` and `P2`, because `main` advanced with a commit `payment-fix` doesn't have, so neither branch's tip is an ancestor of the other

ExplanationA fast-forward merge only happens when the current branch's tip is already an ancestor of the branch being merged in — meaning `main` hasn't moved since the other branch split off. That was true right when `payment-fix` was created at `M3`, but Arjun's commit `M4` moved `main` forward on its own, so `main`'s tip is no longer reachable from `payment-fix`'s history at all. With both branches holding commits the other lacks, there's no single straight line Git can just slide the pointer along, so it falls back to a three-way merge: it compares the common ancestor `M3` against `M4` and `P2`, combines the (non-conflicting) changes from each, and wraps the result in a brand-new commit with two parent links — one to `M4`, one to `P2` — recording both lines of development. That two-parent merge commit is exactly what shows up as the two branches rejoining when you run `git log --graph`. Git never refuses a plain merge just because branches diverged — rebasing first is a stylistic choice some teams make for a cleaner, linear history, not something Git demands before it will combine two branches. And a merge commit's whole purpose is to preserve every parent it merges: neither `M4`'s nor `P2`'s work gets silently dropped, which is precisely why feature branches like `payment-fix` let a team keep shipping on `main` and on a side branch at the same time without anyone's commits vanishing.

Question 186 · Resolving Git Merge Conflicts · hard

Aisha and Rohan are both building ExamMitra, a JEE-style mock-test app. They branched off the same commit of `quiz_engine.py`, whose original (common-ancestor) version was: ```python def calculate_score(correct, total): return (correct / total) * 100 def grade_level(score): return "Pass" ``` On her branch `add-negative-marking`, Aisha changed only `calculate_score` (to subtract 0.25 marks per wrong answer, as in JEE): ```python def calculate_score(correct, total): return ((correct - 0.25 * (total - correct)) / total) * 100 def grade_level(score): return "Pass" ``` On his branch `add-grade-bands`, Rohan changed only `grade_level` (leaving `calculate_score` untouched): ```python def calculate_score(correct, total): return (correct / total) * 100 def grade_level(score): if score >= 90: return "A+" elif score >= 75: return "A" else: return "B" ``` Aisha checks out `add-negative-marking` and runs `git merge add-grade-bands`. What actually happens, and why?

  1. Git merges the branches automatically, producing a single quiz_engine.py that contains both the negative-marking formula in calculate_score and the new grade-band logic in grade_level — with no conflict markers at all.
  2. Git stops the merge and inserts <<<<<<<, =======, >>>>>>> conflict markers around both functions, because quiz_engine.py was independently modified on both branches.
  3. Git refuses to merge and reports "fatal: refusing to merge unrelated histories", since calculate_score and grade_level were changed by two different developers.
  4. Git completes the merge but keeps only Aisha's calculate_score edit and silently discards Rohan's grade_level changes, because the branch currently checked out (add-negative-marking) takes priority over the incoming branch.

Answer: A. Git merges the branches automatically, producing a single quiz_engine.py that contains both the negative-marking formula in calculate_score and the new grade-band logic in grade_level — with no conflict markers at all.

ExplanationGit's three-way merge doesn't compare files as whole blobs — it compares each branch against the common ancestor line by line (a diff3-style algorithm) and asks: did the two branches touch the same lines? Here, Aisha's edit is confined to the lines inside calculate_score, and Rohan's edit is confined to the lines inside grade_level. There's unchanged context between the two functions on both sides, so the two sets of changed lines never overlap. Because the edits sit on disjoint line ranges, git can apply both patches to the common ancestor independently and stitch the results together — the merged quiz_engine.py ends up with Aisha's negative-marking formula AND Rohan's grade-band logic, committed automatically with no conflict markers. A real merge conflict (with <<<<<<<, =======, >>>>>>> markers) only appears when both branches edit the *same* lines relative to the ancestor — for example, if both Aisha and Rohan had rewritten the return statement inside calculate_score. Editing the same file is not what triggers a conflict; editing the same lines is. "fatal: refusing to merge unrelated histories" is a genuine git error, but it fires when two branches share no common commit at all (e.g., two separately initialized repositories being merged with --allow-unrelated-histories) — not when they diverged from a shared ancestor, as Aisha's and Rohan's branches did. And git's merge never silently favors whichever branch is currently checked out; if it can't safely combine changes, it pauses and asks the developer to resolve the conflict by hand rather than discarding anyone's work.

Question 187 · Microservices Architecture: Building Scalable Apps · hard

IRCTC splits its train-ticket booking flow into four independent microservices — Train Search, Seat Availability, Payment, and Booking Confirmation. To confirm one ticket, a request must pass through all four services one after another (synchronously), and each service, on its own, is up and responding 99% of the time. Assuming the four services fail independently of each other, what is the probability that a single booking request makes it successfully through the entire chain?

  1. About 96.06% — since the four services form a sequential chain, the whole request's reliability is the product of each link's individual reliability: 0.99 × 0.99 × 0.99 × 0.99.
  2. Exactly 99% — since each microservice individually has 99% uptime, the whole booking pipeline simply inherits that same number no matter how many services are chained together.
  3. Nearly 100% (about 99.9999999%) — treating the four services as if they were redundant backups of each other, so the booking would fail only if all four happened to go down at the exact same instant.
  4. About 92.27% — treating each request-response round trip as two separate reliability checks (the outgoing call and the acknowledgment), which doubles the effective chain to eight links: 0.99 raised to the 8th power.

Answer: A. About 96.06% — since the four services form a sequential chain, the whole request's reliability is the product of each link's individual reliability: 0.99 × 0.99 × 0.99 × 0.99.

ExplanationWhen services are chained synchronously — each one waiting on the previous one to succeed before it can even be called — the pipeline is only as reliable as ALL of its links working together, not just any single one of them. Because the four services fail independently, you multiply their individual availabilities: 0.99 × 0.99 = 0.9801 after two services, × 0.99 = 0.970299 after three, and × 0.99 = 0.96059601 after all four — about 96.06%. Notice this is noticeably lower than any individual service's own 99% figure, even though nothing "extra" went wrong — the drop comes purely from chaining four imperfect links in series, the same reason a monolith (effectively one link) doesn't suffer this compounding at all. This is precisely why real microservices systems at scale — IRCTC, UPI switches, e-commerce checkouts — can't just chain synchronous calls and hope for the best: they add retries, circuit breakers, caching, and asynchronous message queues specifically to stop this multiplicative reliability loss from eating into the user-facing uptime as more services get added to a request path.

Question 188 · Caching Strategies: Performance Optimization · hard

A train-ticket app keeps an in-memory cache of the last few searched source-destination route codes so repeat searches skip the database. The cache holds exactly 3 route codes and uses a Least Recently Used (LRU) eviction policy: when the cache is full and a new code arrives, whichever code has gone the longest without being read or re-searched is evicted, and every cache hit moves that code to the most-recently-used end. A user searches route codes in this order: ``` A, B, C, A, D, E, A, C ``` How many of these 8 searches are cache hits, and what hit ratio does that give?

  1. 1 hit and a 12.5% hit ratio, since A is evicted before its second request because D and E fill the cache in insertion order
  2. 2 hits and a 25% hit ratio, since A remains cached at the 4th request and again at the 7th request after being refreshed to the most-recently-used position each time
  3. 3 hits and a 37.5% hit ratio, since A and C were both searched earlier and therefore stay available in cache for their later repeat requests
  4. 2 hits, but only out of the 5 requests made after the cache first fills up, giving a 40% hit ratio

Answer: B. 2 hits and a 25% hit ratio, since A remains cached at the 4th request and again at the 7th request after being refreshed to the most-recently-used position each time

ExplanationTrace the LRU cache (capacity 3, most-recently-used end written first) request by request. A, B, C all miss and fill the cache as [C, B, A]. The next request, A, is already cached — a hit — and moves to the front: [A, C, B]. D misses and evicts the least-recently-used entry, B, leaving [D, A, C]. E misses and evicts C, leaving [E, D, A]. The next A is still cached — a second hit — and moves to the front: [A, E, D]. Finally C misses, because C was evicted back when E arrived. That's 2 hits out of all 8 searches, a 25% hit ratio. A FIFO-style trace (evicting by insertion order and never refreshing an entry's position on a hit) only scores 1 hit: A would already be queued for eviction by the time it's searched again, since FIFO doesn't move it to the back on that hit. Treating the cache as unlimited (nothing ever evicted) would incorrectly count 3 hits, because the second search for C would also register as a hit even though a real 3-slot cache had already dropped it. And computing the ratio only over the 5 requests that come after the cache first fills (positions 4 through 8) — instead of all 8 searches, hits and misses alike — produces 40% rather than the correct 25%.

Question 189 · Load Balancing: Distributing Traffic · hard

IRCTC's Tatkal booking system sits behind a load balancer that uses the "least connections" algorithm across three backend servers — A, B, and C — routing each new request to whichever server currently has the fewest active connections, with ties broken by choosing the server whose name comes first alphabetically. At the instant Tatkal booking opens, the servers already hold A = 4, B = 2, and C = 5 active connections from earlier ticket searches. Four new booking requests then arrive one after another, with no existing connection closing in between. In what order are these four requests routed, and what do the final connection counts look like?

  1. B receives requests 1 and 2, A receives request 3 by winning an alphabetical tie-break, and B receives request 4 — final loads end tied at A=5, B=5, C=5.
  2. A, B, C, and A receive requests 1 through 4 respectively, since a fixed round-robin cycle ignores each server's current connection count.
  3. C receives all four requests in a row, because this balancer sends new traffic to whichever server already has the most active connections.
  4. B, B, B, and A receive requests 1 through 4 respectively, because ties for fewest connections are broken by picking the alphabetically later server name.

Answer: A. B receives requests 1 and 2, A receives request 3 by winning an alphabetical tie-break, and B receives request 4 — final loads end tied at A=5, B=5, C=5.

ExplanationTrace each request step by step against the actual connection counts, not a fixed cycle. Before any new traffic arrives, B already holds the fewest connections (2, versus A's 4 and C's 5), so request 1 goes to B, bringing it to 3. Request 2 checks the counts again — B is still lowest at 3 — so B receives it too and rises to 4. Now A and B are tied at 4 connections each; the alphabetical tie-break rule sends request 3 to A, which climbs to 5. For request 4 the counts stand at A=5, B=4, C=5, so B is once again the least-loaded server and takes the request, finishing at 5. All three servers end up tied at 5 active connections apiece — least-connections balancing naturally evens out load over time, even when servers start unevenly loaded. Cycling A, B, C, A regardless of load describes round-robin balancing, a different algorithm that ignores current connection counts entirely; that sequence would only be correct if the balancer were round-robin, not least-connections. Sending every request to C models the opposite of what "least connections" means — routing to the already-busiest server would pile up load exactly where the system is struggling, defeating the point of load balancing. Breaking the A–B tie in favor of B (the alphabetically later name) reverses the stated tie-break rule and produces a different, incorrect routing sequence.

Question 190 · WebSocket Advanced: Building Real-time Systems · hard

You are building a live IRCTC train-delay alert system for 10,000 commuters. To handle the load, your WebSocket backend runs behind a load balancer with three identical Node.js server instances (A, B, and C), and each instance keeps its own in-memory list of the client sockets currently connected to it. A commuter connected to instance A submits a new delay update, and your code on instance A loops through its own socket list and calls socket.send() on each one. Assuming no other infrastructure is added, what actually happens to the update?

  1. Only the commuters currently connected to instance A receive the update; commuters connected to instances B and C never see it, because each instance's socket list only tracks its own connections
  2. All 10,000 commuters receive the update instantly, because the load balancer automatically mirrors every WebSocket message across instances A, B, and C
  3. Every one of the 10,000 commuters eventually receives the update, but those on instances B and C experience extra latency while it is relayed to them over a separate HTTP request as a workaround
  4. None of the 10,000 commuters receive the update, including those on instance A, because a WebSocket send always requires a shared session store to authorize delivery

Answer: A. Only the commuters currently connected to instance A receive the update; commuters connected to instances B and C never see it, because each instance's socket list only tracks its own connections

ExplanationEach Node.js instance's socket list is process-local, in-memory state — a plain array or map living in that process's RAM. When a commuter's browser opens a WebSocket connection, that connection terminates at exactly one backend process (say instance A) and stays open there for the lifetime of the session; instance B and instance C never get a handle to that socket, so they cannot write to it even if they wanted to. So when the update handler on instance A loops over "its" socket list and calls socket.send(), it can only reach the commuters whose long-lived WebSocket connections happen to be pinned to instance A. The 6,000-odd commuters (roughly two-thirds of 10,000, split across B and C) simply never receive the message — not late, not eventually, just never — because instance A has no code path that even knows they exist. A load balancer's job is to route new incoming connection requests to a backend instance; it does not inspect, duplicate, or relay application-level WebSocket frames sent after the connection is already established, so nothing "mirrors" the send across instances for free, and nothing routes it over a separate HTTP channel either. Solving this for real requires an explicit shared layer that all three instances subscribe to — commonly Redis Pub/Sub — where instance A publishes the update to a channel, and every instance (including A itself) has a subscriber that receives the published message and forwards it to only the sockets it personally holds open. This "each instance only knows its own sockets" limitation is exactly why naive horizontal scaling of WebSocket servers silently breaks broadcast features, and why real-time systems add a pub/sub or message-broker layer as soon as they scale past one server process.

Question 191 · REST API Design: Best Practices · hard

A student is building a train-ticket booking REST API modeled on IRCTC. The app calls POST /bookings to reserve a seat. On patchy station Wi-Fi, the request reaches the server and the booking succeeds, but the success response is lost before it reaches the app. Seeing no response, the app automatically resends the exact same POST request. Which design change correctly stops this retry from creating a second, duplicate booking — while still letting the same passenger deliberately book two separate tickets in two separate, later requests?

  1. Require the client to send a unique Idempotency-Key header value with the POST /bookings request, reuse the exact same key when retrying that same booking attempt, and have the server store each key alongside its result so that a repeated key returns the original booking instead of creating a new one
  2. Switch the operation from POST /bookings to PUT /bookings, because PUT is defined as an idempotent HTTP method, so sending the identical PUT request twice will always produce exactly one booking with no extra design changes needed
  3. Keep POST /bookings unchanged, but have the server generate a random confirmation number for every successful booking and require the app to show "Booking Confirmed" only after it receives that number back
  4. Add rate limiting so the /bookings endpoint accepts at most one POST request per client IP address every 5 seconds, rejecting any request that arrives sooner with a 429 Too Many Requests response

Answer: A. Require the client to send a unique Idempotency-Key header value with the POST /bookings request, reuse the exact same key when retrying that same booking attempt, and have the server store each key alongside its result so that a repeated key returns the original booking instead of creating a new one

ExplanationThe idempotency-key pattern is the standard fix because it targets the actual problem: the server cannot tell "this is a retry of the booking I already made" apart from "this is a brand-new booking request" unless the client tells it so explicitly. By generating one key per logical booking attempt and resending that same key on every retry, the app gives the server enough information to recognize a repeat and hand back the original result instead of reserving a second seat. A genuinely new booking simply gets a new key, so it goes through normally — this is exactly the mechanism payment gateways use for UPI and card transactions in India, where a network drop after a successful debit must not trigger a second debit on retry. Switching to PUT does not solve this on its own. PUT's idempotence guarantee only holds when the client addresses a specific resource URI it controls, such as PUT /bookings/{client-generated-id} — repeating that exact call safely overwrites the same resource. But "PUT /bookings" targets the collection, not a single resource, so the server still has no way to distinguish a retry from a new reservation; renaming the verb without redesigning the URI scheme changes nothing about the duplication risk. Generating a confirmation number after success doesn't help either, because the problem is precisely that the response — and any number in it — never reaches the app. With nothing to compare against, the retried POST looks like a fresh request and still creates a second booking. Rate limiting only throttles how fast requests can arrive; it doesn't inspect whether two requests represent the same booking or two different ones. A retry sent after the cooldown window still slips through and duplicates the seat, while a passenger who legitimately wants two tickets within that window gets wrongly blocked.

Question 192 · Integration Testing: Testing Multiple Components · hard

A team is building a UPI-based bus-ticket booking app with three modules, each already unit-tested in isolation: SeatSelector (lets the user pick a seat), FareCalculator (computes the fare from seat and route data), and PaymentProcessor (talks to the UPI gateway). They choose bottom-up integration: first test PaymentProcessor alone, then combine it with FareCalculator, and only at the very end add SeatSelector on top to form the complete system. SeatSelector is still under development when the FareCalculator + PaymentProcessor combination needs to be tested. What should the testers write so this middle-stage integration test can run and be verified?

  1. Write a test driver that mimics SeatSelector's calls, feeding sample seat and passenger data into the FareCalculator-PaymentProcessor pair and capturing their combined output for verification
  2. Write a stub that mimics SeatSelector's expected output, so PaymentProcessor can call it directly to fetch dummy seat data
  3. Skip straight to testing the full three-module system, since FareCalculator and PaymentProcessor have each already passed unit testing individually
  4. Write unit test cases for FareCalculator and PaymentProcessor separately, then compare their individual outputs by hand to check for compatibility

Answer: A. Write a test driver that mimics SeatSelector's calls, feeding sample seat and passenger data into the FareCalculator-PaymentProcessor pair and capturing their combined output for verification

ExplanationIn bottom-up integration, you build the system from the lowest-level modules upward: PaymentProcessor first, then FareCalculator + PaymentProcessor, then finally SeatSelector on top. At each stage, any module that would normally sit ABOVE the modules currently being tested — and call them — is missing until its own turn comes. Here, SeatSelector is the caller: in the real app it collects the user's seat choice and passes it down into FareCalculator. Since SeatSelector doesn't exist yet at this stage, testers need a piece of throwaway test code called a test driver that stands in for it — a small program that supplies realistic seat/passenger data to FareCalculator, triggers the call chain into PaymentProcessor, and records what comes back so it can be checked against expected results. This is the opposite direction from a stub. A stub simulates a module that is CALLED BY the module under test (used in top-down integration, when a lower module isn't ready yet). Here PaymentProcessor already exists and is fully coded, so there is nothing below FareCalculator that needs faking — the gap is above it, which calls for a driver, not a stub. Skipping to full-system testing would hide exactly the kind of bug integration testing exists to catch: two individually correct modules passing data in incompatible formats (for example, FareCalculator returning fare in paise while PaymentProcessor expects rupees) only shows up once they actually talk to each other, and going straight to the three-module system means any failure could come from any of several interfaces at once, making it far harder to isolate. Re-running unit tests and manually comparing outputs also does not test the interfaces themselves — the actual function calls, parameter passing, and data formats between FareCalculator and PaymentProcessor — which is precisely what integration testing is designed to exercise.

Question 193 · Performance Profiling: Finding Bottlenecks · hard

Aarav is building a CBSE Class 9 AI project that uses OpenCV to detect vehicles from a traffic camera feed and adjust signal timing automatically. His script runs slower than expected, so he profiles it and gets the following statistics for one full run of the program: ``` Function Calls Avg Time/Call (ms) resize_frame() 100,000 0.002 detect_vehicle() 50 3.000 extract_edges() 1,000,000 0.0005 draw_bounding_box() 10 8.000 ``` Which function is the actual bottleneck that Aarav should optimize first, based on the total time it contributes to the program's runtime?

  1. draw_bounding_box(), because at 8 ms per call it has by far the highest average time per individual call, making it the slowest single operation in the program.
  2. extract_edges(), because even though each call takes only 0.0005 ms, its 1,000,000 calls add up to about 500 ms, the largest total contribution of any function.
  3. resize_frame(), because with 100,000 calls it has the second-highest call count, so it must account for the largest share of total runtime.
  4. detect_vehicle(), because at 3 ms per call it performs more work per call than resize_frame() or extract_edges(), so cutting its call count would give the biggest overall speedup.

Answer: B. extract_edges(), because even though each call takes only 0.0005 ms, its 1,000,000 calls add up to about 500 ms, the largest total contribution of any function.

ExplanationThe real cost of a function isn't its per-call speed or its call count in isolation — it's calls x average time per call, which is exactly what a profiler's cumulative-time column reports. Working through each function: resize_frame() contributes 100,000 x 0.002 ms = 200 ms; detect_vehicle() contributes 50 x 3 ms = 150 ms; extract_edges() contributes 1,000,000 x 0.0005 ms = 500 ms; and draw_bounding_box() contributes 10 x 8 ms = 80 ms, for a total runtime of about 930 ms. Even though draw_bounding_box() looks alarming at 8 ms per call, it only runs 10 times, so it barely dents the overall runtime. extract_edges() looks cheap per call, but because it runs a million times it eats up more total time than every other function combined — that's the definition of a bottleneck: whichever piece of code consumes the largest slice of total execution time, not the one that looks slowest or busiest when viewed in isolation. This is exactly why profilers such as Python's cProfile report both a per-call time and a cumulative time — reading only the per-call number, the way a rushed programmer might fixate on draw_bounding_box(), leads to optimizing code that barely matters while the function actually slowing everything down goes untouched.

Question 194 · Accessibility: Building Inclusive Web Apps · hard

A Grade 9 student is building an accessible 'Digital India' scholarship portal and chooses light-grey body text (relative luminance L = 0.15) on a near-white background (relative luminance L = 0.90). Using the WCAG contrast formula, contrast ratio = (L_lighter + 0.05) / (L_darker + 0.05), what is the resulting contrast ratio, and does it satisfy the WCAG AA requirement of at least 4.5:1 for normal body text?

  1. 4.75:1 — this meets the WCAG AA minimum of 4.5:1 for normal text, though it falls short of the stricter AAA level of 7:1.
  2. 4.75:1 — but this fails accessibility standards, since WCAG requires at least 7:1 contrast for all normal body text.
  3. 6.0:1 — found by dividing the luminance values directly (0.90 ÷ 0.15) without the WCAG offset constant, comfortably passing even AAA.
  4. 0.21:1 — found by dividing the darker luminance by the lighter one, meaning the text is nearly invisible against the background.

Answer: A. 4.75:1 — this meets the WCAG AA minimum of 4.5:1 for normal text, though it falls short of the stricter AAA level of 7:1.

ExplanationWCAG defines contrast ratio using relative luminance, always placing the lighter value in the numerator: ratio = (L_lighter + 0.05) / (L_darker + 0.05). Here the background is lighter (L = 0.90) and the body text is darker (L = 0.15), so the ratio is (0.90 + 0.05) / (0.15 + 0.05) = 0.95 / 0.20 = 4.75:1. WCAG AA sets 4.5:1 as the minimum for normal-sized body text, so 4.75:1 clears that bar — but it does not reach the stricter AAA threshold of 7:1, which is a separate, higher enhancement level, not the baseline most production sites are held to. Mixing up the AA and AAA thresholds is one common error; another is dropping the '+0.05' offset built into the formula (it exists so the ratio never divides by zero even against pure black, and skipping it inflates the result to 0.90/0.15 = 6.0); a third is placing the darker luminance on top, which inverts the ratio into a fraction below 1 that has no meaningful place on the WCAG scale.

Question 195 · Progressive Enhancement: Graceful Degradation · hard

Two developers at an Indian Railways-style ticketing site independently built a "PNR Status Checker" for users across the country, including people on older Android phones over patchy 2G/3G in rural areas. Developer A built the checker entirely in JavaScript: a button triggers a `fetch()` call, and JavaScript renders the PNR result inside an empty `<div>`. If the JS bundle fails to load or execute, a `<noscript>` tag shows the message "Please enable JavaScript to check your PNR status." Developer B built the checker as a plain HTML form: ```html <form method="GET" action="/pnr-status"> <input name="pnr" required> <button type="submit">Check Status</button> </form> <div id="results"></div> ``` Submitting it makes the server return a full HTML page containing the PNR result. Developer B then added a JavaScript listener that calls `event.preventDefault()` on submit and instead fetches `/pnr-status` via AJAX, swapping only the `#results` div — but only when that JS executes successfully. A rural user's phone fails to download the JS bundle over a weak connection. What happens in each version, and which one is a genuine example of progressive enhancement?

  1. Developer B's user still gets a complete PNR result through a normal form submission and full-page reload, because the HTML form was already a fully working baseline before JavaScript was layered on top as a shortcut — that layering-on-top-of-a-working-base is what makes it progressive enhancement.
  2. Both versions count equally as progressive enhancement, since both developers anticipated a JavaScript failure and supplied some kind of fallback content or message for that situation.
  3. Developer A's version is the true progressive enhancement, because it delivers the richer, more capable fetch()-driven experience by default and only drops down to a plain message when the browser can't support it.
  4. Neither developer implemented progressive enhancement correctly, because genuine progressive enhancement requires the page to render and behave identically whether or not JavaScript is available.

Answer: A. Developer B's user still gets a complete PNR result through a normal form submission and full-page reload, because the HTML form was already a fully working baseline before JavaScript was layered on top as a shortcut — that layering-on-top-of-a-working-base is what makes it progressive enhancement.

ExplanationTrace what actually happens when the JS bundle fails to download on the rural user's phone. For Developer A: the `<div>` that was meant to hold results stays empty forever, because nothing but JavaScript was ever going to fill it. The `<noscript>` tag fires and shows "Please enable JavaScript to check your PNR status" — a dead end. The user cannot check their PNR at all. Note that a `<noscript>` fallback is not automatically progressive enhancement; it's just an apology screen bolted onto an architecture where the working feature requires JS to exist in the first place. For Developer B: the `<form method="GET" action="/pnr-status">` is standard HTML form submission — it works with zero JavaScript, exactly the way HTML forms worked in 1995. When the JS bundle fails, the browser simply does what forms always do: navigate to `/pnr-status?pnr=...`, and the server sends back a full HTML page with the real result. The user gets their PNR status, just with a full page reload instead of a slick partial update. Only when the JS successfully loads does it intercept the same submission to make the experience nicer (no reload, faster) — but that JS is decoration on top of something that already worked, not the thing the feature depends on. That ordering — real baseline functionality first, JS-powered enhancements layered on second, each layer optional — is the actual definition of progressive enhancement. Graceful degradation is close but inverted: it starts by building the rich version and works backward to make failure less ugly, which is what Developer A attempted with the noscript message, except that message isn't even a degraded version of the feature — it's a refusal. The "both have fallbacks so both count" reasoning fails because a fallback message is not the same as fallback functionality: Developer A's fallback lets you read an apology; Developer B's fallback lets you check your actual PNR. Calling Developer A's approach the "true" progressive enhancement inverts the term entirely — building the advanced version first and hoping the fallback is good enough is graceful degradation's whole premise, not progressive enhancement's. And requiring identical rendering with and without JS is not part of the definition either — Developer B's two paths look different (full reload vs. AJAX swap) and behave differently in speed, yet it is still progressive enhancement because the core capability, seeing your PNR status, survives in both cases.

Question 196 · Security Headers: Protecting Your App · hard

A school's fee-payment page loads a third-party UPI checkout widget and sends this header before any page content: ``` Content-Security-Policy: default-src 'self'; script-src 'self' https://checkout.upipay.in; img-src *; style-src 'self' 'unsafe-inline' ``` Which one of these does the browser actually block?

  1. An inline `<script>...</script>` block written directly inside the page's HTML
  2. An `<img src="https://cdn.example.com/logo.png">` tag pointing to a third-party image host
  3. A `<script src="https://checkout.upipay.in/widget.js">` tag that loads the UPI checkout widget
  4. An inline `style="color:red;"` attribute set directly on a `<div>` element

Answer: A. An inline `<script>...</script>` block written directly inside the page's HTML

ExplanationThe key idea is that once a directive like script-src is explicitly written in a CSP header, it completely replaces default-src for that resource type — the browser no longer falls back to default-src 'self' for scripts at all. So to judge inline scripts, only script-src 'self' https://checkout.upipay.in matters, and neither 'self' nor a listed hostname permits inline code — the browser only allows inline `<script>` blocks when the directive explicitly contains 'unsafe-inline' (or a matching nonce/hash), and this policy has none. So the inline script block gets blocked. The image tag survives because img-src * was written explicitly, and the wildcard permits loading from any host reachable over a network scheme (http/https/ws/wss/ftp), which covers cdn.example.com. The externally hosted widget script survives too, since https://checkout.upipay.in is named directly inside script-src as an allowed origin. The inline style attribute survives because style-src 'self' 'unsafe-inline' explicitly whitelists inline styling, unlike script-src, which never got that keyword. So of the four, exactly one request has no matching allowance anywhere in the policy: the inline script, which is why it is the one the browser refuses to execute.

Question 197 · HTTPS and SSL/TLS: Secure Communication · hard

When your browser connects to IRCTC over HTTPS to book a train ticket, the TLS handshake uses the server's public key for only the first few milliseconds of the connection, and then both sides switch to a completely different kind of key for everything that follows — including your card number and OTP. Why does TLS make this switch instead of just using the public/private key pair for the entire session?

  1. Asymmetric (public/private key) encryption is mathematically expensive to compute, so TLS uses it only once, to securely agree on a shared symmetric key; that lightweight symmetric key then encrypts every byte of the actual session, including your payment data.
  2. The website's public key becomes invalid the moment the browser confirms the certificate's authenticity, so both sides are forced to generate a brand-new symmetric key just to keep the connection from closing.
  3. Symmetric encryption is layered on top of the asymmetric keys to double the effective key length, since 256-bit AES by itself would be too weak to resist attacks on a payment gateway like IRCTC's.
  4. Because a public key can only encrypt data and never decrypt it, the server cannot use its own public key to send data back to the browser, so a shared symmetric key is required to allow two-way traffic.

Answer: A. Asymmetric (public/private key) encryption is mathematically expensive to compute, so TLS uses it only once, to securely agree on a shared symmetric key; that lightweight symmetric key then encrypts every byte of the actual session, including your payment data.

ExplanationPublic-key (asymmetric) cryptography relies on hard math problems — like factoring huge numbers for RSA — that make encryption and decryption computationally heavy, roughly a thousand times slower than symmetric algorithms like AES for the same volume of data. If IRCTC tried to encrypt an entire ticket-booking session, with dozens of requests and responses, using only asymmetric keys, both your phone and their servers would slow to a crawl and the servers would struggle to handle millions of simultaneous users during Tatkal booking. So TLS uses the expensive asymmetric step for exactly one job: letting your browser and the server agree on a shared secret without an eavesdropper learning it, either by encrypting a pre-master secret with the server's public key or by running a Diffie-Hellman key exchange. Once that shared secret exists, both sides derive an identical symmetric session key from it, and every subsequent byte — your seat selection, passenger details, UPI ID, OTP — is encrypted with fast symmetric ciphers such as AES-GCM. This is exactly why a full HTTPS handshake happens once, but the padlock stays on instantly for every image and API call afterward. The certificate's public key does not expire when the handshake finishes; it stays valid for the certificate's entire validity period (often a year or more) and is simply reused for the next handshake, so nothing forces a symmetric key into existence to prevent the connection from closing. Stacking symmetric encryption on top of asymmetric encryption does not add up the two key lengths into one stronger cipher; AES-256 is already considered secure on its own, and the two encryption types are used for different jobs (key agreement versus bulk data protection), not combined for extra strength. And while it's true that only the server's private key can decrypt something encrypted with the server's public key, TLS doesn't need the reverse operation at all — the browser never needs to decrypt something the server encrypted with a public key, because once the shared symmetric key is established, both directions of traffic use that same symmetric key, sidestepping the directionality limits of asymmetric encryption entirely.

Question 198 · Feature Stores: Centralized Feature Management · hard

A bank's fraud-detection team keeps a single feature table with one column per customer, txns_last_7_days, which a nightly batch job overwrites with that customer's most recent 7-day transaction count — last night's value is discarded, not archived. To build a training set, the team takes a transaction that was flagged as fraud on March 14 and joins it directly to this table today, in August, to fetch txns_last_7_days for that customer. Every fraud-labeled row built this way ends up carrying a 7-day transaction count drawn from a window many months after the fraud actually happened, rather than from the week surrounding March 14 itself. Which specific feature-store capability was missing from their pipeline, and would have prevented this problem?

  1. The online store's low-latency key-value lookup, which serves the most current feature value within milliseconds for live prediction requests
  2. The offline store's point-in-time join, which retrieves each feature's value as it stood on the label's own timestamp instead of the latest overwritten value
  3. A central feature registry that lets every team discover and reuse one agreed definition of the feature instead of each writing separate computation logic
  4. A streaming pipeline that recomputes the feature every few seconds rather than nightly, keeping the single stored value continuously up to date

Answer: B. The offline store's point-in-time join, which retrieves each feature's value as it stood on the label's own timestamp instead of the latest overwritten value

ExplanationThe team's table holds only one number per customer — whichever value the nightly job most recently computed — so joining a March 14 fraud label to today's table in August pulls in transaction activity from months after the fraud happened, rather than from the days actually surrounding it. That is temporal leakage: the feature effectively describes the future relative to the event it is meant to predict. What the pipeline needed is the offline store's point-in-time join, which retrieves each feature's value as it stood on the label's own timestamp instead of the latest overwritten value — this requires logging feature values with timestamps so any past date can be reconstructed exactly, rather than keeping just one mutable row per customer. Serving the most current value within milliseconds solves latency for live traffic, not historical correctness for a training set; letting every team reuse one agreed feature definition solves inconsistency between teams' code, not leakage from an unversioned table; and recomputing more frequently only narrows the staleness window going forward — it still discards the history needed to correctly describe March 14.

Question 199 · Data Augmentation: Creating More from Less · hard

A team is building a handwritten-digit classifier (digits 0–9, similar to MNIST) and writes the following function to augment every training image (a PIL Image object) before it is fed to the model: ```python import random def augment(image, label): if random.random() < 0.5: image = image.rotate(180) return image, label ``` Every augmented image keeps the same `label` value it started with, whether or not the rotation was applied inside the function. Why would this specific augmentation strategy most likely hurt the trained model's accuracy?

  1. Rotating by 180 degrees swaps an image's width and height, so the convolutional layers receive inputs of inconsistent shape when augmented and original images are combined into the same batch.
  2. Because only images selected by the 50% random check get rotated, the augmented and original versions of each digit no longer appear in equal proportion, which creates class imbalance in the training set.
  3. A 180° rotation can turn one digit into the visual shape of a different digit: for example, a 6 becomes a 9, and a 9 becomes a 6, so keeping the original label on these rotated images silently creates mislabeled training examples.
  4. Since the local variable inside the function is reassigned to the rotated image, the original unrotated version is permanently deleted from the dataset, shrinking the total number of unique training examples.

Answer: C. A 180° rotation can turn one digit into the visual shape of a different digit: for example, a 6 becomes a 9, and a 9 becomes a 6, so keeping the original label on these rotated images silently creates mislabeled training examples.

ExplanationValid data augmentation must preserve the true relationship between an input and its label. A 180-degree rotation is a point-symmetric transformation that flips an image both left-to-right and top-to-bottom, and for several handwritten digits this produces the recognizable shape of a different digit entirely, most notably a 6 rotated 180 degrees looks like a 9 and a 9 rotated 180 degrees looks like a 6. Because the augment() function always returns the label unchanged, every digit whose rotated appearance resembles another digit becomes a training example where the pixels shown to the network contradict the class attached to them, teaching the model incorrect shape-to-class associations instead of improving generalization. The class imbalance claim does not hold because the 50% random rotation is applied independently of label, so the count of examples per digit stays exactly the same before and after augmentation. The claim about the original image being deleted misunderstands Python: reassigning the local image variable inside the function only changes what that local name points to, and has no effect on the dataset or file the image came from. The claim about width and height swapping is also false, since a 180-degree rotation is a point reflection that leaves an image's dimensions unchanged, unlike a 90-degree rotation.

Question 200 · Time Series Analysis with Python · hard

A Grade 9 student at AI Computer Institute is building a weather-trend mini-project. She records Delhi's daily maximum temperature (in °C) for one week and analyses it with the following code: ```python import pandas as pd temps = pd.Series( [21, 23, 22, 26, 29, 28, 31], index=pd.date_range('2026-08-01', periods=7, freq='D') ) rolling_avg = temps.rolling(window=3).mean() print(round(rolling_avg.iloc[4], 2)) ``` Assuming pandas is installed and the code runs without any errors, what value does this code print to the console?

  1. 23.67 — the mean of the Aug 2, Aug 3, and Aug 4 readings (23, 22, and 26), rounded to 2 decimals
  2. NaN — a 3-day rolling window only starts returning numbers from the 5th row onward
  3. 25.67 — the mean of the Aug 3, Aug 4, and Aug 5 readings (22, 26, and 29), rounded to 2 decimals
  4. 22.0 — the mean of the first three readings, Aug 1 to Aug 3 (21, 23, and 22)

Answer: C. 25.67 — the mean of the Aug 3, Aug 4, and Aug 5 readings (22, 26, and 29), rounded to 2 decimals

ExplanationA rolling window of size 3 always looks at the current row together with the two rows immediately before it, and only the very first two positions (index 0 and index 1) lack two prior rows, so pandas marks just those as NaN — not the first five rows. Because .iloc uses zero-based positions, iloc[4] is the fifth entry in the Series, which lines up with August 5, so its window covers August 3, 4, and 5 — the readings 22, 26, and 29. Adding these gives 77, and dividing by 3 gives 25.666..., which round() converts to 25.67 — the number this code actually prints. The choice built from 23, 22, and 26 mistakenly uses the window that ends one day earlier, at iloc[3]; the choice built from 21, 23, and 22 uses the very first window available, at iloc[2]; and the claim that the average only appears from the fifth row onward misstates how few prior rows a size-3 rolling window actually needs before it can start producing results.
← Set 9Set 11 →