Capstone: Ship a Real Java Project
ক্যাপস্টোন — একটি বাস্তব, production-grade Java প্রোজেক্ট তৈরি করুন
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.
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-এ দেখানো যাবে।
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.
Build a REST API with a real DB, authentication, and Docker deployment. Best for backend / full-stack goals।
Build a desktop application with a real GUI. Best for desktop software or Android preparation।
Write a reusable library, publish to Maven Central. Best for open-source and library design goals।
Build a polished command-line tool. Best for DevOps, scripting, and developer tooling goals।
3. Track A — Spring Boot REST API
Expense Tracker API — a bKash-scale fintech backend
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.
Below is the core layered architecture. Each layer is a standard Spring Boot concern:
// 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:
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+Expenseentities with JPA annotations, aUserRepositoryandExpenseRepository. - Auth: Register / login endpoints. On login, issue a JWT. Add a
JwtFilterto the Spring Security filter chain. - REST:
GET /api/expenses,POST /api/expenses,DELETE /api/expenses/{id}— only the authenticated user's data. - Tests:
@SpringBootTestintegration 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
Personal Finance Dashboard — a JavaFX desktop app
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.
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:
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:runfor quick start. - View: Design the main FXML layout — a
TableViewfor transactions, aBarChartfor categories, and an add-form at the bottom. - Controller: Bind the
TableViewto anObservableList<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.cssfile. Use JavaFX CSS to set fonts, colours, and spacing. - Package: Run
jpackageto produce a native installer. Provide aREADMEwith screenshots.
5. Track C — Open-Source Maven Library
bd-money — A Bangladeshi monetary value library
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.
Money class, arithmetic, Bangla numeral formatting। Maven Central-এ publish করুন। ভালো library design, tests ও docs — portfolio-র জন্য শক্তিশালী।
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
BigDecimalfor all monetary arithmetic (neverfloat/doublefor 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
bdstats — A command-line tool for CSV data analysis
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.
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:
@Commandon inner classes forsummary,histogram,sort. Register withsubcommands = {Summary.class, Histogram.class, Sort.class}. - Options: Use
@Optionfor--file,--column,--delimiter. Use@Parametersfor 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.
| # | Criterion | How 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:
mainis 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.
# 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"]
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 (শব্দভাণ্ডার)
| Term | Meaning | বাংলায় |
|---|---|---|
| Capstone | A final project that integrates and demonstrates everything learned. | সব শেখা একত্রিত করে দেখানোর চূড়ান্ত প্রকল্প। |
| Docker | Tool to package an app and its dependencies into a container. | App ও তার dependency container-এ pack করার টুল। |
| CI/CD | Continuous Integration / Continuous Deployment — automated test and release. | Automated test ও deploy pipeline। |
| picocli | Java library for building annotated command-line interfaces. | Annotation-based Java CLI library। |
| GraalVM native-image | Compiles Java to a standalone native binary (no JVM needed at runtime). | Java-কে standalone native binary-তে compile করে — runtime-এ JVM লাগে না। |
| Maven Central | The primary public repository for Java/JVM libraries. | Java/JVM library-র প্রধান public repository। |
| jpackage | JDK tool to package a Java app as a native installer. | Java app-কে native installer হিসেবে package করার JDK টুল। |
| SemVer | Semantic 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.
-
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.