Skip to content
Tenzok
Home
Services
Student Projects
BlogAboutContact
All insights
Engineering insight9 June 2026·11 min read

Mini Project vs Major Project: How to Scope a Final Year Build

A mini project is one hard thing done properly in four weeks. A major project is a system. Here is how to scope either one so you can actually finish it — and defend it.

final year projectcapstonescopingengineering

Most final year projects do not fail in the last week. They fail in the first week, in the ten minutes where a team picks something that sounds impressive and writes it on a form. Everything after that is a slow discovery of how big the thing actually was. The mini project vs major project question is not really a question about size. It is a question about what you can finish, understand completely, and still defend when someone pushes on it.

The examiner in your viva has twenty minutes and has already sat through several projects that day. The thing being tested is not whether your idea was ambitious. It is whether you understand the system in front of you: where it breaks, why you chose this over that, and what happens when the input is bad. An unscoped project makes those questions unanswerable, because you never had time to understand any single part of it.

This post covers the real difference between a mini project and a major project, how to turn a vague department brief into a scoped one, a worked example of narrowing a bad problem statement into a defensible one, a week-by-week skeleton for a 12-week capstone, and the order in which to cut scope when you are behind. You will be behind.

What actually separates a mini project from a major project

It is not size, and it is definitely not screen count. The difference is structural.

  • A mini project is 3 to 4 weeks. It is one hard thing, done properly. It does not need architecture. It needs a correct implementation of a single non-trivial idea, and you need to be able to explain why it works.
  • A major project is 10 to 12 weeks. It is a system: two or more components with contracts between them, real data flowing through, state that persists, and something deployed. The hard thing is still there, but now it has to survive contact with the rest of the system.

The defense differs the same way. For a mini project, the question is whether it works and whether you know why. For a major project, the question is why it is built this way: why a queue and not a cron job, why Postgres and not a JSON file, why this model and not the simpler one that would have been almost as good.

A very common failure mode: a major project that is really a mini project with three CRUD screens and a login page bolted on. The core does one thing, and the rest is padding. The padding is usually where the hardest questions land, because it is the part you understood least and thought about last.

The scoping trap: you picked a topic, not a problem

This is the most common mistake, and it stays invisible until about week six. Students pick topics. Topics are areas: blockchain in supply chain, AI in agriculture, IoT for smart cities. A topic has no failure condition, which means it has no finish line, which means you can work on it forever and never be done.

A problem has four things. If you cannot fill in all four, what you have is still a topic:

  • A user. Someone specific who has this problem. Not "farmers" — a farmer standing in a field holding a phone.
  • A decision. What does this person do differently because your system exists? If nothing changes, the system is decoration.
  • A cost of being wrong. What does a false positive cost? A false negative? These are not the same, and pretending they are is why so many students end up reporting accuracy on imbalanced data.
  • A way to check. How do you know it worked, on data the system has never seen, measured the way the real world would measure it?

"AI in agriculture" fails all four. "Detect leaf blight from a phone photo taken in field lighting, so a farmer decides whether to spray this week, where a missed infection costs a crop and a false alarm costs one spray" passes all four — and now you know exactly what to build and exactly what to measure.

How do I turn a vague department brief into a scoped one?

Departments hand you one line. "Machine learning for healthcare." "Web application for campus management." Your job is not to complain about it. Your job is to convert it into a contract with yourself, in writing, before you open an editor. Answer these five, in this order:

  1. 1Who is the user and what decision are they making?
  2. 2What data actually exists, today, that I can get my hands on this week?
  3. 3What is the one hard thing here — the part that could genuinely fail?
  4. 4What is the dumbest possible version that still does the hard thing? That is v1.
  5. 5What am I explicitly not doing? Write the list. It is the most useful part.

Then commit it to the repo as scope.yml and update it whenever reality changes. It is worth more than a Gantt chart: half the viva questions are answered directly out of it, and in week nine, when you are behind and tempted to invent new work, the not_doing list is the thing that stops you.

