Capstone: Ship a Real Java Project

ক্যাপস্টোন — একটি বাস্তব, production-grade Java প্রোজেক্ট তৈরি করুন

Read: ~40 min Advanced 4 project tracks Live code runner Final Module

1. You Made It — Now Ship Something

Forty-nine modules ago you wrote System.out.println("Hello, Bangladesh!"). Since then you have worked through variables, OOP, generics, lambdas, streams, concurrency, the JVM memory model, Spring Boot, REST APIs, JPA, testing, and algorithm patterns. You now have the full toolkit of a working Java developer.

This final module does not teach another API. It gives you a structured path to ship a complete, real project — something you can put on GitHub, deploy to the cloud, and reference in a job interview. Choose the track that fits your goal.

উনচল্লিশটি module আগে আপনি লিখেছিলেন System.out.println("Hello, Bangladesh!")। তারপর থেকে variables, OOP, generics, lambdas, streams, concurrency, JVM memory model, Spring Boot, REST API, JPA, testing, algorithm — সব শিখেছেন। এখন আপনার কাছে একজন কার্যকর Java developer-এর পূর্ণ toolkit আছে।

এই শেষ module-এ নতুন API নেই। আপনাকে একটি সম্পূর্ণ, বাস্তব প্রোজেক্ট ship করার পথ দেওয়া হবে — যেটি GitHub-এ রাখা যাবে, cloud-এ deploy করা যাবে, এবং interview-এ দেখানো যাবে।
Your Java Journey — Module 01 → Module 50 L01 Hello L10 OOP L20 Generics L30 Streams L40 Threads L49 Algorithms L50 SHIP IT Figure 50.1 — ৫০টি module-এর পুরো যাত্রা। এখন সময় ship করার।

2. Choose Your Track

There is no single "correct" capstone. Pick the track that best fits your current goal — backend developer, Android/desktop, open-source contributor, or command-line tool builder. All four tracks share the same finishing criteria: clean code, passing tests, a README, and a live demo.

সঠিক capstone একটিই নয়। আপনার লক্ষ্য অনুযায়ী track বেছে নিন — backend developer, desktop/Android, open-source contributor, বা command-line tool builder। সব track-এর শেষ criteria একই: clean code, passing tests, README, এবং live demo।
Track A — Spring Boot REST API
Build a REST API with a real DB, authentication, and Docker deployment. Best for backend / full-stack goals।
Track B — JavaFX Desktop App
Build a desktop application with a real GUI. Best for desktop software or Android preparation।
Track C — Maven Library
Write a reusable library, publish to Maven Central. Best for open-source and library design goals।
Track D — CLI Tool with picocli
Build a polished command-line tool. Best for DevOps, scripting, and developer tooling goals।

3. Track A — Spring Boot REST API

Track A

Expense Tracker API — a bKash-scale fintech backend

Spring Boot 3 PostgreSQL / H2 Spring Security + JWT Docker + Compose GitHub Actions CI

Build a REST API for tracking personal expenses: users register, authenticate with JWT, create/list/delete expense records. Deploy with Docker Compose (app + database containers) and add a GitHub Actions pipeline that runs tests on every push.

Personal expense tracker REST API তৈরি করুন: user registration, JWT authentication, expense CRUD। Docker Compose দিয়ে deploy করুন এবং GitHub Actions CI যোগ করুন।

Below is the core layered architecture. Each layer is a standard Spring Boot concern:

ExpenseController.java — REST layer (reference, not runnable in sandbox)
// Spring Boot 3 · Java 21 · Controller layer
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import java.util.List;

@RestController
@RequestMapping("/api/expenses")
public class ExpenseController {

    private final ExpenseService service;

    ExpenseController(ExpenseService service) { this.service = service; }

    @GetMapping
    public ResponseEntity<List<ExpenseDTO>> list(@AuthenticationPrincipal AppUser user) {
        return ResponseEntity.ok(service.listFor(user));
    }

