Phase 03 Projects — Ship These, Not Blog Posts¶
Three projects. Each is portfolio-grade and demonstrates a specific chunk of Phase 03’s mental model. Do them in order; each builds on habits from the last. Push everything to GitHub as separate repos with proper READMEs — recruiters and study partners do skim these.
Project A — Refactor the Legacy OrderProcessor¶
Take an intentionally smelly class and refactor it into a clean modular design. Ship it as a before/ and after/ in the same repo so the diff tells the story.
The starter (paste this into before/OrderProcessor.java)¶
This class deliberately embodies most Phase 03 smells: God Class, primitive obsession, feature envy, long parameter lists, comment-as-deodorant, hardcoded strategies, inheritance abuse.
package legacy;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
public class OrderProcessor {
// --- Global mutable state (bad) ---
public static Map<String, BigDecimal> TAX_RATES = new HashMap<>();
static {
TAX_RATES.put("IN", new BigDecimal("0.18"));
TAX_RATES.put("US", new BigDecimal("0.07"));
TAX_RATES.put("UK", new BigDecimal("0.20"));
}
private String db_url = "jdbc:mysql://prod-db:3306/orders";
private String smtp_host = "smtp.example.com";
private List<Map<String, Object>> allOrders = new ArrayList<>();
// Process an order end-to-end: validate, price, tax, discount, ship, save, email.
public String processOrder(String customerId, String customerEmail, String customerCountry,
boolean isPremium, int membershipYears,
List<String> itemSkus, List<Integer> itemQtys, List<BigDecimal> itemPrices,
String couponCode, String paymentMethod, String cardNumber,
String cardCvv, String shippingCarrier, boolean expedited) {
// Validate
if (customerId == null || customerId.isEmpty()) return "ERROR: no customer";
if (itemSkus.size() != itemQtys.size() || itemSkus.size() != itemPrices.size())
return "ERROR: mismatched item arrays";
if (customerEmail == null || !customerEmail.contains("@")) return "ERROR: bad email";
// Compute subtotal
BigDecimal subtotal = BigDecimal.ZERO;
for (int i = 0; i < itemSkus.size(); i++) {
subtotal = subtotal.add(itemPrices.get(i).multiply(new BigDecimal(itemQtys.get(i))));
}
// Apply discount if premium and long-term member and order over 500
BigDecimal discount = BigDecimal.ZERO;
if (isPremium && membershipYears > 1 && subtotal.compareTo(new BigDecimal("500")) > 0) {
discount = subtotal.multiply(new BigDecimal("0.10"));
}
// Also apply coupon
if ("SAVE20".equals(couponCode)) discount = discount.add(subtotal.multiply(new BigDecimal("0.20")));
else if ("SAVE10".equals(couponCode)) discount = discount.add(subtotal.multiply(new BigDecimal("0.10")));
else if ("FLAT100".equals(couponCode)) discount = discount.add(new BigDecimal("100"));
// Tax
BigDecimal taxRate = TAX_RATES.getOrDefault(customerCountry, new BigDecimal("0.10"));
BigDecimal tax = subtotal.subtract(discount).multiply(taxRate);
// Shipping
BigDecimal shipping;
if ("FEDEX".equals(shippingCarrier)) shipping = expedited ? new BigDecimal("40") : new BigDecimal("20");
else if ("UPS".equals(shippingCarrier)) shipping = expedited ? new BigDecimal("35") : new BigDecimal("18");
else if ("USPS".equals(shippingCarrier)) shipping = new BigDecimal("10");
else shipping = new BigDecimal("25");
BigDecimal total = subtotal.subtract(discount).add(tax).add(shipping);
// Payment
boolean paid;
if ("CARD".equals(paymentMethod)) {
if (cardNumber == null || cardNumber.length() < 12) return "ERROR: bad card";
System.out.println("Charging card " + cardNumber.substring(0, 4) + "**** for " + total);
paid = true; // pretend
} else if ("UPI".equals(paymentMethod)) {
System.out.println("UPI collect request sent for " + total);
paid = true;
} else if ("COD".equals(paymentMethod)) {
paid = true;
} else {
return "ERROR: unknown payment method";
}
// Save (pretend)
String orderId = "ORD-" + System.currentTimeMillis();
Map<String, Object> row = new HashMap<>();
row.put("id", orderId);
row.put("customer", customerId);
row.put("total", total);
row.put("date", LocalDate.now());
allOrders.add(row);
System.out.println("Saved to " + db_url + ": " + row);
// Email
System.out.println("SMTP " + smtp_host + " -> " + customerEmail
+ " Subject: Order " + orderId + " confirmed");
return "OK:" + orderId + ":" + total;
}
}
The refactoring plan¶
Attack it in these commits, in this order. Do not skip steps — the sequence matters:
Characterization tests first. Write JUnit 5 tests that pin current behavior (all return paths). Do not touch the class until tests pass.
Extract value objects.
CustomerId,Email,CountryCode,Money,LineItem,Coupon,PaymentMethod(sealed),Carrier(enum). Delete matching primitive params.Introduce parameter object. Everything the method takes becomes an
OrderRequestrecord.Extract classes.
OrderValidator,PricingEngine,TaxCalculator,DiscountEngine,ShippingCalculator,PaymentGateway(sealed withCardPayment/UpiPayment/CodPayment),OrderRepository,NotificationService.Strategy pattern for discounts and shipping. Each coupon type and carrier is its own class or lambda registered in a
Map.Dependency injection.
OrderProcessoraccepts collaborators via constructor. Make it Spring-compatible with@Serviceand@Autowired(or manual wiring).Sealed
OrderResult. ReplaceStringreturn withsealed interface OrderResult permits Success, ValidationFailed, PaymentDeclined.Pattern-matching switch at the caller to handle each result.
Definition of done¶
after/OrderProcessoris under 60 lines.Every collaborator is under 100 lines with a single reason to change.
Test coverage ≥ 90% by line.
README.mdshows the before/after diff summary and lists which smells from05_refactoring_and_code_smells.mdyou fixed, mapped to your commits.One paragraph honestly explaining what you did not refactor and why (e.g., “payment gateway is still stubbed — real integration is Phase 09”).
study prep dividend¶
You now have a 30-minute story: “I inherited an 800-line class doing 15 things. Here is how I broke it apart safely.” Every senior study asks a variation of this.
Project B — Plugin System with Sealed Interfaces + Pattern Matching¶
Build a mini expression evaluator or command dispatcher that demonstrates data-oriented modeling in Java 21.
Option B1 — Expression Evaluator¶
A tiny arithmetic language: numbers, variables, let bindings, +, *, if. Parser optional (accept an already-built AST); focus on the evaluator.
sealed interface Expr permits Num, Var, Add, Mul, If, Let {}
record Num(double v) implements Expr {}
record Var(String name) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
record If(Expr cond, Expr thenBranch, Expr elseBranch) implements Expr {}
record Let(String name, Expr value, Expr body) implements Expr {}
Implement eval(Expr, Env), simplify(Expr) (constant folding: Add(Num(1), Num(2)) → Num(3)), and pretty(Expr) (renders back to source). All three exhaustive switches, zero default branches.
Stretch: Add a Fun/Call for lambdas. Closures via environment capture.
Option B2 — Command Dispatcher¶
A CLI tool where each command is a sealed variant of Command:
sealed interface Command permits AddUser, RemoveUser, ListUsers, ImportCsv, ExportJson, Help, Quit {}
record AddUser(String name, String email, Role role) implements Command {}
record RemoveUser(UserId id) implements Command {}
record ListUsers(Optional<Role> filter) implements Command {}
record ImportCsv(Path file) implements Command {}
record ExportJson(Path file) implements Command {}
record Help(Optional<String> topic) implements Command {}
record Quit() implements Command {}
Parse strings to Command, dispatch via one exhaustive switch. Add a new command and let the compiler tell you every dispatch site to update.
Definition of done¶
Sealed interface with ≥ 5 permitted subtypes, all records.
At least two exhaustive switches with zero
default.Add one new variant in a separate commit; the diff shows the compiler-driven update sites.
README.mdexplains why this design beats Visitor pattern with a code-size comparison.
Project C — A Real Domain Model with 3+ Patterns¶
Pick one: Library Management System, Hotel Booking, or E-Commerce Cart. Model it end-to-end with records, sealed types, and at least three GoF patterns from the alive list. No UI, no persistence — pure domain. This is a modeling exercise.
Suggested scope (Library Management)¶
Entities as records or record-backed classes:
Book,Member,Loan,Reservation.Sealed hierarchies:
LoanStatus permits Active, Returned, Overdue;MemberType permits Standard, Student, Faculty.Strategy:
LateFeeCalculator— standard 5₹/day, student half-rate, faculty exempt.Factory:
LoanFactoryproducing the rightLoanvariant based on member type.Builder:
SearchQuery.builder().author(\"Bloch\").afterYear(2015).available(true).build().Bonus — Decorator:
AuditingLibraryServicewrappingLibraryServiceto log every operation.Bonus — Observer:
ReservationNotifier— members subscribe to book availability.
Definition of done¶
15–25 classes/records total. Not more — the discipline is not to over-engineer.
Test coverage ≥ 85%.
README lists which patterns you used and, for each, why you chose it over the simplest alternative. study partners ask this.
One paragraph on what you deliberately did not do (“no persistence, no auth — out of scope for a domain model exercise”).
Timeline¶
Week |
Focus |
|---|---|
1–2 |
Project A: characterization tests + value objects + strategy extraction |
3–4 |
Project A: sealed result + DI + finish. Publish. |
5–6 |
Project B: pick one, ship it. |
7–8 |
Project C: domain model. Ship it. Write a short blog post tying the three. |
If you have Zoho-specific ML integration goals, use Project C as the modeling half of an ML feature: a book recommender or fraud-check on hotel bookings, with the ML piece stubbed as a Recommender interface. Phase 10 will fill in the ML integration.
Deliverable Checklist¶
Three public GitHub repos, one per project
Each with a proper README (problem, design, pattern choices, tradeoffs)
CI green (GitHub Actions running
mvn testorgradle test)Project A: before/after diff summary in README
Project B: a commit showing compiler-driven update of a new variant
Project C: one paragraph per pattern used, one paragraph on omissions
Return to README.md · Previous: 05_refactoring_and_code_smells.md · Next phase: ../04_generics_streams_functional/README.md