yaml
problem: >
  Flag diabetic inpatients at high risk of readmission within 30 days,
  so the discharge planner can schedule a follow-up call before discharge.

user: hospital discharge planner
decision: schedule a follow-up call, or do not
cost_of_false_negative: patient readmitted, no call was made
cost_of_false_positive: one wasted 10-minute phone call

data:
  source: UCI Diabetes 130-US hospitals (public, ~100k encounters)
  key_detail: patients repeat across rows; patient_nbr != encounter_id
  gotcha: missing values are the literal string "?", not empty cells

one_hard_thing: leakage-free evaluation across repeat patients

v1:
  - patient-grouped train/test split (no patient in both sides)
  - 8 features, named and frozen; preprocessing lives in the pipeline
  - one gradient-boosted model, fitted pipeline persisted with the threshold
  - threshold chosen from a recall target, not left at 0.5
  - FastAPI /predict endpoint, deployed, public URL

not_doing:
  - no live hospital integration (no HL7, no FHIR)
  - no fairness audit (too few positives per subgroup to report a stable
    metric; named as a limitation in the report)
  - no use of the weight column (empty for almost every encounter)
  - no explainability dashboard, no mobile app

Worked example: narrowing "AI for healthcare" into something defensible

Start with the brief as given: AI for healthcare. Useless. Pass one, pick a user and a decision: a discharge planner deciding whether to book a follow-up call. The output is no longer "insight", it is a yes or no about a specific patient at a specific moment.

Pass two, data reality. There is a well-known public dataset of roughly a hundred thousand diabetic hospital encounters. Two facts about it matter more than anything you will read in a paper. First, it has two ID columns: encounter_id identifies a visit, patient_nbr identifies a person, and the same person appears across many rows. Second, missing values are written as the literal string "?", so if you read the CSV naively, "missing" quietly becomes a first-class category in every column. Run df.isna().mean().sort_values() before you trust anything — you will find that weight is empty for roughly 97 percent of rows, which is why nobody serious uses it as a feature.

Pass three, find the one hard thing. It is not the model; any gradient-boosting library fits this in three lines. The hard thing is evaluating it honestly, because the obvious split puts the same patient on both sides. Here is the wrong version, which is what a lot of submissions contain:

python
# train_wrong.py -- the split most submissions ship
import pandas as pd
from sklearn.model_selection import train_test_split

df = pd.read_csv("diabetic_data.csv", na_values="?")
y = (df["readmitted"] == "<30").astype(int)

NUMERIC = ["time_in_hospital", "num_medications", "number_inpatient",
           "number_emergency", "number_diagnoses"]
CATEGORICAL = ["age", "insulin", "diabetesMed"]
X = df[NUMERIC + CATEGORICAL]

# WRONG: this splits rows, not people. patient_nbr repeats across rows, so one
# admission by a patient can sit in train while a later admission sits in test.
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

overlap = (set(df.loc[X_train.index, "patient_nbr"])
           & set(df.loc[X_test.index, "patient_nbr"]))
print(f"{len(overlap)} patients appear in BOTH train and test")

The model gets to see a patient's earlier admissions while training and is then scored on their later ones, so it can memorise the person instead of learning the pattern. Your reported score is then partly a measure of memorisation, and you should expect it to be optimistic — how optimistic depends on how many of your encounters are repeat patients, so measure it rather than guessing. Either way you cannot defend the number, and "did you split by patient or by row?" is a very easy question for an examiner or an interviewer to ask.

The fix is two changes, and together they are the spine of the project. Split on people, using GroupShuffleSplit with patient_nbr as the group. And put the preprocessing inside the pipeline, so that the exact transformer fitted on the training data is the object that later runs at inference — otherwise your deployed API and your trained model disagree about what a feature even is, and you will not find out until something returns nonsense.

python
# train.py -- complete and standalone. Produces model.joblib.
import joblib
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.impute import SimpleImputer
from sklearn.metrics import precision_recall_curve, roc_auc_score
from sklearn.model_selection import GroupShuffleSplit
from sklearn.pipeline import Pipeline, make_pipeline
from sklearn.preprocessing import OneHotEncoder