    @PostMapping
    public ResponseEntity<ExpenseDTO> create(@RequestBody CreateExpenseRequest req,
                                                    @AuthenticationPrincipal AppUser user) {
        return ResponseEntity.status(201).body(service.create(req, user));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id,
                                           @AuthenticationPrincipal AppUser user) {
        service.delete(id, user);
        return ResponseEntity.noContent().build();
    }
}

The service and repository layers follow the same pattern. Here is a runnable demo of the core domain logic — the Expense model and service — without the Spring framework, so you can see the business logic clearly:

Main.java — Core domain logic (runnable)
import java.util.*;
import java.time.LocalDate;

record Expense(long id, String description, double amount, LocalDate date) {}

class ExpenseService {
    private final List<Expense> store = new ArrayList<>();
    private long seq = 1;

    public Expense create(String desc, double amount) {
        Expense e = new Expense(seq++, desc, amount, LocalDate.now());
        store.add(e);
        return e;
    }

    public List<Expense> list() { return Collections.unmodifiableList(store); }

    public double totalThisMonth() {
        LocalDate firstOfMonth = LocalDate.now().withDayOfMonth(1);
        return store.stream()
            .filter(e -> !e.date().isBefore(firstOfMonth))
            .mapToDouble(Expense::amount)
            .sum();
    }

    public boolean delete(long id) { return store.removeIf(e -> e.id() == id); }
}

class Main {
    public static void main(String[] args) {
        ExpenseService svc = new ExpenseService();
        svc.create("Office lunch", 120.0);
        svc.create("Rickshaw", 30.0);
        svc.create("Mobile top-up", 50.0);

        System.out.println("--- All Expenses ---");
        svc.list().forEach(e ->
            System.out.printf("#%d %-18s %.2f BDT (%s)%n",
                e.id(), e.description(), e.amount(), e.date()));

        System.out.printf("%nTotal this month: %.2f BDT%n", svc.totalThisMonth());

        svc.delete(2);
        System.out.println("After deleting #2 → count: " + svc.list().size());
    }
}

Track A — Step-by-Step Checklist

  • Scaffold: spring initializr → add Web, JPA, Security, H2 (dev) / PostgreSQL (prod).
  • Domain: User + Expense entities with JPA annotations, a UserRepository and ExpenseRepository.
  • Auth: Register / login endpoints. On login, issue a JWT. Add a JwtFilter to the Spring Security filter chain.
  • REST: GET /api/expenses, POST /api/expenses, DELETE /api/expenses/{id} — only the authenticated user's data.
  • Tests: @SpringBootTest integration tests using an H2 in-memory DB + MockMvc.
  • Docker: Write a Dockerfile + docker-compose.yml (app + postgres containers).
  • CI/CD: GitHub Actions workflow: on push → test → build image → push to Docker Hub.

4. Track B — JavaFX Desktop Application

Track B

Personal Finance Dashboard — a JavaFX desktop app

JavaFX 21 FXML + CSS SQLite (via JDBC) Maven / Gradle jpackage native installer

Build a desktop GUI for the same expense tracking domain: a table showing monthly expenses, a bar chart of spending by category, and a form to add/delete records. Data persists to an SQLite file. Package as a native installer (.exe / .dmg / .deb) using jpackage.

একটি desktop GUI তৈরি করুন — monthly expense table, category-wise bar chart, add/delete form। SQLite-এ data persist করুন। jpackage দিয়ে native installer (.exe/.deb) তৈরি করুন।

JavaFX code requires the JavaFX SDK and cannot run in the sandbox. Below is the runnable business layer — the same data model you would wire to the JavaFX view:

Main.java — Dashboard business layer (runnable)
import java.util.*;
import java.util.stream.*;

enum Category { FOOD, TRANSPORT, UTILITIES, HEALTH, ENTERTAINMENT, OTHER }

record Tx(String label, Category cat, double amount) {}

