Big Picture — What Are We Building?
The ShopEase e-commerce platform powers every example in this guide
Imagine you are building ShopEase — a simple online shopping app. A customer opens the
app, browses products, adds to cart, places an order, and gets a confirmation. Every topic in this guide
is one piece of that app. We will keep coming back to the same example so nothing feels random.
System Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ CLIENT TIER │
│ 🌐 Browser / Mobile App / Postman (HTTP Requests) │
└────────────────────────────┬────────────────────────────────────────┘
│ REST / HTTP
▼
┌─────────────────────────────────────────────────────────────────────┐
│ API GATEWAY (Port 8080) │
│ Routes requests to the right microservice │
└──────┬───────────────────────┬──────────────────────┬───────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Product │ │ Order Service │ │ User Service │
│ Service │ │ (Port 8082) │ │ (Port 8083) │
│ (Port 8081)│ │ │ │ │
└──────┬──────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
▼ ┌────────┘ ┌─────────────┘
┌─────────────┐ ▼ ▼
│ Redis Cache │ Azure ┌─────────────────────────────┐
│ (fast data) │ Service Bus ──▶│ Notification Service │
└─────────────┘ (messages) │ (Port 8084) │
│ └─────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ DATA TIER │
│ MySQL Database | Azure Key Vault (secrets/passwords) │
└─────────────────────────────────────────────────────────────────────┘
Components at a Glance
| Component | What It Does in ShopEase | Technology |
|---|---|---|
| Client | Browser / mobile app that calls APIs | Any HTTP client |
| API Gateway | Single entry point, routes to services | Spring Cloud Gateway |
| Product Service | List products, get product details | Spring Boot + MySQL |
| Order Service | Place orders, track status | Spring Boot + MySQL |
| User Service | Register, login, profile | Spring Boot + MySQL |
| Notification Service | Send order confirmation emails/SMS | Spring Boot |
| MySQL | Persistent storage for all services | MySQL 8 + JPA/Hibernate |
| Redis Cache | Cache product catalog to reduce DB calls | Redis + Spring Cache |
| Azure Key Vault | Store DB passwords, API keys safely | Azure + Spring Cloud |
| Azure Service Bus | Order placed → notify user (async) | Azure Service Bus + Spring |
Remember this flow: Customer places order → Order Service saves to MySQL → publishes event to Service Bus → Notification Service picks it up → sends email. This one flow touches every concept in this guide.
1. Client–Server Concept
How a browser talks to your Spring Boot app
Client = anyone who asks for data (browser, mobile app, Postman).
Server = the program that handles the request and sends back a response.
They communicate using HTTP — the language of the web.
Request–Response Cycle
STEP 1: Client sends HTTP Request
──────────────────────────────────────────────────────────
GET /api/products/101 HTTP/1.1
Host: shopease.com
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
──────────────────────────────────────────────────────────
STEP 2: Server processes it and sends HTTP Response
──────────────────────────────────────────────────────────
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 101,
"name": "Wireless Headphones",
"price": 2499.00,
"stock": 50
}
──────────────────────────────────────────────────────────
HTTP Methods (CRUD)
| Method | Action | ShopEase Example | Status Code |
|---|---|---|---|
| GET | Read data | Get product details | 200 OK |
| POST | Create data | Place a new order | 201 Created |
| PUT | Update data (full) | Update product details | 200 OK |
| PATCH | Update data (partial) | Update only product price | 200 OK |
| DELETE | Delete data | Remove a product | 204 No Content |
Spring Boot: Your First REST Endpoint
ProductController.java — Product Service@RestController
@RequestMapping("/api/products")
public class ProductController {
@Autowired
private ProductService productService;
// GET /api/products → list all products
@GetMapping
public List<Product> getAllProducts() {
return productService.getAllProducts();
}
// GET /api/products/101 → get one product
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product); // 200 OK
}
// POST /api/products → create a product
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
Product saved = productService.save(product);
return ResponseEntity.status(HttpStatus.CREATED).body(saved); // 201 Created
}
// DELETE /api/products/101 → delete a product
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.delete(id);
return ResponseEntity.noContent().build(); // 204 No Content
}
}
@RestController = @Controller + @ResponseBody. It automatically converts your Java object to JSON and sends it back in the HTTP response. You don't write any JSON manually — Spring does it for you.
JSON — The Language of APIs
// Java Object ──Spring Boot converts──▶ JSON Response
public class Product {
private Long id; // "id": 101
private String name; // "name": "Headphones"
private double price; // "price": 2499.00
// getters + setters
}
// Client receives:
// { "id": 101, "name": "Headphones", "price": 2499.00 }
2. Core Java Essentials
OOP, Collections, Streams, Exception Handling — all with ShopEase
Object-Oriented Programming (OOP) — 4 Pillars
1. Encapsulation
Hide internal data. Only expose what is needed via getters/setters.
2. Inheritance
Child class inherits behavior from parent. OrderItem extends BaseEntity
3. Polymorphism
Same method, different behavior. PaymentService has CreditCard, UPI, NetBanking
4. Abstraction
Hide implementation details. Interface hides HOW, shows WHAT.
// ── ABSTRACTION: interface defines the contract ──
public interface PaymentService {
boolean processPayment(double amount, String customerId);
}
// ── INHERITANCE + POLYMORPHISM: multiple implementations ──
public class CreditCardPayment implements PaymentService {
@Override
public boolean processPayment(double amount, String customerId) {
System.out.println("Charging credit card for " + customerId);
return true; // simplified
}
}
public class UpiPayment implements PaymentService {
@Override
public boolean processPayment(double amount, String customerId) {
System.out.println("Processing UPI payment for " + customerId);
return true;
}
}
// ── ENCAPSULATION: Product class hides its fields ──
public class Product {
private Long id; // private = encapsulated
private String name;
private double price;
private int stock;
// Only way to access = through getters/setters
public Long getId() { return id; }
public void setPrice(double price) {
if (price < 0) throw new IllegalArgumentException("Price cannot be negative");
this.price = price; // validation inside setter
}
public double getPrice() { return price; }
// ... other getters/setters
}
// ── POLYMORPHISM in action ──
PaymentService payment = new UpiPayment(); // pick at runtime
payment.processPayment(2499.00, "cust_123"); // calls UPI version
payment = new CreditCardPayment();
payment.processPayment(2499.00, "cust_123"); // calls CreditCard version
Collections — Managing Lists of Data
| Collection | Use When | ShopEase Usage |
|---|---|---|
ArrayList<E> | Ordered list, fast read | List of products in cart |
LinkedList<E> | Frequent inserts/deletes | Order processing queue |
HashSet<E> | Unique values, no duplicates | Set of unique category tags |
HashMap<K,V> | Key-value lookup | productId → Product map (cache) |
TreeMap<K,V> | Sorted key-value | Products sorted by price |
LinkedHashMap<K,V> | Insertion-order map | Recent searches in order |
// ── ArrayList: ordered list of cart items ──
List<Product> cart = new ArrayList<>();
cart.add(new Product(101L, "Headphones", 2499.0));
cart.add(new Product(102L, "Phone Case", 299.0));
cart.add(new Product(101L, "Headphones", 2499.0)); // duplicates allowed
System.out.println(cart.size()); // 3
// ── HashSet: unique product IDs viewed ──
Set<Long> recentlyViewed = new HashSet<>();
recentlyViewed.add(101L);
recentlyViewed.add(102L);
recentlyViewed.add(101L); // ignored — already exists
System.out.println(recentlyViewed.size()); // 2
// ── HashMap: product catalogue (id → product) ──
Map<Long, Product> catalogue = new HashMap<>();
catalogue.put(101L, new Product(101L, "Headphones", 2499.0));
catalogue.put(102L, new Product(102L, "Phone Case", 299.0));
Product p = catalogue.get(101L); // O(1) lookup by ID
System.out.println(p.getName()); // Headphones
// ── Iteration ──
for (Map.Entry<Long, Product> entry : catalogue.entrySet()) {
System.out.println(entry.getKey() + " → " + entry.getValue().getName());
}
// ── Sorting with Comparator ──
cart.sort(Comparator.comparingDouble(Product::getPrice)); // sort by price asc
cart.sort(Comparator.comparingDouble(Product::getPrice).reversed()); // desc
Streams API — Process Collections in One Line
Streams let you filter, transform, and aggregate collections without writing explicit loops.
Think of it like a conveyor belt: data flows through operations.
List<Product> products = Arrays.asList(
new Product(1L, "Headphones", 2499.0, "Electronics"),
new Product(2L, "Phone Case", 299.0, "Accessories"),
new Product(3L, "Laptop", 65999.0, "Electronics"),
new Product(4L, "USB Cable", 199.0, "Accessories"),
new Product(5L, "Keyboard", 3499.0, "Electronics")
);
// ── filter: only Electronics products ──
List<Product> electronics = products.stream()
.filter(p -> p.getCategory().equals("Electronics"))
.collect(Collectors.toList());
// Result: Headphones, Laptop, Keyboard
// ── map: extract just the names ──
List<String> names = products.stream()
.map(Product::getName) // p -> p.getName()
.collect(Collectors.toList());
// Result: ["Headphones", "Phone Case", "Laptop", ...]
// ── filter + map + sorted: names of products under 5000, sorted ──
List<String> affordable = products.stream()
.filter(p -> p.getPrice() < 5000)
.sorted(Comparator.comparingDouble(Product::getPrice))
.map(Product::getName)
.collect(Collectors.toList());
// Result: ["USB Cable", "Phone Case", "Headphones", "Keyboard"]
// ── reduce: total cart value ──
double total = products.stream()
.mapToDouble(Product::getPrice)
.sum();
// Result: 72495.0
// ── findFirst: cheapest product ──
Optional<Product> cheapest = products.stream()
.min(Comparator.comparingDouble(Product::getPrice));
cheapest.ifPresent(p -> System.out.println("Cheapest: " + p.getName()));
// ── groupingBy: group by category ──
Map<String, List<Product>> byCategory = products.stream()
.collect(Collectors.groupingBy(Product::getCategory));
// Result: { "Electronics": [...], "Accessories": [...] }
// ── count ──
long electronicsCount = products.stream()
.filter(p -> p.getCategory().equals("Electronics"))
.count(); // 3
Exception Handling — When Things Go Wrong
Exception Hierarchy:
Throwable
├── Error (JVM errors — don't catch these)
│ └── OutOfMemoryError, StackOverflowError
└── Exception
├── Checked Exception (must handle or declare)
│ └── IOException, SQLException
└── RuntimeException (unchecked — optional to handle)
└── NullPointerException, IllegalArgumentException,
ArrayIndexOutOfBoundsException
// ── 1. Custom Exception ──
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super("Product not found with ID: " + id);
}
}
public class InsufficientStockException extends RuntimeException {
public InsufficientStockException(Long productId, int requested, int available) {
super("Product " + productId + " has only " + available +
" items but " + requested + " were requested");
}
}
// ── 2. Throwing custom exceptions in Service ──
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
// ↑ throws 404-worthy exception
}
public void placeOrder(Long productId, int quantity) {
Product product = getProductById(productId);
if (product.getStock() < quantity) {
throw new InsufficientStockException(productId, quantity, product.getStock());
}
// proceed with order
}
}
// ── 3. Global exception handler — catches ALL exceptions in one place ──
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ProductNotFoundException ex) {
ErrorResponse error = new ErrorResponse(404, ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(InsufficientStockException.class)
public ResponseEntity<ErrorResponse> handleStock(InsufficientStockException ex) {
ErrorResponse error = new ErrorResponse(400, ex.getMessage());
return ResponseEntity.badRequest().body(error);
}
@ExceptionHandler(Exception.class) // catch-all
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
ErrorResponse error = new ErrorResponse(500, "Something went wrong");
return ResponseEntity.internalServerError().body(error);
}
}
// ── 4. Error Response DTO ──
public class ErrorResponse {
private int status;
private String message;
private LocalDateTime timestamp = LocalDateTime.now();
// constructor + getters
}
Generics — Write Once, Use for Any Type
Generics let you write classes and methods that work with any data type
while keeping type safety at compile time.
// ── Without generics — unsafe ──
List list = new ArrayList();
list.add("hello");
list.add(123); // no error at compile time!
String s = (String) list.get(1); // ClassCastException at runtime 💥
// ── With generics — safe ──
List<String> names = new ArrayList<>();
names.add("hello");
// names.add(123); // COMPILE ERROR — caught early ✅
// ── Generic class: API response wrapper ──
public class ApiResponse<T> { // T = Type parameter
private boolean success;
private String message;
private T data; // can hold any type
public ApiResponse(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
}
// getters...
}
// ── Usage in controller ──
@GetMapping("/{id}")
public ApiResponse<Product> getProduct(@PathVariable Long id) {
Product product = productService.getProductById(id);
return new ApiResponse<>(true, "Product found", product);
// Response: { "success": true, "message": "Product found", "data": {...} }
}
@GetMapping
public ApiResponse<List<Product>> getAllProducts() {
List<Product> products = productService.getAllProducts();
return new ApiResponse<>(true, "Success", products);
}
// ── Generic method ──
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(true, "OK", data);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, message, null);
}
3. Microservices Architecture
Break the app into small, independent services
Monolith vs Microservices
Monolith (Old Way)
┌──────────────────────────────┐ │ ONE BIG APPLICATION │ │ │ │ Users + Products + Orders │ │ + Payments + Notifications │ │ + Admin + Reports + ... │ │ │ │ Deploy as ONE .jar file │ └──────────────────────────────┘ Problem: If Product code has a bug, the ENTIRE app goes down. Scaling: you must scale everything.
Microservices (Modern Way)
┌──────────┐ ┌──────────┐ │ Product │ │ Order │ │ Service │ │ Service │ └──────────┘ └──────────┘ ┌──────────┐ ┌──────────┐ │ User │ │ Notif. │ │ Service │ │ Service │ └──────────┘ └──────────┘ Each is a separate .jar file. Product bug? Only Product restarts. Need to scale Orders? Scale only Orders.
Key Principles of Microservices
- 1Single Responsibility: Each service does ONE thing. Product Service only manages products. It doesn't care about orders or users.
- 2Own Database: Each service has its own MySQL database/schema. Order Service can't directly query Product Service's database.
- 3Communicate via API or Messages: Services talk to each other via REST calls or message queues (Service Bus).
- 4Independent Deployment: Deploy Product Service without touching Order Service.
- 5Independently Scalable: If Orders spike during a sale, scale only the Order Service.
ShopEase: Inter-Service Communication
Order Service calling Product Service via REST// ── Order Service needs product details ──
// It calls Product Service's REST API using RestTemplate or WebClient
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate; // HTTP client
@Autowired
private OrderRepository orderRepository;
public Order placeOrder(Long productId, int quantity, String userId) {
// Step 1: Call Product Service to check stock + get price
String productUrl = "http://product-service/api/products/" + productId;
Product product = restTemplate.getForObject(productUrl, Product.class);
if (product == null || product.getStock() < quantity) {
throw new InsufficientStockException("Not enough stock");
}
// Step 2: Save order to Order Service's own database
Order order = new Order();
order.setProductId(productId);
order.setQuantity(quantity);
order.setTotalAmount(product.getPrice() * quantity);
order.setUserId(userId);
order.setStatus("PLACED");
Order savedOrder = orderRepository.save(order);
// Step 3: Publish event to Azure Service Bus → Notification Service picks it up
orderEventPublisher.publishOrderPlaced(savedOrder);
return savedOrder;
}
}
// ── RestTemplate Bean Configuration ──
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Service Discovery: In a real app, services register with Eureka (Netflix) or Kubernetes. Then
http://product-service/ is resolved automatically — no hardcoded IPs needed.4. Spring Boot Application
Structure, DI, Layers, Configuration — building Product Service
Project Structure (Standard Layered Architecture)
product-service/
├── src/main/java/com/shopease/product/
│ ├── ProductServiceApplication.java ← Entry point
│ ├── controller/
│ │ └── ProductController.java ← Layer 1: Handle HTTP requests
│ ├── service/
│ │ ├── ProductService.java ← Layer 2: Business logic (interface)
│ │ └── ProductServiceImpl.java ← Implementation
│ ├── repository/
│ │ └── ProductRepository.java ← Layer 3: Database operations
│ ├── entity/
│ │ └── Product.java ← Database table mapping
│ ├── dto/
│ │ ├── ProductRequest.java ← What client sends
│ │ └── ProductResponse.java ← What server returns
│ ├── exception/
│ │ ├── ProductNotFoundException.java
│ │ └── GlobalExceptionHandler.java
│ └── config/
│ └── AppConfig.java ← Beans, Redis, etc.
└── src/main/resources/
└── application.properties ← All configuration
Request Flow Through Layers
Client (Postman)
│ GET /api/products/101
▼
┌──────────────────────┐
│ ProductController │ ← receives HTTP request
│ @RestController │ validates URL, path variables
└──────────┬───────────┘ calls service layer
│
▼
┌──────────────────────┐
│ ProductService │ ← business logic lives here
│ @Service │ check stock, apply discount
└──────────┬───────────┘ calls repository layer
│
▼
┌──────────────────────┐
│ ProductRepository │ ← talks to database
│ extends JpaRepo... │ SQL is auto-generated!
└──────────┬───────────┘
│
▼
MySQL DB
Entry Point
@SpringBootApplication // = @Configuration + @ComponentScan + @EnableAutoConfiguration
public class ProductServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
// Starts embedded Tomcat server on port 8081
}
}
Dependency Injection (DI) — Spring's Magic
Instead of creating objects with
new, you tell Spring "I need this" and Spring
gives it to you. This is called Inversion of Control (IoC).
// ── Without DI (old way) — tightly coupled, hard to test ──
public class ProductController {
private ProductService service = new ProductServiceImpl(); // You create it
private ProductRepository repo = new ProductRepository(); // You create it
}
// ── With DI (Spring way) — loosely coupled ──
@RestController
public class ProductController {
@Autowired // Spring injects it for you
private ProductService productService;
// OR use constructor injection (recommended):
}
@RestController
@RequiredArgsConstructor // Lombok generates constructor
public class ProductController {
private final ProductService productService; // Spring injects via constructor
}
// ── Spring manages beans with these annotations ──
@Component // Generic bean
@Service // Business logic layer
@Repository // Data access layer
@Controller // Web layer
@RestController // @Controller + @ResponseBody
application.properties
// src/main/resources/application.properties
# Server
server.port=8081
spring.application.name=product-service
# MySQL Database
spring.datasource.url=jdbc:mysql://localhost:3306/shopease_products
spring.datasource.username=${DB_USERNAME} # from environment / Key Vault
spring.datasource.password=${DB_PASSWORD} # NEVER hardcode passwords!
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA/Hibernate
spring.jpa.hibernate.ddl-auto=update # auto-create/update tables
spring.jpa.show-sql=true # logs all SQL (disable in prod)
spring.jpa.properties.hibernate.format_sql=true
# Redis
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
# Logging
logging.level.com.shopease=DEBUG
5. MySQL + Spring Data JPA
Save and query data without writing SQL — JPA/Hibernate does it for you
JPA (Java Persistence API) is a spec. Hibernate is its most popular
implementation. Spring Data JPA builds on top — you write a Java interface and
Spring writes the SQL queries automatically.
Entity — Java Class ↔ Database Table
// This Java class maps to a MySQL table called "products"
@Entity
@Table(name = "products")
@Data // Lombok: generates getters, setters, toString, equals
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // AUTO_INCREMENT
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(nullable = false)
private double price;
@Column(nullable = false)
private int stock;
@Column(length = 50)
private String category;
@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();
}
// Spring generates this SQL automatically:
// CREATE TABLE products (
// id BIGINT AUTO_INCREMENT PRIMARY KEY,
// name VARCHAR(100) NOT NULL,
// price DOUBLE NOT NULL,
// stock INT NOT NULL,
// category VARCHAR(50),
// created_at DATETIME
// );
Repository — Zero SQL for CRUD
// Extend JpaRepository → GET FREE CRUD methods!
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
// ↑Entity ↑ID type
// ─── FREE methods from JpaRepository ───
// save(product) → INSERT or UPDATE
// findById(id) → SELECT WHERE id = ?
// findAll() → SELECT * FROM products
// deleteById(id) → DELETE WHERE id = ?
// count() → SELECT COUNT(*)
// existsById(id) → SELECT 1 WHERE id = ?
// ─── Custom queries — just name the method! ───
List<Product> findByCategory(String category);
// SELECT * FROM products WHERE category = ?
List<Product> findByPriceLessThan(double price);
// SELECT * FROM products WHERE price < ?
List<Product> findByCategoryAndPriceLessThanOrderByPriceAsc(
String category, double maxPrice);
// SELECT * FROM products WHERE category = ? AND price < ? ORDER BY price ASC
Optional<Product> findByName(String name);
// ─── Custom JPQL query (Java, not SQL) ───
@Query("SELECT p FROM Product p WHERE p.stock > 0 ORDER BY p.price ASC")
List<Product> findAvailableProductsByPrice();
// ─── Native SQL query ───
@Query(value = "SELECT * FROM products WHERE price BETWEEN ?1 AND ?2",
nativeQuery = true)
List<Product> findByPriceRange(double min, double max);
}
Service Layer using Repository
@Service
@Transactional // all DB ops in this class are transactional
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductRepository productRepository;
@Override
public List<Product> getAllProducts() {
return productRepository.findAll();
}
@Override
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
@Override
public Product createProduct(Product product) {
return productRepository.save(product); // INSERT
}
@Override
public Product updateProduct(Long id, Product updated) {
Product existing = getProductById(id);
existing.setName(updated.getName());
existing.setPrice(updated.getPrice());
existing.setStock(updated.getStock());
return productRepository.save(existing); // UPDATE (id already set)
}
@Override
public void deleteProduct(Long id) {
if (!productRepository.existsById(id)) {
throw new ProductNotFoundException(id);
}
productRepository.deleteById(id);
}
}
Table Relationships
// One Order has MANY OrderItems (OneToMany)
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String userId;
private String status; // PLACED, SHIPPED, DELIVERED
private double totalAmount;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<OrderItem> items; // one order → many items
}
// One OrderItem belongs to ONE Order (ManyToOne)
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long productId;
private int quantity;
private double price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id") // foreign key column
private Order order;
}
6. Redis Cache
Speed up reads by storing frequently accessed data in memory
Redis is an in-memory data store — it's like a super-fast HashMap that lives
on a server. Instead of hitting MySQL every single time someone views a product, we store the
result in Redis. Next request → answer from Redis in <1ms instead of 10ms from MySQL.
Without Cache vs With Cache
WITHOUT CACHE (every request hits database):
───────────────────────────────────────────
1000 users view product 101
│
├─ Request 1 → MySQL query → 10ms
├─ Request 2 → MySQL query → 10ms
├─ Request 3 → MySQL query → 10ms ← same data, 1000 times!
└─ ...1000x → MySQL OVERLOADED 💥
WITH REDIS CACHE:
──────────────────────────────────────────
Request 1 → Cache MISS → MySQL query → store in Redis → 10ms
Request 2 → Cache HIT → Redis reply → 0.1ms ✅
Request 3 → Cache HIT → Redis reply → 0.1ms ✅
...999 more → Cache HIT → Redis reply → 0.1ms ✅
MySQL gets only 1 query instead of 1000! 🎉
Spring Cache Annotations
// ── 1. Enable caching in the app ──
@SpringBootApplication
@EnableCaching // ← add this!
public class ProductServiceApplication { ... }
// ── 2. application.properties ──
// spring.cache.type=redis
// spring.redis.host=localhost
// spring.redis.port=6379
// ── 3. Use @Cacheable on service methods ──
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductRepository productRepository;
// Cache result. Key = "product::101", "product::102" etc.
@Cacheable(value = "products", key = "#id")
public Product getProductById(Long id) {
System.out.println(">>> HIT DATABASE for product " + id); // only prints on cache miss
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
// Cache all products
@Cacheable(value = "all-products")
public List<Product> getAllProducts() {
return productRepository.findAll();
}
// When product is updated → delete the cache (stale data)
@CacheEvict(value = "products", key = "#id")
public Product updateProduct(Long id, Product updated) {
// ... update logic
}
// When product is deleted → remove from cache too
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
// Update the cache instead of deleting
@CachePut(value = "products", key = "#product.id")
public Product createProduct(Product product) {
return productRepository.save(product);
}
}
TTL — Cache Expiry
// Set Time-To-Live on cached data
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // cache expires in 10 minutes
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer())
);
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withCacheConfiguration("products", // product cache: 10 min
config.entryTtl(Duration.ofMinutes(10)))
.withCacheConfiguration("all-products", // all-products cache: 5 min
config.entryTtl(Duration.ofMinutes(5)))
.build();
}
}
Cache-Aside Pattern: App checks cache first. If miss → load from DB → store in cache → return to user. This is the most common pattern. Spring's @Cacheable implements this for you.
7. Azure Key Vault
Store secrets safely — never hardcode passwords in code
Imagine you need a MySQL password in your Spring Boot app. Wrong: write it in code or properties.
Right: store it in Azure Key Vault. Your app asks Key Vault at startup — nobody can see the
password in your Git repo.
The Problem Key Vault Solves
❌ WRONG — password visible in code/GitHub:
─────────────────────────────────────────────────
spring.datasource.password=MyDB@Pass123! ← exposed!
azure.servicebus.connection-string=Endpoint=sb://...SharedAccessKey=abc123 ← exposed!
✅ RIGHT — secrets stored in Azure Key Vault:
─────────────────────────────────────────────────
application.properties: (safe to commit)
spring.datasource.password=${shopease-db-password}
Azure Key Vault:
┌───────────────────────────────────┐
│ Secret Name │ Value │
│ shopease-db-password │ **** │ ← encrypted, access-controlled
│ shopease-db-username │ **** │
│ shopease-sb-conn-str │ **** │
└───────────────────────────────────┘
At startup → Spring fetches secrets from Key Vault → injects into app
Setup (Maven Dependency)
// pom.xml
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-starter-keyvault-secrets</artifactId>
</dependency>
application.properties (safe to commit)
# Azure Key Vault config
spring.cloud.azure.keyvault.secret.endpoint=https://shopease-vault.vault.azure.net/
spring.cloud.azure.keyvault.secret.property-sources[0].endpoint=https://shopease-vault.vault.azure.net/
# These values are FETCHED FROM KEY VAULT at startup:
# Secret name in Key Vault → property name here
# shopease-db-password → spring.datasource.password
# shopease-db-username → spring.datasource.username
# shopease-redis-password → spring.redis.password
spring.datasource.url=jdbc:mysql://shopease-db.mysql.database.azure.com:3306/products
spring.datasource.username=${shopease-db-username} # Key Vault fetches this
spring.datasource.password=${shopease-db-password} # Key Vault fetches this
How Authentication Works (Managed Identity)
// In Azure, your app (running on App Service / Container App) gets a
// "Managed Identity" — a built-in identity that Azure trusts.
// No credentials needed! Azure automatically authenticates your app to Key Vault.
// application.properties
spring.cloud.azure.credential.managed-identity-enabled=true
// OR for local development (using your Azure CLI login):
// Run: az login — then Spring uses your account automatically
// ── Programmatic access (when needed) ──
@Service
public class SecretService {
@Value("${shopease-db-password}") // Spring injects Key Vault secret!
private String dbPassword;
// You can also use SecretClient directly:
@Autowired
private SecretClient secretClient;
public String getSecret(String secretName) {
KeyVaultSecret secret = secretClient.getSecret(secretName);
return secret.getValue();
}
}
8. Azure Service Bus
Async messaging between microservices — decouple Order from Notification
Problem: When a customer places an order, we need to send a confirmation email. But why should
the Order Service wait for the email to be sent? What if the email server is slow?
Solution: Order Service drops a message on the Service Bus queue and moves on. Notification
Service picks it up at its own pace and sends the email.
Synchronous vs Asynchronous
❌ SYNCHRONOUS (tightly coupled):
──────────────────────────────────────────
OrderService → directly calls → NotificationService
- If Notification is down → Order FAILS ❌
- Order waits for email to be sent ❌
- Slow email server = slow order API ❌
✅ ASYNCHRONOUS (loosely coupled via Service Bus):
──────────────────────────────────────────────────
OrderService NotificationService
│ │
│ 1. Save order to DB │
│ 2. Drop msg to Service Bus │
│ 3. Return 201 Created ✅ │
│ │
│ Service Bus Queue │
│ [OrderPlacedEvent]─────────▶ │ 4. Pick up message
│ │ 5. Send email
│ │ (even if delayed - it will happen)
Queue vs Topic
| Queue | Topic (Pub/Sub) | |
|---|---|---|
| Consumers | One consumer reads each message | Multiple consumers, each gets a copy |
| Use case | Task distribution (one worker handles it) | One event → many services react |
| ShopEase example | Email sending (one email per order) | Order placed → Email + SMS + Inventory update |
Setup
// pom.xml
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-starter-servicebus-jms</artifactId>
</dependency>
// application.properties
spring.jms.servicebus.connection-string=${shopease-sb-connection-string} // from Key Vault!
spring.jms.servicebus.pricing-tier=standard
Publisher — Order Service sends event
// ── Event class (message payload) ──
public class OrderPlacedEvent {
private Long orderId;
private String userId;
private String customerEmail;
private double totalAmount;
private String productName;
private LocalDateTime placedAt;
// constructor + getters
}
// ── Publisher ──
@Service
public class OrderEventPublisher {
@Autowired
private JmsTemplate jmsTemplate;
public void publishOrderPlaced(Order order) {
OrderPlacedEvent event = new OrderPlacedEvent(
order.getId(),
order.getUserId(),
order.getCustomerEmail(),
order.getTotalAmount(),
order.getProductName(),
LocalDateTime.now()
);
jmsTemplate.convertAndSend("order-placed-queue", event);
// Message is now in Azure Service Bus queue ✅
System.out.println("Published OrderPlacedEvent for order: " + order.getId());
}
}
// ── In OrderService.placeOrder() ──
@Service
public class OrderService {
@Autowired private OrderRepository orderRepository;
@Autowired private OrderEventPublisher eventPublisher;
public Order placeOrder(OrderRequest request) {
Order order = new Order(/*...*/);
Order saved = orderRepository.save(order); // 1. save
eventPublisher.publishOrderPlaced(saved); // 2. publish event
return saved; // 3. return immediately
// Notification Service will send email asynchronously
}
}
Subscriber — Notification Service listens
// ── Listener in Notification Service ──
@Service
public class OrderEventListener {
@Autowired
private EmailService emailService;
@JmsListener(destination = "order-placed-queue")
public void onOrderPlaced(OrderPlacedEvent event) {
// This method is called whenever a new message arrives in the queue
System.out.println("Received order event for order: " + event.getOrderId());
// Send confirmation email
emailService.sendOrderConfirmation(
event.getCustomerEmail(),
event.getOrderId(),
event.getTotalAmount()
);
System.out.println("Email sent to " + event.getCustomerEmail());
}
}
// ── Email Service ──
@Service
public class EmailService {
public void sendOrderConfirmation(String email, Long orderId, double amount) {
// send email via SMTP / SendGrid / SES
System.out.println("Sending email to: " + email);
System.out.println("Your order #" + orderId + " of ₹" + amount + " is confirmed!");
}
}
Dead Letter Queue: If Notification Service fails to process a message (e.g., email server down), Azure Service Bus retries it automatically. After max retries, the message goes to a Dead Letter Queue so nothing is lost.
60+ Java Interview Questions
Click each question to reveal the answer. These are real questions from top companies.
☕ Core Java
What is the difference between
== and .equals() in Java? ▶== compares references (memory addresses). .equals() compares content/values. For Strings: "hello" == "hello" may be true (due to string pool) but new String("hello") == new String("hello") is false. Always use .equals() to compare String content.What is the difference between
ArrayList and LinkedList? ▶ArrayList: backed by array, fast random access O(1), slow insert/delete in middle O(n). LinkedList: doubly linked, slow random access O(n), fast insert/delete at ends O(1). Use ArrayList for most cases. Use LinkedList only when you frequently add/remove from the middle.
What is the difference between
HashMap and Hashtable? ▶HashMap: not thread-safe, allows one null key, faster. Hashtable: thread-safe (synchronized), no null keys, slower. Use
ConcurrentHashMap for thread safety instead of Hashtable.What is
final, finally, and finalize()? ▶final: keyword — variable can't be reassigned, method can't be overridden, class can't be extended. finally: block that always executes after try-catch (for cleanup). finalize(): deprecated method called by GC before object is garbage collected. In interviews say: "finalize is deprecated, use try-with-resources instead."
Explain
interface vs abstract class. ▶Interface: all methods abstract by default (Java 8+ allows default/static methods), no state, a class can implement multiple interfaces. Abstract class: can have both abstract and concrete methods, can have state (fields), only single inheritance. Use interface to define a contract; use abstract class for shared base behavior.
What is the difference between
Checked and Unchecked exceptions? ▶Checked: must be handled (try-catch) or declared with
throws. Extends Exception (not RuntimeException). Examples: IOException, SQLException. Unchecked: don't need to be declared. Extend RuntimeException. Examples: NullPointerException, IllegalArgumentException. In Spring Boot, use unchecked exceptions for cleaner code.What is a lambda expression? ▶
A lambda is a short anonymous function. Instead of:
new Comparator<Product>() { public int compare(Product a, Product b) { return Double.compare(a.getPrice(), b.getPrice()); } }, you write: (a, b) -> Double.compare(a.getPrice(), b.getPrice()). Lambdas work with functional interfaces (interfaces with exactly one abstract method): Comparator, Runnable, Predicate, Function, Consumer, Supplier.What is
Optional and why use it? ▶Optional is a container that may or may not hold a value — it's a better alternative to returning null. Instead of
return null (which causes NPE), return Optional.empty(). The caller must handle the absent case explicitly: optional.orElseThrow(() -> new ProductNotFoundException(id)). Used heavily in Spring Data JPA: repository.findById(id) returns Optional<Product>.What is String immutability and why is String stored in a pool? ▶
Strings in Java are immutable — once created, the value cannot change.
String s = "hello"; s = s + " world"; creates a NEW object, the original "hello" stays in memory. The String Pool is a cache — "hello" == "hello" is true because both point to the same pool entry. Benefit: thread safety, caching, security (passwords can't be changed mid-use).What is the difference between
String, StringBuilder, and StringBuffer? ▶String: immutable, thread-safe, creates new object on each modification — slow for concatenation in loops. StringBuilder: mutable, NOT thread-safe, fast — use in single-threaded code. StringBuffer: mutable, thread-safe (synchronized), slightly slower. Rule: use StringBuilder in loops, String for simple concatenation.
🏗️ OOP Concepts
Explain the 4 pillars of OOP with examples. ▶
Encapsulation: hiding data with private fields + getters/setters. Inheritance:
class Manager extends Employee — Manager inherits salary, name from Employee. Polymorphism: PaymentService pay = new UpiPayment() — same interface, different behavior. Abstraction: interface PaymentService { processPayment(); } — hides HOW payment works, shows WHAT it does.What is method overloading vs method overriding? ▶
Overloading (compile-time polymorphism): same method name, different parameters in the SAME class.
calculatePrice(int qty) and calculatePrice(int qty, double discount). Overriding (runtime polymorphism): child class provides a new implementation of parent's method. Must have same signature. Use @Override annotation. Key interview point: @Override is checked at compile time, preventing typos in method name.Can you override a static method? ▶
No. Static methods are hidden, not overridden. If a child class defines a static method with the same signature as the parent's static method, it hides the parent method — not polymorphism. The method called depends on the reference type (compile-time), not the actual object (runtime). Instance methods are polymorphic; static methods are not.
🌱 Spring Boot
What does
@SpringBootApplication do? ▶It is a meta-annotation combining three annotations: @Configuration (marks class as bean definition source), @ComponentScan (scans package and subpackages for @Component, @Service, @Repository, @Controller), @EnableAutoConfiguration (Spring Boot auto-configures beans based on classpath — if MySQL driver is on classpath, DataSource is auto-configured).
What is
@Autowired and Dependency Injection? ▶Dependency Injection: instead of creating dependencies with
new, Spring creates and injects them. @Autowired tells Spring to inject a bean. Types: field injection (@Autowired on field — not recommended), setter injection, constructor injection (recommended — immutable, testable). Use @RequiredArgsConstructor with final fields for clean constructor injection.What is the difference between
@Component, @Service, @Repository, @Controller? ▶All are specializations of
@Component (they ARE @Component). They differ in semantics and AOP behavior: @Service = business logic layer. @Repository = data access layer (also translates DB exceptions to Spring's DataAccessException). @Controller = web layer (handles HTTP). @RestController = @Controller + @ResponseBody (for REST APIs).What is
@Transactional? ▶@Transactional wraps a method in a database transaction. If ANY exception is thrown, ALL database changes in that method are rolled back. Example: placing an order = save order + deduct stock. If stock deduction fails, the order save is also rolled back. Without @Transactional, partial data could corrupt the database. Place it on service methods, not controller or repository.What is the difference between
@RequestBody and @RequestParam? ▶@RequestBody: reads the request body (JSON) and maps it to a Java object. Used with POST/PUT. @RequestParam: reads URL query parameters —
GET /products?category=Electronics&page=1 → @RequestParam String category. @PathVariable: reads path variables — GET /products/101 → @PathVariable Long id.What is Bean scope in Spring? ▶
Singleton (default): one instance per Spring container. All requests share the same object. Prototype: new instance every time the bean is requested. Request: one instance per HTTP request (web apps). Session: one instance per HTTP session. For most services, singleton is fine. Use prototype for stateful beans (rare in Spring Boot).
🧩 Microservices
What is the difference between Monolith and Microservices? ▶
Monolith: all features in one codebase, one deployable unit. Simple to develop initially, hard to scale individual parts. Microservices: each feature/domain in its own service, deployed independently. Benefits: independent scaling, independent deployment, technology freedom per service, fault isolation. Downsides: network latency, distributed system complexity, harder to debug.
What is an API Gateway? Why do we need it? ▶
API Gateway is a single entry point for all clients. Instead of clients knowing 10 service URLs, they call one gateway URL. Gateway handles: routing (which service handles this), authentication (verify JWT once, not in every service), rate limiting (prevent abuse), load balancing. In ShopEase: client calls
gateway.shopease.com/api/products → gateway routes to product-service:8081.What is the Circuit Breaker pattern? ▶
Like an electrical circuit breaker — if a downstream service fails repeatedly, stop calling it and return a fallback immediately. Three states: Closed (normal, calls go through), Open (service is down, return fallback immediately — no waiting), Half-Open (try one call to see if service recovered). Implemented with Resilience4j in Spring Boot. Example: if Notification Service is down, Order Service still works — it just logs "email will be sent later."
What is Service Discovery? ▶
Microservices register their IP/port with a Service Registry (like Eureka). Instead of hardcoding
http://192.168.1.5:8081, services call each other by name http://product-service. The registry resolves the name to the current IP. This is essential in cloud environments where IPs change on every deploy or restart. Kubernetes has built-in service discovery via kube-dns.How do microservices communicate? ▶
Synchronous: REST (HTTP) via RestTemplate/WebClient — caller waits for response. Good for real-time queries. Asynchronous: Message queue (Azure Service Bus, Kafka, RabbitMQ) — fire and forget. Good for events. Rule: use REST when you need the response to continue. Use messaging for "notify that something happened."
🗄️ MySQL + JPA
What is ORM and why use JPA/Hibernate? ▶
ORM (Object-Relational Mapping): maps Java objects to database tables automatically. Without ORM, you write raw SQL:
INSERT INTO products (name, price) VALUES (?, ?). With JPA: just call repository.save(product) — Hibernate generates the SQL. Benefits: less boilerplate, automatic table creation, built-in transactions, caching.What is
FetchType.LAZY vs FetchType.EAGER? ▶EAGER: loads related entities immediately when the parent is loaded.
Order loaded → all OrderItems loaded in same query. Can cause performance issues (N+1 problem). LAZY: loads related entities only when accessed. Order loaded → OrderItems loaded only when you call order.getItems(). Default: @OneToMany = LAZY, @ManyToOne = EAGER. Best practice: use LAZY everywhere, load eagerly only when needed.What is the N+1 query problem? ▶
If you load 10 orders (1 query) and then access order.getItems() for each (10 more queries) = 11 queries instead of 1. Solution: use
JOIN FETCH in JPQL: SELECT o FROM Order o JOIN FETCH o.items — loads everything in one query. Or use @EntityGraph annotation.What is
@Transactional propagation? ▶Propagation defines how a method behaves when called from within another transaction. Key ones: REQUIRED (default): use existing transaction or create new one. REQUIRES_NEW: always create new transaction, suspend existing. NESTED: run in nested transaction (savepoint). NOT_SUPPORTED: execute without transaction. In 90% of cases, stick with the default REQUIRED.
⚡ Redis Cache
What is caching and when should you use it? ▶
Caching stores computed or fetched data in fast memory so subsequent requests are faster. Use cache when: data is read-heavy (product catalog), data changes infrequently (category list), computation is expensive (complex DB queries). Don't cache: user-specific data without proper key isolation, highly volatile data, write-heavy data.
What is cache invalidation? What are the strategies? ▶
Cache invalidation = removing/updating stale cache when data changes. Strategies: TTL (Time-To-Live): cache expires after N seconds automatically. Write-Through: update DB and cache simultaneously. Write-Back: update cache first, DB later (risk of data loss). Cache-Aside: app checks cache → miss → DB → store in cache. Cache-Aside + TTL is the most common pattern. In Spring:
@CacheEvict on update/delete methods.What is the difference between Redis and Memcached? ▶
Redis: supports complex data structures (strings, lists, sets, sorted sets, hashes), persistence (data survives restart), pub/sub messaging, clustering. Memcached: only key-value strings, no persistence, simpler. In practice: Redis is preferred for almost all use cases due to versatility. Memcached is only chosen for pure simple caching at extreme scale.
☁️ Azure (Key Vault + Service Bus)
Why should we use Azure Key Vault instead of application.properties? ▶
Hardcoding secrets in properties files means: secrets in Git repo (anyone with repo access sees DB password), secrets visible in logs, no audit trail (who accessed what). Key Vault provides: encryption at rest, access control (only specific apps/users), audit logs, secret rotation without redeployment, Managed Identity (no credentials needed to access Key Vault itself).
What is the difference between a queue and a topic in Azure Service Bus? ▶
Queue: point-to-point. One publisher, one consumer. Each message is delivered to exactly one consumer. Use for task distribution. Topic + Subscriptions: pub/sub. One publisher, multiple subscribers. Each subscriber gets its own copy. Example: "Order Placed" topic → Email Service subscription + SMS Service subscription + Inventory subscription. All three get the same message independently.
What is a Dead Letter Queue? ▶
A Dead Letter Queue (DLQ) is a special queue that holds messages that could not be processed after the maximum retry attempts. If Notification Service throws an exception 10 times for the same message, the message moves to the DLQ instead of being retried infinitely or lost. Ops teams monitor DLQs for failed messages. This ensures no message is ever silently dropped.
What is the difference between synchronous and asynchronous communication in microservices? ▶
Synchronous (REST): Service A calls Service B and waits. If B is slow, A is slow. If B is down, A fails. Tight coupling. Asynchronous (Message Queue): Service A drops a message and immediately continues. Service B processes at its own pace. If B is down, messages accumulate and process when B comes back. Loose coupling, better resilience. Use sync for queries (need data now). Use async for commands/events (fire and forget).
🏛️ System Design & Misc
What are HTTP status codes? List the important ones. ▶
2xx Success: 200 OK, 201 Created, 204 No Content. 3xx Redirect: 301 Moved Permanently, 304 Not Modified (cached). 4xx Client Error: 400 Bad Request (invalid input), 401 Unauthorized (not logged in), 403 Forbidden (logged in but no permission), 404 Not Found, 409 Conflict (duplicate). 5xx Server Error: 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable.
What is REST? What makes an API RESTful? ▶
REST = Representational State Transfer. Principles: Stateless (no session on server, each request is self-contained), Uniform Interface (standard HTTP methods), Resource-based URLs (
/products/101 not /getProduct?id=101), Client-Server separation, Cacheable responses. Use nouns for URLs, HTTP verbs for actions: GET /orders not GET /getOrders.What is SOLID principles? ▶
Single Responsibility: each class does one thing. Open-Closed: open for extension, closed for modification (use interfaces/inheritance). Liskov Substitution: subclass must work wherever parent is used. Interface Segregation: small specific interfaces over large general ones. Dependency Inversion: depend on abstractions (interfaces), not concrete classes. Spring's DI (@Autowired with interfaces) enforces D automatically.
What is Docker and why do microservices use it? ▶
Docker packages your app + its dependencies into a container — a lightweight portable unit. "Works on my machine" is solved: the container runs identically everywhere. Each microservice is packaged as a Docker image. Benefits: isolation, consistency, fast startup, easy scaling.
docker build -t product-service . creates image. docker run -p 8081:8081 product-service starts it.What is the difference between
@Bean and @Component? ▶Both create Spring beans. @Component (and its specializations): class-level, Spring detects via classpath scanning. You own the class. @Bean: method-level inside a
@Configuration class, you manually instantiate and return the object. Use @Bean when: you don't own the class (third-party library), you need custom initialization logic, or you need multiple bean configurations for the same type.How would you secure a REST API? ▶
JWT (JSON Web Token): user logs in → server returns JWT → client sends JWT in Authorization header on every request → server validates JWT (no DB lookup needed). OAuth 2.0: delegate authentication to Google/Microsoft. HTTPS: always. Role-based access:
@PreAuthorize("hasRole('ADMIN')"). In Spring Boot: add Spring Security dependency, configure SecurityFilterChain, validate JWT in a filter.CampusToAI Academy
Live Online Bootcamp · Java + Spring Boot + React + Azure + AI/LLM · ₹49,999 · 2 Months
📞 +91-90233-88910 · campustoai.com