NUMERIC = ["time_in_hospital", "num_medications", "number_inpatient",
           "number_emergency", "number_diagnoses"]
CATEGORICAL = ["age", "insulin", "diabetesMed"]
COLUMNS = NUMERIC + CATEGORICAL

df = pd.read_csv("diabetic_data.csv", na_values="?")
X = df[COLUMNS]
y = (df["readmitted"] == "<30").astype(int)
groups = df["patient_nbr"]

# Split people, not rows.
splitter = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
train_idx, test_idx = next(splitter.split(X, y, groups))
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]

# Preprocessing lives INSIDE the pipeline, so the transformer that was fitted
# on the training data is the same object that runs at inference.
pipe = Pipeline([
    ("prep", ColumnTransformer([
        ("num", "passthrough", NUMERIC),
        ("cat", make_pipeline(
            SimpleImputer(strategy="constant", fill_value="missing"),
            OneHotEncoder(handle_unknown="ignore"),
        ), CATEGORICAL),
    ])),
    ("clf", HistGradientBoostingClassifier(random_state=42)),
])
pipe.fit(X_train, y_train)

proba = pipe.predict_proba(X_test)[:, 1]
print("grouped AUC:", round(roc_auc_score(y_test, proba), 3))

# A probability is not a decision. A missed readmission costs a readmission;
# a false alarm costs one 10-minute phone call. So take the highest threshold
# that still catches 60 percent of true readmissions.
precision, recall, thresholds = precision_recall_curve(y_test, proba)
i = int(np.where(recall[:-1] >= 0.60)[0].max())
threshold = float(thresholds[i])
print(f"threshold={threshold:.3f} "
      f"precision={precision[i]:.3f} recall={recall[i]:.3f}")

# Ship the fitted pipeline, the threshold and the split together. One artifact.
joblib.dump(
    {
        "pipeline": pipe,
        "threshold": threshold,
        "columns": COLUMNS,
        "train_patients": sorted(groups.iloc[train_idx].unique().tolist()),
        "test_patients": sorted(groups.iloc[test_idx].unique().tolist()),
    },
    "model.joblib",
)

Three things just happened. Your reported score is now honest. You stopped shipping a probability and started shipping a decision, with the threshold derived from which mistake hurts more. And the feature list is now a real, named, frozen thing — eight columns, not whatever get_dummies happened to produce — which means the API you deploy in a moment can actually be built against it.

One more move, and it is the one people skip. Do not put the no-leakage assertion next to the splitter, where it checks a property GroupShuffleSplit already guarantees and can never fail. Put it in the eval script, where it reads the split back out of the saved artifact. There it can fail, because a future refactor of train.py can quietly reintroduce the leak, and this is the thing that catches it.

python
# eval.py -- scores the artifact that ships, not the notebook that made it.
import joblib
import pandas as pd
from sklearn.metrics import roc_auc_score

bundle = joblib.load("model.joblib")
train_patients = set(bundle["train_patients"])
test_patients = set(bundle["test_patients"])

# This assertion can actually fail. It checks the persisted split, so a change
# to train.py that reintroduces the leak fails here, in CI, not in the viva.
leaked = train_patients & test_patients
assert not leaked, f"leak: {len(leaked)} patients in both splits"

df = pd.read_csv("diabetic_data.csv", na_values="?")
test = df[df["patient_nbr"].isin(test_patients)]
y_test = (test["readmitted"] == "<30").astype(int)
proba = bundle["pipeline"].predict_proba(test[bundle["columns"]])[:, 1]

print("held-out patients:", len(test_patients))
print("grouped AUC:", round(roc_auc_score(y_test, proba), 3))

An examiner cannot break a project that has already told them where it breaks.

The final scoped statement now fits in one sentence: "A readmission-risk service for diabetic inpatients, evaluated with a patient-grouped split so that no patient appears in both train and test, with a decision threshold tuned to a 60 percent recall target, served from a deployed API whose request schema is the model's feature schema." That is a defensible major project. "AI for healthcare" was not.