class DashboardModel {
    private final List<Tx> txns = new ArrayList<>();

    void add(Tx t) { txns.add(t); }

    Map<Category, Double> byCategory() {
        return txns.stream().collect(
            Collectors.groupingBy(Tx::cat, Collectors.summingDouble(Tx::amount)));
    }

    double total() { return txns.stream().mapToDouble(Tx::amount).sum(); }

    Category topCategory() {
        return byCategory().entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .map(Map.Entry::getKey).orElse(Category.OTHER);
    }
}

class Main {
    public static void main(String[] args) {
        DashboardModel m = new DashboardModel();
        m.add(new Tx("Lunch",        Category.FOOD,          150));
        m.add(new Tx("Rickshaw",     Category.TRANSPORT,     40));
        m.add(new Tx("Internet bill", Category.UTILITIES,     500));
        m.add(new Tx("Doctor",        Category.HEALTH,        300));
        m.add(new Tx("Dinner out",    Category.FOOD,          250));
        m.add(new Tx("Cinema",        Category.ENTERTAINMENT, 180));

        System.out.println("--- Category Breakdown ---");
        m.byCategory().forEach((cat, total) ->
            System.out.printf("  %-15s %.2f BDT%n", cat, total));

        System.out.printf("%nTotal Spent : %.2f BDT%n", m.total());
        System.out.println("Top Category: " + m.topCategory());
    }
}

Track B — Step-by-Step Checklist

  • Scaffold: Maven project with JavaFX SDK dependency. Use mvn javafx:run for quick start.
  • View: Design the main FXML layout — a TableView for transactions, a BarChart for categories, and an add-form at the bottom.
  • Controller: Bind the TableView to an ObservableList<Tx>. Add/delete event handlers call the service.
  • Persistence: Use plain JDBC with an SQLite driver. On startup, run CREATE TABLE IF NOT EXISTS. Load all rows into the observable list.
  • Style: Add a style.css file. Use JavaFX CSS to set fonts, colours, and spacing.
  • Package: Run jpackage to produce a native installer. Provide a README with screenshots.

5. Track C — Open-Source Maven Library

Track C

bd-money — A Bangladeshi monetary value library

Java 17+ JUnit 5 + 95%+ coverage Javadoc Maven Central Semantic Versioning

Design and publish a small, focused library for working with BDT monetary values: immutable Money class, arithmetic, formatting in Bangla numerals, and currency exchange stub. A well-designed library with excellent docs and tests is a strong portfolio item and teaches API design deeply.

একটি ছোট, focused library তৈরি করুন — BDT monetary value-এর জন্য: immutable Money class, arithmetic, Bangla numeral formatting। Maven Central-এ publish করুন। ভালো library design, tests ও docs — portfolio-র জন্য শক্তিশালী।
Main.java — bd-money library core (runnable)
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;

final class Money implements Comparable<Money> {
    private static final String[] BN_DIGITS = {
        "০","১","২","৩","৪","৫","৬","৭","৮","৯"
    };

    private final BigDecimal amount;

    private Money(BigDecimal amount) {
        this.amount = amount.setScale(2, RoundingMode.HALF_UP);
    }

    public static Money of(double taka) { return new Money(BigDecimal.valueOf(taka)); }
    public static Money of(long taka)   { return new Money(BigDecimal.valueOf(taka)); }

    public Money plus(Money other)  { return new Money(amount.add(other.amount)); }
    public Money minus(Money other) { return new Money(amount.subtract(other.amount)); }
    public Money times(double factor) {
        return new Money(amount.multiply(BigDecimal.valueOf(factor)));
    }

    public String formatBn() {
        String s = amount.toPlainString();
        StringBuilder sb = new StringBuilder();
        for (char c : s.toCharArray()) {
            sb.append(c >= '0' && c <= '9' ? BN_DIGITS[c - '0'] : c);
        }
        return "৳" + sb;
    }

