A Java-based console e-commerce application demonstrating core OOP concepts including Encapsulation, Inheritance, Interfaces, Exception Handling, Collections, and Wrapper Classes.
E_CommerceWebsite2/
├── ECommerceApp.java # Main entry point
├── User.java # User model
├── UserService.java # User interface
├── UserServiceImpl.java # User logic (signup/login/validation)
├── Product.java # Product model
├── ProductService.java # Product interface
├── ProductServiceImpl.java # Product catalog & search
├── Category.java # Enum for product categories
├── CartItem.java # Cart item (product + quantity)
├── CartService.java # Cart interface
├── CartServiceImpl.java # Cart logic (add/remove/checkout)
├── InvalidUserException.java # Custom checked exception
├── InvalidProductException.java # Custom unchecked exception
└── PaymentException.java # Custom checked exception
- Encapsulation — all 4 fields are
private, hidden from outside classes - Constructor — all fields are set at object creation time, not after
- Immutability pattern — no setters exist, so once a
Useris created, its data cannot be changed - Access Modifiers —
privateon fields,publicon getters — controlled access - Wrapper Class —
Stringis a wrapper/reference type used for all text fields
privatekeyword ensures no other class can directly douser.password = "hack"— they must go through the getter- No
setPassword()orsetEmail()exists — this is intentional to prevent accidental data mutation this.username = usernameinside the constructor usesthisto distinguish the field from the parameter- All 4 getters return the raw field value — read-only access from outside
public class User {
// Encapsulation: all fields private — not directly accessible outside
private String username;
private String password;
private String email;
private String mobileNumber;
// Constructor: sets all fields once at creation
public User(String username, String password, String email, String mobileNumber) {
this.username = username; // 'this' refers to current object's field
this.password = password;
this.email = email;
this.mobileNumber = mobileNumber;
}
} // public getters — the ONLY way to read these fields from outside
public String getUsername() { return username; }
public String getPassword() { return password; }
public String getEmail() { return email; }
public String getMobileNumber() { return mobileNumber; }
// ❌ No setters — data cannot be changed after object is created
// This prevents: user.setPassword("hacked") from being possible🔑 How it works:
- Fields are hidden (
private) — direct access likeuser.passwordcauses a compile error- Outside classes call
user.getPassword()to read — but cannot write- This pattern (private fields + public getters) is the foundation of Encapsulation in OOP
Stringitself is a Wrapper class — it wraps character data into an object with built-in methods like.equals(),.length(),.trim()
- Interface — a pure contract with no implementation, only method signatures
- Abstraction — hides how signup/login works; only exposes what they do
- Exception Declaration —
throws InvalidUserExceptionis declared at the interface level, forcing all implementations to handle it - Return Types —
booleanreturn tells the caller whether the operation succeeded
- An interface contains zero implementation — no method body, no fields (except constants)
- Any class that says
implements UserServicemust define bothsignup()andlogin()or it won't compile - Declaring
throws InvalidUserExceptionon the interface forces every implementing class to either throw or handle it - This makes it easy to swap
UserServiceImplfor a different implementation (e.g., database-backed) without changing any calling code
public interface UserService {
// Method signature only — no body, no logic
// 'throws' forces callers to handle the checked exception
boolean signup(String username, String password, String email, String mobile)
throws InvalidUserException;
boolean login(String username, String password);
// No implementation here — just the contract
}// 'implements UserService' = this class fulfills the contract
public class UserServiceImpl implements UserService {
@Override // annotation confirms this method satisfies the interface
public boolean signup(String username, String password, String email, String mobile)
throws InvalidUserException {
// actual logic lives here, not in the interface
}
@Override
public boolean login(String username, String password) {
// actual logic lives here
}
}🔑 How it works:
- Interface = blueprint,
UserServiceImpl= actual building- If
UserServiceImplforgets to implementlogin(), Java gives a compile-time error immediately@Overrideannotation confirms the method correctly matches the interface signature- This pattern (interface + implementation) is called Program to an Interface — a key design principle
- Interface Implementation —
implements UserServicefulfills the contract - Collections —
ArrayList<User>stores all registered users in memory - Exception Handling — throws
InvalidUserExceptionfor every validation failure - Access Modifiers —
privatehelper validators are internal only, not exposed - Wrapper Classes —
Stringmethods like.equalsIgnoreCase(),.matches(),.indexOf()used throughout
ArrayList<User>acts as an in-memory database — all users are lost when the program exits- Each validation step (
validateUsername,validatePassword,validateEmail,validateMobile) is a separateprivatemethod — keeps code clean and single-responsibility - Duplicate check uses
.equalsIgnoreCase()— so"Alice"and"alice"are treated as the same username - Email validation checks: not empty → has
@→ has.after@→ has valid TLD (e.g.,.com) - Mobile validation uses regex
\\d{10}— exactly 10 numeric digits, nothing else login()does NOT throw an exception — it just returnsfalseon failure (non-critical path)
public class UserServiceImpl implements UserService {
// Collection: ArrayList stores all users in memory (acts as in-memory DB)
private List<User> users = new ArrayList<>();
@Override
public boolean signup(String username, String password, String email, String mobile)
throws InvalidUserException {
// Step 1: validate all inputs (throws if any field is invalid)
validateUsername(username);
validatePassword(password);
validateEmail(email);
validateMobile(mobile);
// Step 2: check for duplicates in the ArrayList
for (User u : users) {
if (u.getUsername().equalsIgnoreCase(username))
throw new InvalidUserException("Username '" + username + "' is already taken!");
if (u.getEmail().equalsIgnoreCase(email))
throw new InvalidUserException("Email '" + email + "' is already registered!");
}
// Step 3: all checks passed — add to the list
users.add(new User(username, password, email, mobile));
return true;
}
} @Override
public boolean login(String username, String password) {
// Search through ArrayList for matching credentials
for (User u : users) {
if (u.getUsername().equals(username) && u.getPassword().equals(password)) {
System.out.printf(" Welcome back, %s | 📧 %s | 📱 %s%n",
u.getUsername(), u.getEmail(), u.getMobileNumber());
return true; // found — login success
}
}
System.out.println(" ✘ Invalid username or password.");
return false; // not found — login failed (no exception, just false)
} // Access Modifier: private — only used internally, not part of the interface
private void validateUsername(String username) throws InvalidUserException {
if (username == null || username.trim().isEmpty())
throw new InvalidUserException("Username cannot be empty.");
if (username.length() < 3)
throw new InvalidUserException("Username must be at least 3 characters.");
}
private void validateEmail(String email) throws InvalidUserException {
int atIndex = email.indexOf('@'); // Wrapper class: String.indexOf()
if (atIndex <= 0)
throw new InvalidUserException("Email must contain '@'.");
String domain = email.substring(atIndex + 1);
if (!domain.contains("."))
throw new InvalidUserException("Email domain must contain '.'.");
String tld = domain.substring(domain.lastIndexOf('.') + 1);
if (tld.isEmpty())
throw new InvalidUserException("Email must have a valid TLD like .com or .in");
}
private void validateMobile(String mobile) throws InvalidUserException {
if (!mobile.matches("\\d{10}")) // Regex: exactly 10 digits, no spaces/symbols
throw new InvalidUserException("Mobile must be exactly 10 digits.");
}🔑 How it works:
implements UserService— this class provides the actual body for both interface methodsArrayList<User>— a resizable list that grows as users register- Each
validate*()method throwsInvalidUserExceptionimmediately if a rule is broken — the signup stops at the first failurelogin()returnsfalseinstead of throwing — login failure is expected behavior, not an errorprivatevalidators cannot be called from outside — they are internal helpers only
- Enum — a special Java class that holds a fixed set of named constants
- Type Safety — using
Categoryas a type prevents invalid strings like"Electronix"from being passed - Method in Enum —
displayName()adds behaviour to a constant, making enums more powerful than plain strings - Switch Statement — used inside
displayName()to return a human-readable label per constant
- Enums are compile-time constants —
Category.ELECTRONICSwill never benullor misspelled Category.values()returns an array of all 4 constants — used inProductServiceImplto loop over all categories- Adding a method (
displayName()) inside an enum makes it behave like a class, not just a list of labels - If you add a new category (e.g.,
BOOKS), the compiler will warn you everywhere aswitchonCategoryexists — safer than using raw strings
public enum Category {
ELECTRONICS, // constant 1
FOOD, // constant 2
CLOTHING, // constant 3
SPORTS; // constant 4 — semicolon needed when methods follow
} // Enum can have methods — returns a readable string for each constant
public String displayName() {
switch (this) { // 'this' refers to the current enum constant
case ELECTRONICS: return "Electronics";
case FOOD: return "Food";
case CLOTHING: return "Clothing";
case SPORTS: return "Sports";
default: return this.name(); // fallback: returns "ELECTRONICS" etc.
}
}// In Product.java — Category used as a field type
private Category category;
new Product(101, "Laptop", 50000.00, Category.ELECTRONICS); // type-safe, can't pass "Electronics"
// In ProductServiceImpl.java — iterating all enum values
for (Category cat : Category.values()) { // [ELECTRONICS, FOOD, CLOTHING, SPORTS]
displayByCategory(cat);
}
// In ProductServiceImpl.java — filtering by category
.filter(p -> p.getCategory() == category) // enum comparison with ==, not .equals()🔑 How it works:
Category.ELECTRONICSis a singleton constant — you compare with==(not.equals())Category.values()gives you[ELECTRONICS, FOOD, CLOTHING, SPORTS]— great for loopingdisplayName()lets you print"Electronics"instead of the raw constant name"ELECTRONICS"- If you try to pass a
Stringwhere aCategoryis expected, Java gives a compile error — this is type safety
- Encapsulation — all fields
private, exposed only viapublicgetters - Enum as a field type —
Categoryenum used instead of a rawStringfor type safety - Formatted Output —
System.out.printf()with format specifiers for aligned display - Object Design —
display()method keeps formatting logic inside the class (responsibility principle)
id,name,price,categoryare allprivate— no direct access from outsideCategory categoryas a field means only valid enum values are accepted — you cannot accidentally assign"Electrnics"(typo)display()uses%2d,%-20s,%8.2fformat specifiers — right-aligns ID, left-aligns name, formats price to 2 decimal placescategory.displayName()delegates to the enum method — Product doesn't need to know how to format the category name
public class Product {
private int id; // primitive — product ID
private String name; // Wrapper class — product name
private double price; // primitive — product price
private Category category; // Enum type — only valid categories allowed
// Constructor: all 4 fields required at creation
public Product(int id, String name, double price, Category category) {
this.id = id;
this.name = name;
this.price = price;
this.category = category;
}
} // public getters — read-only access to private fields
public int getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
public Category getCategory(){ return category; }
// Note: no setters — product data doesn't change after creation // Formats and prints product in a clean table row
public void display() {
System.out.printf(" [%2d] %-20s $%8.2f [%s]%n",
id, // %2d → right-aligned integer, width 2
name, // %-20s → left-aligned string, width 20
price, // %8.2f → right-aligned float, 2 decimal places
category.displayName()); // calls enum method → "Electronics", "Food" etc.
}
// Output example:
// [101] Laptop $ 50000.00 [Electronics]
// [201] Basmati Rice 5kg $ 450.00 [Food]// Category enum ensures only valid categories can be passed
products.add(new Product(101, "Laptop", 50000.00, Category.ELECTRONICS));
products.add(new Product(201, "Basmati Rice 5kg", 450.00, Category.FOOD));
products.add(new Product(301, "Cotton T-Shirt", 599.00, Category.CLOTHING));
products.add(new Product(401, "Cricket Bat", 1800.00, Category.SPORTS));
// ❌ new Product(101, "Laptop", 50000.00, "Electronics") — compile error! String ≠ Category🔑 How it works:
Category categoryas a field type means Java won't compile if you try to pass a plain string- Getters expose values, but nothing can modify a product's price or name after it's created
display()is a behaviour that belongs toProduct— it knows its own fields, so it formats itselfprintfformat strings keep the output table aligned regardless of name/price length
- Interface — defines what a product service must be able to do, not how
- Abstraction — callers don't need to know if products come from a file, DB, or hardcoded list
- Collections in Interface —
List<Category>as a return type shows interfaces can use generics - Exception Declaration —
throws InvalidProductExceptionbaked into the contract
displayAllProducts()— shows every product across all categoriesdisplayByCategory(Category)— filters to one category; usesCategoryenum for type safetygetProductById(int id)— search by ID; declared to throwInvalidProductExceptionif not foundgetAvailableCategories()— returns aList<Category>(a collection), useful for building category menus- Any future implementation (e.g.,
DatabaseProductService) must provide all 4 methods
public interface ProductService {
void displayAllProducts(); // show all 20 products grouped by category
void displayByCategory(Category category); // filter by enum — type-safe
// declares exception: caller must handle or rethrow
Product getProductById(int id) throws InvalidProductException;
List<Category> getAvailableCategories(); // returns Collection of category enums
}🔑 How it works:
ProductServiceImplimplements this — but any other class could too (e.g.,DatabaseProductServiceImpl)- The interface means calling code only depends on
ProductService, not the concrete class — easy to swap implementationsthrows InvalidProductExceptionon the interface forces the implementing class to propagate or handle this exception
- Collections —
ArrayList<Product>stores the 20-product catalog - Stream API —
.stream().filter().collect()for functional-style category filtering - Lambda Expression —
p -> p.getCategory() == categoryis an inline function - Exception Handling — throws
InvalidProductExceptionwhen product ID not found - Constructor seeding — product catalog is populated when the object is created
- 20 products are added in the constructor — 5 per category (Electronics, Food, Clothing, Sports)
displayByCategory()uses Java Streams to filter without writing a manual loopgetProductById()does a linear search — if no match, throws exception instead of returningnullArrays.asList(Category.values())wraps the enum array into aList— this is a fixed-size listdisplayAllProducts()loops overCategory.values()and callsdisplayByCategory()for each
public class ProductServiceImpl implements ProductService {
// Collection: stores entire product catalog
private List<Product> products = new ArrayList<>();
public ProductServiceImpl() {
// Seeded with 5 products per category at construction time
products.add(new Product(101, "Laptop", 50000.00, Category.ELECTRONICS));
products.add(new Product(102, "Smartphone", 20000.00, Category.ELECTRONICS));
products.add(new Product(103, "Headphones", 1500.00, Category.ELECTRONICS));
products.add(new Product(201, "Basmati Rice 5kg", 450.00, Category.FOOD));
products.add(new Product(202, "Olive Oil 1L", 650.00, Category.FOOD));
products.add(new Product(301, "Cotton T-Shirt", 599.00, Category.CLOTHING));
products.add(new Product(302, "Denim Jeans", 1299.00, Category.CLOTHING));
products.add(new Product(401, "Cricket Bat", 1800.00, Category.SPORTS));
products.add(new Product(402, "Football", 650.00, Category.SPORTS));
// ... 20 total
}
} @Override
public void displayByCategory(Category category) {
// Stream API: functional-style filtering of the ArrayList
List<Product> filtered = products.stream() // convert list to stream
.filter(p -> p.getCategory() == category) // lambda: keep matching items
.collect(Collectors.toList()); // collect results to new List
// Without streams, this would be a manual for-loop with an if-check inside
for (Product p : filtered) p.display(); // OOP: each product displays itself
} @Override
public Product getProductById(int id) throws InvalidProductException {
// Linear search through ArrayList
for (Product p : products) {
if (p.getId() == id) return p; // found — return immediately
}
// Not found — throw custom exception with helpful message
// ✅ Better than returning null (which would cause NullPointerException later)
throw new InvalidProductException("No product found with ID: " + id);
} @Override
public List<Category> getAvailableCategories() {
// Arrays.asList wraps enum array into a List
return Arrays.asList(Category.values());
// Returns: [ELECTRONICS, FOOD, CLOTHING, SPORTS]
}🔑 How it works:
- Constructor runs once when
new ProductServiceImpl()is called — catalog is ready immediately.stream().filter(lambda).collect()replaces a manual loop + if-check in one readable line- Throwing
InvalidProductExceptioninstead of returningnullprevents hiddenNullPointerExceptionbugs laterArrays.asList()creates an unmodifiable list — you can read it but not add/remove from it
- Object Composition —
CartItemHAS-AProduct(not inherits from it) - Encapsulation —
privatefields, one setter (setQuantity) for controlled mutation - Delegation —
getTotalPrice()delegates price lookup to theProductobject - Single Responsibility — this class only knows about quantity + its product
CartItemwraps aProductreference — it does not copy the product's data, just holds a pointer to itsetQuantity()is the only setter — because quantity can change (add more of same item), but the product itself doesn't changegetTotalPrice()=product.getPrice() * quantity— delegates price toProduct, not hardcoded- When
CartServiceImplfinds the same product already in cart, it callsitem.setQuantity(old + new)
public class CartItem {
// Composition: CartItem HAS-A Product (not IS-A Product)
private Product product; // holds reference to a Product object
private int quantity;
public CartItem(Product product, int quantity) {
this.product = product; // store the Product object reference
this.quantity = quantity;
}
} // Delegation: asks the Product for its price — doesn't store it separately
public double getTotalPrice() {
return product.getPrice() * quantity;
// e.g., Laptop ($50000) x 2 = $100000
}
// Only setter: quantity can be updated when same item is added again
public void setQuantity(int quantity) {
this.quantity = quantity;
}
// Getters — read-only access to fields
public Product getProduct() { return product; }
public int getQuantity() { return quantity; }🔑 How it works:
CartItemdoesn't copy price/name fromProduct— it holds the actualProductobject- If
product.getPrice()ever changed (in a more advanced version),getTotalPrice()would automatically reflect itsetQuantity()is allowed because the same product can be added multiple times — quantity increases- This is Composition:
CartItemis built from aProduct, not a subtype of it
- Interface — contract for all cart operations, no implementation
- Exception Declaration —
checkout()declaresthrows PaymentException - Return Types —
booleansignals success/failure to the caller - Abstraction — hides all cart storage and logic details
- 6 methods defined:
addToCart,removeFromCart,viewCart,calculateTotal,checkout,isCartEmpty checkout()is the only method that declares a checked exception — payment failure is criticalisCartEmpty()is a utility method — used to guard against checking out with nothing in the cartcalculateTotal()returnsdouble— the grand total of all items in cart
public interface CartService {
void addToCart(Product product, int quantity); // add item to cart
void removeFromCart(int productId); // remove by product ID
void viewCart(); // display formatted cart
double calculateTotal(); // sum of all item totals
// Declares checked exception — caller MUST handle PaymentException
boolean checkout(double amountEntered) throws PaymentException;
boolean isCartEmpty(); // true if cart has no items
}🔑 How it works:
- The
throws PaymentExceptiononcheckout()means any code calling this method must usetry-catch— Java enforces it at compile timeisCartEmpty()returnsboolean— used inECommerceAppto prevent empty-cart checkout attempts- Separating the interface from implementation means you could replace
CartServiceImplwith aDatabaseCartServicewithout touching calling code
- Collections —
ArrayList<CartItem>stores the cart items - Exception Handling — throws
PaymentExceptionfor invalid payment - Interface Implementation — implements all 6
CartServicemethods - Formatted Output —
printffor aligned cart display - Wrapper Classes —
Doubleimplicitly used inString.format()
addToCart()first checks if the product already exists in the cart — if yes, increments quantity; if no, adds newCartItemremoveFromCart()does NOT throw an exception on missing ID — it just prints a message (removal failure is non-critical)viewCart()usesprintfto format a table with ID, name, quantity, and total price per itemcalculateTotal()sums upitem.getTotalPrice()across all items in theArrayListcheckout()throwsPaymentExceptionif amount ≤ 0, prints error if underpaid or overpaid, clears cart on exact matchcart.clear()on success empties theArrayList— equivalent to placing the order
private List<CartItem> cart = new ArrayList<>(); // Collection: holds cart items
@Override
public void addToCart(Product product, int quantity) {
// Check if product already in cart — update quantity if so
for (CartItem item : cart) {
if (item.getProduct().getId() == product.getId()) {
item.setQuantity(item.getQuantity() + quantity); // increment
System.out.printf(" ✔ Updated: %-20s → Qty: %d%n", product.getName(), item.getQuantity());
return; // stop here — no need to add again
}
}
// Product not in cart yet — add as new CartItem
cart.add(new CartItem(product, quantity));
System.out.printf(" ✔ Added: %-20s x%d to cart.%n", product.getName(), quantity);
}@Override
public void removeFromCart(int productId) {
for (CartItem item : cart) {
if (item.getProduct().getId() == productId) {
System.out.println(" ✔ Removed: " + item.getProduct().getName());
cart.remove(item); // ArrayList.remove() — removes the CartItem object
return;
}
}
// No exception thrown — just informational message (non-critical failure)
System.out.println(" ✘ Product ID " + productId + " not found in cart.");
}@Override
public void viewCart() {
if (cart.isEmpty()) { // ArrayList.isEmpty()
System.out.println(" Your cart is empty.");
return;
}
System.out.printf(" %-6s %-22s %-6s %10s%n", "ID", "Product", "Qty", "Total");
for (CartItem item : cart) {
System.out.printf(" [%3d] %-22s x%-4d $%8.2f%n",
item.getProduct().getId(),
item.getProduct().getName(),
item.getQuantity(),
item.getTotalPrice()); // delegates to CartItem.getTotalPrice()
}
System.out.printf(" %-33s $%8.2f%n", "TOTAL:", calculateTotal());
}@Override
public boolean checkout(double amountEntered) throws PaymentException {
// Exception Handling: invalid input → throw checked exception
if (amountEntered <= 0) {
throw new PaymentException("Payment amount must be positive. Entered: $" + amountEntered);
}
double total = calculateTotal();
if (amountEntered == total) {
System.out.println(" ✔ Payment received. Order confirmed! 🎉");
cart.clear(); // ArrayList.clear() — empties cart after successful payment
return true;
} else if (amountEntered < total) {
System.out.printf(" ✘ Insufficient. Entered $%.2f but due is $%.2f%n", amountEntered, total);
} else {
System.out.printf(" ✘ Overpayment. Please enter exactly $%.2f%n", total);
}
return false;
}🔑 How it works:
ArrayList<CartItem>grows/shrinks as items are added/removedaddToCart()avoids duplicate entries — finds and updates instead of adding a second entrycheckout()validates amount first, throwsPaymentExceptionfor zero/negative values, then checks for exact matchcart.clear()after payment mirrors a real system where the cart is emptied after order placementremoveFromCart()uses areturnafter removing — avoidsConcurrentModificationExceptionby stopping iteration immediately
- Custom Exception — extends Java's built-in
Exceptionclass - Inheritance —
InvalidUserExceptionIS-AExceptionviaextends - Checked Exception — compiler forces all callers to handle or declare it
super()call — passes the message up toException's constructor
- Extends
Exception(notRuntimeException) → this makes it a checked exception - The compiler will not let you call
signup()without atry-catchorthrowsdeclaration super(message)stores the message in the parentException— retrievable viae.getMessage()- Used in:
UserServiceImpl.signup()for username/password/email/mobile validation failures
// Inheritance: extends Exception → makes this a CHECKED exception
public class InvalidUserException extends Exception {
public InvalidUserException(String message) {
super(message); // passes message to Exception's constructor
// Caller can later do: e.getMessage() → "Username cannot be empty."
}
}// THROWING (in UserServiceImpl):
private void validateUsername(String username) throws InvalidUserException {
if (username.length() < 3)
throw new InvalidUserException("Username must be at least 3 characters.");
}
// CATCHING (in ECommerceApp / calling code):
try {
userService.signup("Al", "pass123", "al@gmail.com", "9876543210");
} catch (InvalidUserException e) {
System.out.println(" ✘ Signup failed: " + e.getMessage());
// prints: ✘ Signup failed: Username must be at least 3 characters.
}🔑 How it works:
extends Exception= checked → Java compiler enforces handling at the call sitesuper(message)stores the error message in the parent class — you retrieve it withe.getMessage()- Every specific validation failure gets its own descriptive message, making debugging easy
- If you forget the
try-catch, the code won't compile — this is the key difference from unchecked exceptions
- Custom Exception — extends
RuntimeException - Inheritance —
InvalidProductExceptionIS-ARuntimeException - Unchecked Exception — compiler does NOT force handling; optional
try-catch super()call — message passed toRuntimeException
- Extends
RuntimeException→ unchecked — callers don't have to usetry-catch - Thrown by
ProductServiceImpl.getProductById()when an ID doesn't exist - Used for "programmer errors" or invalid input that shouldn't normally happen
- If uncaught, it will crash the program with a stack trace — which is acceptable for unexpected states
// Inheritance: extends RuntimeException → UNCHECKED exception
// Compiler does NOT force callers to handle this
public class InvalidProductException extends RuntimeException {
public InvalidProductException(String message) {
super(message); // same pattern — message stored in RuntimeException
}
}// THROWING (in ProductServiceImpl):
public Product getProductById(int id) throws InvalidProductException {
for (Product p : products) {
if (p.getId() == id) return p;
}
throw new InvalidProductException("No product found with ID: " + id);
}
// CATCHING — optional, but good practice:
try {
Product p = productService.getProductById(999);
} catch (InvalidProductException e) {
System.out.println(" ✘ " + e.getMessage());
// prints: ✘ No product found with ID: 999
}
// WITHOUT try-catch — also valid (unchecked), but will crash if ID is invalid:
Product p = productService.getProductById(999); // compiles fine, crashes at runtime🔑 How it works:
extends RuntimeException— the compiler doesn't forcetry-catcharound calls togetProductById()- Still a good practice to catch it — but Java won't stop you from skipping the catch block
- Throwing this is better than returning
null— anullcan silently causeNullPointerException10 lines later
- Custom Exception — extends
Exception - Inheritance —
PaymentExceptionIS-AException - Checked Exception — compiler forces handling at the call site
- Domain-Specific Exception — named after the business operation it protects
- Same structure as
InvalidUserException— extendsException→ checked - Thrown only inside
CartServiceImpl.checkout()when payment amount is ≤ 0 - Calling code (in
ECommerceApp) must wrapcheckout()in atry-catch (PaymentException e) - Makes the payment flow explicit — the method signature itself tells you "this can fail"
// Inheritance: extends Exception → CHECKED exception
public class PaymentException extends Exception {
public PaymentException(String message) {
super(message); // stores message in Exception
}
}// THROWING (in CartServiceImpl.checkout):
if (amountEntered <= 0) {
throw new PaymentException("Payment amount must be positive. Entered: $" + amountEntered);
}
// CATCHING (in ECommerceApp):
try {
boolean success = cartService.checkout(amountEntered);
if (success) System.out.println(" ✔ Order placed!");
} catch (PaymentException e) {
System.out.println(" ✘ Payment error: " + e.getMessage());
// prints: ✘ Payment error: Payment amount must be positive. Entered: $-50.0
}// CHECKED (InvalidUserException, PaymentException — extends Exception):
// → Compiler ERROR if you don't handle it:
userService.signup(...); // ❌ compile error — must add try-catch or throws
cartService.checkout(amount); // ❌ compile error — must add try-catch or throws
// UNCHECKED (InvalidProductException — extends RuntimeException):
// → Compiler is SILENT, but crashes at runtime if not handled:
productService.getProductById(999); // ✅ compiles — but crashes if ID not found and not caught🔑 How it works:
- Three custom exceptions follow the same structure but differ in what they extend
extends Exception= checked = compiler enforced = use for critical business operations (signup, payment)extends RuntimeException= unchecked = optional handling = use for programming errors (invalid ID lookup)- All three use
super(message)to store a descriptive message readable viae.getMessage()
- Encapsulation →
User,Product,CartItem—privatefields,publicgetters, no direct field access - Interface →
UserService,ProductService,CartService— contract-based design, separates what from how - Interface Implementation →
UserServiceImpl,ProductServiceImpl,CartServiceImpl—implementskeyword,@Overrideon every method - Object Composition →
CartItemHAS-AProduct— not inheritance, but embedding one object inside another - Enum →
Category— fixed constantsELECTRONICS,FOOD,CLOTHING,SPORTSwith adisplayName()method
- ArrayList → used in all 3
*ServiceImplclasses — storesUser,Product, andCartItemobjects - List interface → return type of
getAvailableCategories()and field type of all lists - Stream API →
ProductServiceImpl.displayByCategory()—.stream().filter(lambda).collect() - Lambda →
p -> p.getCategory() == category— inline function passed to.filter() - Arrays.asList() → wraps
Category.values()enum array into a fixedList - ArrayList methods used →
.add(),.remove(),.clear(),.isEmpty(), enhanced for-loop
- Custom Checked Exception →
InvalidUserException,PaymentException— extendException, compiler enforced - Custom Unchecked Exception →
InvalidProductException— extendsRuntimeException, optional handling throw→ used inside methods to trigger an exception immediatelythrows→ declared on method signatures to warn callers of possible exceptionstry-catch→ used in calling code to handle checked exceptions gracefullysuper(message)→ passes the error message to the parent exception classe.getMessage()→ retrieves the stored message from a caught exception
String→ wrapper class used throughout; methods like.equals(),.equalsIgnoreCase(),.trim(),.matches(),.indexOf(),.substring()Double→ implicitly used inString.format("%.2f", total)printfformatting →%2d,%-20s,%8.2f,%nused for aligned console outputthiskeyword → used in constructors to distinguish field from parameter@Override→ annotation confirming interface method is correctly implemented- Regex →
"\\d{10}"invalidateMobile()— matches exactly 10 numeric digits
# Compile all files
javac E_CommerceWebsite2/*.java
# Run the main application
java E_CommerceWebsite2.ECommerceAppStart
└── Signup (validates username, password, email, mobile)
└── Login (checks credentials)
└── Browse Products (by category or all)
└── Add to Cart (by product ID + quantity)
└── View Cart (formatted table with totals)
└── Checkout (enter exact amount → PaymentException if invalid)
└── Order Confirmed / Error Message