The one hard thing rule

Every good project has exactly one part that could genuinely fail — where you do not know the answer on day one and have to go find out. Everything else exists to hold that part up.

  • ML: honest evaluation under leakage, class imbalance, or distribution shift. Rarely the model itself.
  • Systems: making something correct under concurrency, or fast when it has no right to be.
  • Embedded: doing real work inside a power, memory, or latency budget you cannot exceed.
  • Web and product: a genuinely hard piece of state — offline sync, conflict resolution, real-time collaboration.
  • Security: a threat model you can articulate, and a mitigation you can demonstrate breaking and then holding.

Everything around the one hard thing should be boring on purpose. Postgres. A single server. Server-rendered pages. A managed host. Boring infrastructure is not a lack of ambition; it is what buys you the weeks you need for the part that is actually hard. Two hard things in a 12-week project usually means you do neither of them well, and the seam between them is exactly where the viva goes.

A week-by-week skeleton for a 12-week capstone

The ordering matters more than the content. Note where deployment sits — week three, not week eleven.

  1. 1Week 1: Write scope.yml. Get the data or the API keys in hand. If the data does not exist, the project does not exist — find that out now, not in week seven.
  2. 2Week 2: Build the ugliest end-to-end path. Input goes in one end, a garbage answer comes out the other. Hardcode everything. It must run.
  3. 3Week 3: Deploy that garbage version to a real URL. Yes, now. This is the cheapest week you will ever have to fight config, secrets, ports, and build tooling.
  4. 4Week 4: Set up the evaluation or the test harness — the thing that tells you whether you are getting better. Before you try to get better.
  5. 5Weeks 5-7: The one hard thing. This is the project. Protect these three weeks like rent.
  6. 6Week 8: Freeze the core. No new capability after this point. Write down the honest numbers, including the disappointing ones.
  7. 7Week 9: The system around it — persistence, auth if you truly need it, error handling, the interface a human touches.
  8. 8Week 10: Break it on purpose. Empty input, huge input, malformed input, the network dying mid-request. Fix what you can, document what you cannot.
  9. 9Week 11: The report and the README. Diagrams of the real architecture, not the one you imagined in week one.
  10. 10Week 12: Rehearse the defense out loud, in front of someone who will interrupt you. Leave buffer for the thing that will go wrong.

Leaving deployment to the final week is a reliable way to end a capstone with a laptop demo and an apology. Deployment failures are rarely small: environment, dependency, secret, and build failures tend to arrive at once, in the week you have the least slack. Do it in week three, when it costs you two days and teaches you what your project actually needs.

What do I cut when I am behind?

You will probably be behind by week eight. That is normal and survivable, as long as you cut in the right order. Cut from the top of this list first:

  1. 1Breadth of data. One disease, one crop, one city, one language. Not five.
  2. 2Feature count. The three features nobody asked for, including the admin panel.
  3. 3UI polish. A plain, fast, working interface is far easier to defend than a beautiful broken one.
  4. 4Extra models and extra comparisons. Two baselines you understand beat six you cannot explain.
  5. 5Real-time anything. Batch is fine. Say it is batch and say why.

Never cut these three: the evaluation, the deployment, and the honest write-up of what does not work. And when you cut, cut loudly — put it in the report as a named limitation with a reason. "We did not audit fairness across demographic subgroups: splitting the held-out set by subgroup left too few positive readmissions in several groups to report a rate we could stand behind, so we report none and name it here." A limitation you declared reads very differently from one that gets discovered.

Why "deployed to a real URL" changes how the project is read

A local demo asks the viewer to trust you. A URL does not ask for anything; it either loads or it does not. But the real value is not the impression. Deployment forces you to confront what a laptop demo lets you avoid: config that is not hardcoded, secrets that are not in the repo, a model small enough to load, latency you can measure, cold starts, CORS — and above all the feature contract, because the moment a request arrives as JSON you have to say exactly which columns your model expects and in what form. That is precisely why the fitted pipeline gets persisted and reused rather than rebuilt at inference.