    @Override public int     compareTo(Money o)  { return amount.compareTo(o.amount); }
    @Override public boolean equals(Object o)    { return o instanceof Money m && amount.equals(m.amount); }
    @Override public int     hashCode()         { return Objects.hash(amount); }
    @Override public String  toString()         { return "BDT " + amount.toPlainString(); }
}

class Main {
    public static void main(String[] args) {
        Money price  = Money.of(1250.50);
        Money tax    = price.times(0.15);
        Money total  = price.plus(tax);
        Money change = Money.of(2000).minus(total);

        System.out.println("Price  : " + price.formatBn());
        System.out.println("Tax 15%: " + tax.formatBn());
        System.out.println("Total  : " + total.formatBn());
        System.out.println("Change : " + change.formatBn());
        System.out.println("price < total? " + (price.compareTo(total) < 0));
    }
}

Track C — Step-by-Step Checklist

  • Design the API: Write the Javadoc before the code. Clear, stable public API is the hardest part of library design.
  • Implement: Use BigDecimal for all monetary arithmetic (never float/double for money).
  • Test: Aim for 95%+ branch coverage with JUnit 5. Test edge cases: zero, negative, large sums, rounding.
  • Javadoc: Every public class and method must have a Javadoc comment with @param, @return, and @throws.
  • pom.xml: Configure the Maven release plugin, GPG signing, and the OSSRH Sonatype repository.
  • Publish: Follow the Sonatype OSSRH guide to create a Jira ticket and release to Maven Central.

6. Track D — CLI Tool with picocli

Track D

bdstats — A command-line tool for CSV data analysis

picocli 4 Java 17 GraalVM native-image Homebrew / scoop Shell completion

Build a CLI tool that reads a CSV file and prints statistical summaries: count, sum, mean, min, max, percentiles per column. Add sub-commands (summary, histogram, sort). Compile to a native binary with GraalVM so it starts in milliseconds with no JVM cold start.

একটি CLI tool তৈরি করুন যা CSV file পড়ে statistical summary দেয় — count, sum, mean, min, max, percentile। Sub-commands যোগ করুন। GraalVM native-image দিয়ে compile করুন — JVM ছাড়াই millisecond-এ start।
Main.java — CSV stats engine (runnable)
import java.util.*;
import java.util.stream.*;

class Stats {
    final String column;
    final double min, max, mean, p50, p90;
    final long   count;

    private Stats(String col, double[] sorted) {
        column = col;
        count  = sorted.length;
        min    = sorted[0];
        max    = sorted[sorted.length - 1];
        mean   = Arrays.stream(sorted).average().orElse(0);
        p50    = percentile(sorted, 50);
        p90    = percentile(sorted, 90);
    }

    static Stats of(String col, List<Double> values) {
        double[] sorted = values.stream().mapToDouble(Double::doubleValue).sorted().toArray();
        return new Stats(col, sorted);
    }

    private static double percentile(double[] s, int p) {
        int idx = (int) Math.ceil(p / 100.0 * s.length) - 1;
        return s[Math.max(0, idx)];
    }

    @Override public String toString() {
        return String.format(
            "%-12s count=%-4d min=%-8.2f max=%-8.2f mean=%-8.2f p50=%-8.2f p90=%.2f",
            column, count, min, max, mean, p50, p90);
    }
}

class CsvParser {
    /** Parse a CSV string into column→values map. */
    static Map<String, List<Double>> parse(String csv) {
        String[] lines = csv.strip().split("\n");
        String[] headers = lines[0].split(",");
        Map<String, List<Double>> cols = new LinkedHashMap<>();
        for (String h : headers) cols.put(h.trim(), new ArrayList<>());

        for (int i = 1; i < lines.length; i++) {
            String[] vals = lines[i].split(",");
            for (int j = 0; j < headers.length; j++) {
                try { cols.get(headers[j].trim()).add(Double.parseDouble(vals[j].trim())); }
                catch (NumberFormatException ignored) {}
            }
        }
        return cols;
    }
}

