Beginner Projects
Prerequisites: Stage 1, Modules 12–14, 18.
Learning Objectives
Build three complete small systems: Student Management, Library Management, Bank Account.
Practise classes, encapsulation, constructors, validation, collections, associations, composition, exceptions, value objects and tests.
Prerequisites: Stage 1, Modules 12–14, 18.
Intuition and Definition
Each project follows the same delivery checklist:
Requirements & use cases what the system must do, main and alternative flows class diagram of the core where code and tests live small, testable increments runnable core with checks what's good, what to watch next steps that stress the design
Why It Matters
Projects connect isolated concepts into working designs and reveal the trade-offs that exercises hide.
Syntax and Language Notes
Suggested layout for each project (C++ shown; the same split works in every language):
project/
src/domain/ entities, value objects, rules
src/app/ use cases / services
src/infra/ storage, I/O adapters
tests/ unit tests per class, one end-to-end test
CMakeLists.txt (or pyproject.toml / package.json / .csproj)Complete Example
Project 1 — Student Management Requirements: register students with unique ids and valid grades (0–100); search by name; list students sorted by average; reject duplicates and invalid grades.
Milestones: (1) Student with validation and tests; (2) registry with duplicate check; (3) search and ranking; (4) CSV import adapter.
Project 2 — Library Management Requirements: members borrow and return books; a member may hold at most 3 loans; a book copy can be lent once at a time; loans are due after 14 days; late returns cost 0.50 per day; history is kept.
Project 3 — Bank Account Requirements: accounts with integer-cent balances; deposits, withdrawals and transfers; no overdraft; every operation recorded as a transaction; transfers are atomic (both sides or neither); statement listing.
classDiagram
class Student {
-id : int
-name : string
-grades : vector~int~
+addGrade(g)
+average() double
}
class StudentRegistry {
-students : map~int, Student~
+add(student)
+findByName(text) vector
+rankedByAverage() vector
}
StudentRegistry *-- "0..*" Student#include <algorithm>
#include <cassert>
#include <iostream>
#include <map>
#include <numeric>
#include <stdexcept>
#include <string>
#include <vector>
class Student {
int id_; std::string name_; std::vector<int> grades_;
public:
Student(int id, std::string name) : id_(id), name_(std::move(name)) {
if (id <= 0) throw std::invalid_argument("id must be positive");
if (name_.empty()) throw std::invalid_argument("name required");
}
void addGrade(int g) { if (g < 0 || g > 100) throw std::out_of_range("grade must be 0..100"); grades_.push_back(g); }
double average() const { return grades_.empty() ? 0.0 : std::accumulate(grades_.begin(), grades_.end(), 0.0) / grades_.size(); }
int id() const { return id_; }
const std::string& name() const { return name_; }
};
class StudentRegistry {
std::map<int, Student> students_;
public:
void add(Student s) {
if (!students_.emplace(s.id(), std::move(s)).second) throw std::invalid_argument("duplicate id");
}
Student& get(int id) { return students_.at(id); }
std::vector<const Student*> findByName(const std::string& part) const {
std::vector<const Student*> out;
for (
…Raj Iyer 91.5
Ana Rao 84.6667
Anika Shah 70
tests passedclassDiagram
Library *-- "0..*" Book
Library o-- "0..*" Member
Library *-- "0..*" Loan
Loan --> Book
Loan --> Memberfrom dataclasses import dataclass, field
@dataclass
class Book:
isbn: str
title: str
copies: int
on_loan: int = 0
def available(self): return self.copies - self.on_loan
@dataclass
class Member:
member_id: str
name: str
active_loans: int = 0
@dataclass
class Loan:
book: Book
member: Member
day_out: int
returned_day: int | None = None
def fee(self, today):
late = max(0, (self.returned_day or today) - (self.day_out + 14))
return late * 0.5
class LibraryError(Exception): pass
class Library:
MAX_LOANS = 3
def __init__(self): self.books, self.members, self.loans = {}, {}, []
def add_book(self, b): self.books[b.isbn] = b
def join(self, m): self.members[m.member_id] = m
def lend(self, isbn, member_id, day):
book, member = self.books.get(isbn), self.members.get(member_id)
if not book or not member: raise LibraryError("unknown book or member")
if book.available() == 0: raise LibraryError(f"no copy of '{book.title}' available")
if member.active_loans >= self.MAX_LOANS: raise LibraryError(f"{member.name} already has {self.MAX_LOANS} loans")
book.on_loan += 1; mem
…refused: no copy of 'Clean Architecture' available
fee on time: 0.0
fee 6 days late: 3.0
Raj can borrow now: Clean Architecture
refused: already returned#include <cassert>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
class Money { // value object
long long cents_;
public:
explicit Money(long long c) : cents_(c) {}
long long cents() const { return cents_; }
Money operator+(Money o) const { return Money(cents_ + o.cents_); }
Money operator-(Money o) const { return Money(cents_ - o.cents_); }
bool operator<(Money o) const { return cents_ < o.cents_; }
std::string str() const { std::ostringstream s; s << cents_ / 100 << "." << std::setw(2) << std::setfill('0') << (cents_ < 0 ? -cents_ : cents_) % 100; return s.str(); }
};
struct Transaction { std::string kind; Money amount; Money balanceAfter; };
class Account {
std::string id_;
Money balance_{0};
std::vector<Transaction> history_;
public:
explicit Account(std::string id) : id_(std::move(id)) {}
void deposit(Money m) {
if (m.cents() <= 0) throw std::invalid_argument("deposit must be positive");
balance_ = balance_ + m;
history_.push_back({"deposit", m, balance_});
}
void withdraw(Money m) {
…refused: insufficient funds in ACC-2
statement ACC-1:
deposit 500.00 balance 500.00
withdraw 125.50 balance 374.50
statement ACC-2:
deposit 125.50 balance 125.50
tests passedStep-by-Step Walkthrough
Students: validation lives in Student; the registry enforces uniqueness and provides queries. Tests cover duplicates, invalid grades and search.
Library: the loan is its own object (association class) holding book, member and dates; rules (max loans, availability, late fee) live in Library/Loan.
Bank: Money is a value object; transfer withdraws first so a failure leaves both accounts unchanged; tests assert conservation of money.
Common Mistakes
Doubles for money; public balance fields; no history.
Storing loan state in Book only ("borrowedBy") — can't represent multiple copies or history.
Search and sort logic in main instead of the registry.
Trade-offs and Alternatives
These cores are in-memory; persistence comes as an adapter (Module 72). For concurrency, transfers would need locking or a ledger design (append-only transactions).
Real-World Use
These are miniature versions of school information systems, library systems and core banking ledgers — the same invariants apply at scale.
Practice
Q1. (Easy) Extension for Students: allow removing a grade by index with validation.
Answer: Add removeGrade(size_t i) that throws std::out_of_range if i >= grades_.size() and erases the element; add a test for a valid and an invalid index.
Q2. (Medium) Extension for Library: reservations — when a book is returned, the first member in its waiting queue gets it.
Answer: Add a per-book deque of member ids; reserve(isbn, member) appends; give_back checks the queue and creates a loan for the first waiting member (respecting their loan limit) or keeps the copy available.
Q3. (Advanced) Extension for Bank: make transfers safe under concurrency.
Answer: Lock both accounts with std::scoped_lock (deadlock-free, Module 60), or route all operations for an account through a single-threaded actor/queue; keep the "withdraw then deposit" order inside the critical section.
Interview Question
Q1. "Design a bank account class. What invariants and operations?"
Answer: Invariants: balance never below the allowed minimum (0 or an overdraft limit), money is conserved by transfers, history is append-only. Operations express intent (deposit, withdraw, transfer) and validate; balances use integer minor units or decimal types; every operation records a transaction; transfers are atomic (strong guarantee) and thread-safe if shared; no setBalance.
Quick Quiz
Q1. Why is Loan a class rather than a field on Book?
Answer: It has its own data (dates, fee) and lifecycle, and links a member to a book copy — an association class.
Q2. Which invariant do the bank tests check?
Answer: Conservation of money and no partial transfer on failure.
Q3. Where should grade validation live?
Answer: In Student::addGrade.
Summary
Small projects practise validation, collections, associations, value objects and tests.
Related: Modules 2–3, 9, 14, 18, 64.