python
# tests/test_smoke.py  ->  APP_URL=https://your-app.example.com pytest tests/
import os

import httpx

BASE = os.environ["APP_URL"].rstrip("/")

# The same eight fields the model was trained on. The request model in the API
# declares age as a Literal of the ten dataset buckets, insulin as a Literal of
# its four values, and time_in_hospital as an int with ge=1, le=14.
VALID = {
    "age": "[70-80)",
    "insulin": "Steady",
    "diabetesMed": "Yes",
    "time_in_hospital": 5,
    "num_medications": 18,
    "number_inpatient": 2,
    "number_emergency": 0,
    "number_diagnoses": 9,
}


def test_health():
    r = httpx.get(f"{BASE}/health", timeout=10)
    assert r.status_code == 200
    assert r.json()["status"] == "ok"


def test_predict_returns_a_probability_and_a_decision():
    r = httpx.post(f"{BASE}/predict", json=VALID, timeout=10)
    assert r.status_code == 200
    body = r.json()
    assert 0.0 <= body["readmission_probability"] <= 1.0
    assert body["decision"] in {"schedule_call", "no_call"}


def test_rejects_an_age_outside_the_dataset_vocabulary():
    # A full, otherwise-valid payload with exactly ONE bad field, so the 422 is
    # about the value and not about a key we forgot to send.
    r = httpx.post(f"{BASE}/predict", json=VALID | {"age": "banana"}, timeout=10)
    assert r.status_code == 422  # FastAPI validation, not a 500


def test_rejects_an_impossible_length_of_stay():
    r = httpx.post(f"{BASE}/predict", json=VALID | {"time_in_hospital": -3}, timeout=10)
    assert r.status_code == 422

Those last two tests are what separate a project from an exercise, and note carefully what makes them work. They send a complete, valid payload and corrupt exactly one field. That only returns 422 because the request model constrains the value — age is a Literal of the dataset's ten bucket strings, not a free string. If you type age as a plain str, then "banana" is a perfectly good string, your service accepts it, the one-hot encoder shrugs at an unknown category, and you cheerfully score a patient who does not exist. Returning a clean 422 instead of a stack trace, or worse a confident number, is the difference between something that was built and something that was submitted.

The questions you will actually be asked

Scope your project so that you can answer all of these in one breath:

  • What is the input, and what is the output? Say it in one sentence.
  • How did you split the data, and why that way?
  • What is your baseline, and by how much do you beat it?
  • Show me where it fails. What input breaks it?
  • Why this technology and not the simpler one?
  • What did you cut, and why?
  • What would you do with four more weeks?

None of them are about how ambitious your idea was. Every one of them is about whether you understand the thing you made. A tightly scoped project can answer all seven. A sprawling one struggles with every single one, which is exactly why it is so uncomfortable to stand next to.

This is how we scope work at Tenzok: one hard thing, boring infrastructure around it, deployed early, and an honest number rather than an impressive one. It is not a trick. It is what shipping software looks like when somebody has to maintain it afterwards.

So pick the smallest problem you would still be proud to defend, and then defend it properly. That is worth more than an ambitious project you have to apologise for.

Frequently asked

Questions people actually ask

What is the real difference between a mini project and a major project?

A mini project is 3 to 4 weeks and does one hard thing properly — it needs a correct implementation and a clear explanation, not architecture. A major project is 10 to 12 weeks and is a system: multiple components with contracts between them, persistent state, real data, and something deployed. The mini project asks "does it work and do you know why". The major project also asks "why is it built this way".

How do I choose a final year project if my department only gives a vague topic?

Convert the topic into a problem before you write any code. A problem has a specific user, a decision that user makes differently because of your system, a cost of being wrong (false positives and false negatives usually cost different amounts), and a way to check that it worked on unseen data. If you cannot fill in all four, you still have a topic, and a topic has no finish line.

