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 passed
classDiagram
  Library *-- "0..*" Book
  Library o-- "0..*" Member
  Library *-- "0..*" Loan
  Loan --> Book
  Loan --> Member
from 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 passed

Step-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.