class Main {
    public static void main(String[] args) {
        String csv =
            "salary,hours,rating\n" +
            "45000,40,4.2\n50000,38,3.8\n62000,45,4.7\n" +
            "38000,42,3.5\n71000,35,4.9\n55000,40,4.1\n" +
            "48000,43,3.9\n66000,37,4.6\n42000,41,4.0\n" +
            "58000,39,4.3\n";

        System.out.println("bdstats summary");
        System.out.println("─".repeat(80));
        CsvParser.parse(csv).forEach((col, vals) ->
            System.out.println(Stats.of(col, vals)));
    }
}

Track D — Step-by-Step Checklist

  • Scaffold: Maven project + picocli dependency. Annotate the main class with @Command(name = "bdstats", mixinStandardHelpOptions = true).
  • Sub-commands: @Command on inner classes for summary, histogram, sort. Register with subcommands = {Summary.class, Histogram.class, Sort.class}.
  • Options: Use @Option for --file, --column, --delimiter. Use @Parameters for positional args.
  • Shell completion: picocli generates Bash/Zsh completion scripts automatically — add it to your README.
  • Native image: Add the GraalVM native-image Maven plugin. Run mvn -Pnative package.
  • Distribution: Publish releases to GitHub Releases. Add Homebrew tap instructions in the README.

7. The Universal Definition of "Done"

Regardless of track, every capstone project is not finished until it meets these five criteria. They are the same standards that professional teams use before shipping to production.

Track যাই হোক, পাঁচটি মানদণ্ড পূরণ না হলে capstone শেষ হয়নি। এগুলো professional team-এর production shipping-এর মানদণ্ড।
#CriterionHow to verifyবাংলায়
1 Tests pass mvn test exits 0 on a clean checkout. Coverage ≥ 80%. clean checkout-এ mvn test সফল। Coverage ≥ 80%।
2 README works A stranger can clone the repo and run the project using only the README instructions. অপরিচিত কেউ README পড়ে repo clone করে project চালাতে পারবে।
3 CI is green GitHub Actions (or equivalent) runs tests on every push to main. প্রতিটি push-এ GitHub Actions test চালায় এবং সবুজ।
4 No secrets in git DB passwords, API keys, and JWT secrets are in .env / env vars, not in source. DB password, API key, JWT secret — সব .env/env var-এ, code-এ নয়।
5 Live demo Track A: Render/Railway deploy URL. Track B: screenshot. Track C: Maven Central link. Track D: GitHub Release binary. Track অনুযায়ী live demo লিংক বা screenshot README-এ।

✅ Pro habits to build now

  • Write failing tests before the feature (TDD)
  • Commit often with meaningful messages ("add expense delete endpoint" not "fix")
  • Open a GitHub issue for each feature; close it with a PR
  • Use a branch strategy: main is always deployable
  • Code-review your own PR the next morning

⚠️ Common capstone mistakes

  • Never testing — "it works on my machine"
  • Hard-coding credentials and pushing to GitHub
  • One giant commit at the end
  • README with no setup steps
  • Scope creep — pick one track and finish it

8. Docker & CI Quick Reference (Track A)

For Track A developers, here are the two files that turn a local Spring Boot app into a cloud-deployable, auto-tested service.

Track A-র জন্য — এই দুটি ফাইল local Spring Boot app-কে cloud-deployable, auto-tested service-এ পরিণত করে।
Dockerfile
# Stage 1: build
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY . .
RUN ./mvnw -q package -DskipTests

# Stage 2: run — smaller image
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
.github/workflows/ci.yml
name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
          cache: maven
      - name: Run tests
        run: ./mvnw -q test
      - name: Build Docker image
        run: docker build -t expense-tracker:ci .

9. Vocabulary (শব্দভাণ্ডার)