Is it okay to use a public dataset for a final year project?

Yes, and it is usually the right call — collecting data can eat half your timeline. What matters is handling the dataset honestly. Many public datasets contain a trap, such as the same subject appearing across many rows, or missing values encoded as a literal "?" string that silently becomes a feature category. Finding and fixing that trap is often a stronger contribution than the model itself. My own view: a leakage-free evaluation on a well-known dataset is worth more than a novel dataset with a broken split.

What is data leakage in a final year ML project, and how do I avoid it?

The common form is group leakage: your dataset has repeated subjects (the same patient, user, or device across many rows), and a random row-level split puts the same subject in both train and test. The model can then memorise the subject instead of learning the pattern, and your score is usually optimistic as a result. Split on the group id with GroupShuffleSplit or GroupKFold, then assert in your eval script — reading the split back from the saved model artifact — that no group id appears on both sides, so a later refactor cannot quietly reintroduce the leak.

Should I deploy my final year project, and when?

Deploy in week three, not week eleven. Deploy the ugliest working version before you build anything real. Deployment failures tend to arrive all at once — environment, dependencies, secrets, build config — and you want to hit them while you still have nine weeks of slack. Deployment also forces you to define your feature contract: once a request arrives as JSON you must state exactly which fields the model expects, which is a question a laptop demo lets you dodge.

What should I cut first if my capstone is running behind?

Cut in this order: breadth of data (one disease, one language, one city), extra features and admin panels, UI polish, extra model comparisons, and real-time behaviour (batch is fine). Never cut the evaluation, the deployment, or the honest write-up of what does not work. When you cut, name the cut in your report as an explicit limitation with a reason — a limitation you declared reads very differently from one that gets discovered.

Apply it to your project

Stuck on this in your own build?

This is the kind of problem we work through in code reviews every week. Send the problem statement and we’ll tell you honestly whether the scope is right.

Talk to us

On this page

  • What actually separates a mini project from a major project
  • The scoping trap: you picked a topic, not a problem
  • How do I turn a vague department brief into a scoped one?
  • Worked example: narrowing "AI for healthcare" into something defensible
  • The one hard thing rule
  • A week-by-week skeleton for a 12-week capstone
  • What do I cut when I am behind?
  • Why "deployed to a real URL" changes how the project is read
  • The questions you will actually be asked

Need a second opinion?

Send the problem statement directly to the Tenzok team.

Email us

Keep reading

Related engineering notes

Browse all insights

14 July 2026 · 13 min

Spring Boot Microservices Project: What to Build, What to Skip

Most Spring Boot microservices projects are three CRUD apps in Docker with a Eureka server; here is what actually makes it a distributed-systems project, and what to cut.

Read article

8 July 2026 · 11 min

12 Viva Questions Examiners Ask About Your Final Year Project

Viva questions cluster into a few recognisable families, and in most vivas every one of them ends with the same follow-up: show me where that happens in the code.

Read article

1 July 2026 · 11 min

How to Deploy Your Final Year Project to a Real URL

A localhost screenshot says "I got it working once." A live URL says "this runs without me." Here is the shortest honest path from your laptop to a real deployment: Docker, secrets, a health check, TLS, and CI/CD that ships on merge.

Read article
Your next build starts here

Turn the idea into software people trust.

Bring us a product brief, a business problem, or a final-year project. We’ll turn it into a clear scope, a working build, and a handover you fully own.

Start Your ProjectSend your brief

Prefer email? info@tenzok.in

Tenzok

A product engineering studio for ambitious companies, founders, and students who want real, production-minded work.

info@tenzok.in

Company

HomeBlogAboutContactFAQ

Services

MentorshipStudent ProjectsCompany ServicesDigital MarketingLaunch Support

Project domains

Python Full-StackJava & EnterpriseAI & LLM ApplicationsMachine LearningExplore all 18 domainsRSS feed

© 2026 Tenzok. All rights reserved.

Obsession · Purpose · Excellence

Published by Tenzok. Contact info@tenzok.in.