TermMeaningবাংলায়
CapstoneA final project that integrates and demonstrates everything learned.সব শেখা একত্রিত করে দেখানোর চূড়ান্ত প্রকল্প।
DockerTool to package an app and its dependencies into a container.App ও তার dependency container-এ pack করার টুল।
CI/CDContinuous Integration / Continuous Deployment — automated test and release.Automated test ও deploy pipeline।
picocliJava library for building annotated command-line interfaces.Annotation-based Java CLI library।
GraalVM native-imageCompiles Java to a standalone native binary (no JVM needed at runtime).Java-কে standalone native binary-তে compile করে — runtime-এ JVM লাগে না।
Maven CentralThe primary public repository for Java/JVM libraries.Java/JVM library-র প্রধান public repository।
jpackageJDK tool to package a Java app as a native installer.Java app-কে native installer হিসেবে package করার JDK টুল।
SemVerSemantic Versioning — MAJOR.MINOR.PATCH version numbering.MAJOR.MINOR.PATCH version naming convention।

10. Your Capstone Brief

There is one practice problem for this module. It is not a ten-minute exercise — it is a project. Block out time, pick a track, and ship it.

এই module-এ একটিই practice problem — এটি দশ মিনিটের কাজ নয়, এটি একটি পূর্ণ প্রকল্প। সময় আলাদা করুন, track বেছে নিন এবং ship করুন।
  1. Build and ship your capstone project. Choose one of the four tracks, follow the step-by-step checklist, and meet all five "definition of done" criteria. When you are finished, post the GitHub link in the community.
    চারটি track-এর যেকোনো একটি বেছে step-by-step checklist অনুসরণ করুন এবং "definition of done"-এর পাঁচটি মানদণ্ড পূরণ করুন। শেষ হলে community-তে GitHub লিংক শেয়ার করুন।
    ✨ Starter Template — Track A (উত্তর দেখুন)

    Below is a fully self-contained runnable prototype for Track A that you can expand into the full project. It simulates the complete REST API lifecycle — request routing, service layer, repository, and JSON-style response — all without Spring, so it runs directly in the sandbox.

    Main.java — Full Track A prototype (runnable)
    import java.util.*;
    import java.time.*;
    import java.time.format.*;
    
    // ─── Domain ───────────────────────────────────────────────
    record Expense(long id, String userId, String desc, double amount, LocalDate date) {
        String toJson() {
            return String.format("""
                {"id":%d,"userId":"%s","description":"%s","amount":%.2f,"date":"%s"}""",
                id, userId, desc, amount, date);
        }
    }
    
    // ─── Repository ───────────────────────────────────────────
    class ExpenseRepository {
        private final List<Expense> db = new ArrayList<>();
        private long seq = 1;
    
        Expense save(String userId, String desc, double amount) {
            Expense e = new Expense(seq++, userId, desc, amount, LocalDate.now());
            db.add(e); return e;
        }
        List<Expense> findByUser(String userId) {
            return db.stream().filter(e -> e.userId().equals(userId)).toList();
        }
        boolean delete(long id, String userId) {
            return db.removeIf(e -> e.id() == id && e.userId().equals(userId));
        }
    }
    
    // ─── Service ──────────────────────────────────────────────
    class ExpenseService {
        private final ExpenseRepository repo;
        ExpenseService(ExpenseRepository r) { repo = r; }
    
        Expense   create(String uid, String desc, double amount) { return repo.save(uid, desc, amount); }
        List<Expense> list(String uid)   { return repo.findByUser(uid); }
        boolean   delete(long id, String uid) { return repo.delete(id, uid); }
        double    total(String uid) {
            return repo.findByUser(uid).stream().mapToDouble(Expense::amount).sum();
        }
    }
    
    // ─── Simulated HTTP Router ─────────────────────────────────
    class Router {
        private final ExpenseService svc;
        Router(ExpenseService s) { svc = s; }
    
        void handle(String method, String path, String user, Object body) {
            System.out.println("\n→ " + method + " " + path + " (user=" + user + ")");
            if ("GET".equals(method) && "/api/expenses".equals(path)) {
                List<Expense> list = svc.list(user);
                System.out.println("200 OK → [" + list.stream().map(Expense::toJson)
                    .collect(java.util.stream.Collectors.joining(",")) + "]");
                System.out.printf("    Total: %.2f BDT%n", svc.total(user));
            } else if ("POST".equals(method) && "/api/expenses".equals(path)) {
                Object[] p = (Object[]) body;
                Expense e  = svc.create(user, (String) p[0], (double) p[1]);
                System.out.println("201 Created → " + e.toJson());
            } else if ("DELETE".equals(method) && path.startsWith("/api/expenses/")) {
                long id = Long.parseLong(path.substring("/api/expenses/".length()));
                System.out.println(svc.delete(id, user) ? "204 No Content" : "404 Not Found");
            }
        }
    }
    
    class Main {
        public static void main(String[] args) {
            Router r = new Router(new ExpenseService(new ExpenseRepository()));
    
            r.handle("POST", "/api/expenses", "alice", new Object[]{"Office lunch",  120.0});
            r.handle("POST", "/api/expenses", "alice", new Object[]{"Mobile top-up", 50.0});
            r.handle("POST", "/api/expenses", "bob",   new Object[]{"Rickshaw",       35.0});
    
            r.handle("GET",    "/api/expenses",   "alice", null);
            r.handle("DELETE", "/api/expenses/1", "alice", null);
            r.handle("GET",    "/api/expenses",   "alice", null);
            r.handle("GET",    "/api/expenses",   "bob",   null);
        }
    }

    এটি Spring ছাড়া পুরো REST API-এর domain, repository, service ও routing layer দেখায়। এখন প্রতিটি অংশ Spring annotation দিয়ে replace করুন এবং JPA repository যোগ করুন।

🎓 কোর্স সম্পূর্ণ — Congratulations!

You started with System.out.println("Hello, Bangladesh!") and worked through 50 modules spanning variables, OOP, generics, lambdas, streams, concurrency, the JVM memory model, Spring Boot, REST APIs, JPA, testing, algorithms, and now a complete production project.

আপনি System.out.println("Hello, Bangladesh!") দিয়ে শুরু করেছিলেন এবং ৫০টি module-এ variables, OOP, generics, lambdas, streams, concurrency, JVM memory model, Spring Boot, REST API, JPA, testing, algorithm — সব শিখেছেন।

Java is not a language you finish learning — it is a language you grow into. Every large system you build from here will teach you more than any module could. Go ship something.
Java একটি শেষ হওয়ার ভাষা নয় — এটি বেড়ে ওঠার ভাষা। এখন থেকে আপনি যে বড় system তৈরি করবেন সেগুলোই সবচেয়ে বেশি শেখাবে। এগিয়ে যান — ship করুন।

Summary — Module 50 & The Course

This final module gave you four concrete paths to turn 49 modules of knowledge into a real, deployed, testable Java project: a Spring Boot REST API, a JavaFX desktop app, an open-source Maven library, or a picocli CLI tool. All four share the same definition of done — passing tests, a working README, green CI, no secrets in git, and a live demo. The coding was only ever the beginning; shipping is what separates engineers from learners.

এই শেষ module ৪৯ module-এর জ্ঞানকে বাস্তব, deployed, testable project-এ পরিণত করার চারটি পথ দিয়েছে — Spring Boot REST API, JavaFX desktop app, Maven library, বা picocli CLI tool। চারটির "done" মানদণ্ড একই — passing tests, কার্যকর README, green CI, code-এ কোনো secret নেই, live demo। Code লেখা কেবল শুরু — ship করাই engineer আর learner-এর পার্থক্য।

 Java Programming — From First Principles to Enterprise Production — সম্পূর্ণ। ধন্যবাদ, এবং শুভকামনা।