Java Backend
Complete Roadmap
Every important topic, in exact order, with official docs + videos + practice links. You know Java basics and Node.js — this starts from your gaps and builds to production-grade skills. Skip topics you know using the ⚡ button.
- Abstract class — can have state (fields), constructors, concrete + abstract methods. Use when: IS-A with shared behavior/state
- Interface — contract only (pre-Java 8). Use when: CAN-DO capability across unrelated classes
-
Default methods in interfaces (Java 8+) —
interfaces can now have concrete methods:
default void log() { ... } -
Static methods in interfaces — utility
methods:
static int compare(A a, B b) - Multiple interface implementation — a class can implement many interfaces but extend only one class
- Marker interfaces — empty interfaces for tagging: Serializable, Cloneable, Remote
-
Functional interface — exactly ONE abstract
method. Enables lambda assignment.
@FunctionalInterfaceenforces this INTERVIEW
- Sealed interfaces (Java 17) — restrict which classes can implement
- Interface segregation principle (ISP) — keep interfaces small and focused
- Why Spring beans implement interfaces — for JDK dynamic proxy (AOP)
"Why does Spring recommend coding to interfaces?" — Spring creates proxies (for @Transactional, @Cacheable) using JDK Proxy which requires an interface, or CGLIB which subclasses. Interface makes your code mockable in tests too.
-
Generic classes:
class Repository<T>— type parameter makes class reusable -
Generic methods:
<T extends Comparable<T>> T max(T a, T b) -
Upper bounded wildcard:
List<? extends Number>— read-only, accepts Number and subclasses -
Lower bounded wildcard:
List<? super Integer>— write-friendly, accepts Integer and parents -
Unbounded wildcard:
List<?>— unknown type, very flexible -
Type erasure — generics are compile-time
only. At runtime
List<String>becomesList. Cannot donew T()at runtime INTERVIEW - PECS rule: Producer Extends, Consumer Super — when in doubt what wildcard to use
- Exception hierarchy: Throwable → Error (JVM errors) | Exception → RuntimeException (unchecked) INTERVIEW
-
Checked exceptions — must declare with
throwsor catch. E.g. IOException, SQLException - Unchecked exceptions — RuntimeException subclasses. No forced handling. Spring uses these exclusively
- try-catch-finally — finally always runs (even if exception thrown)
-
try-with-resources — auto-closes Closeable:
try (Connection c = ds.getConnection()) { ... } -
Multi-catch:
catch (IOException | SQLException e) - Custom exception: extend RuntimeException, add message + cause constructor
-
Exception chaining:
throw new ServiceException("msg", originalException)— preserve root cause
"Why does Spring use unchecked exceptions?" — Forces callers to handle every possible exception (checked) breaks API contracts when internal implementation changes. Unchecked lets global @ControllerAdvice catch everything at the boundary.
- List: ArrayList (O(1) get, O(n) insert), LinkedList (O(1) insert, O(n) get), Vector (synchronized legacy)
- Map: HashMap (unordered, O(1) avg), LinkedHashMap (insertion order), TreeMap (sorted keys, O(log n)), ConcurrentHashMap (thread-safe) INTERVIEW
- Set: HashSet (no duplicates, backed by HashMap), LinkedHashSet (ordered), TreeSet (sorted)
- Queue/Deque: LinkedList, ArrayDeque, PriorityQueue
-
Immutable collections:
List.of(),Map.of(),Set.of()(Java 9+) - Collections utility class: sort(), shuffle(), unmodifiableList(), synchronizedList()
- Comparable vs Comparator — natural ordering vs custom ordering INTERVIEW
- equals() and hashCode() contract — if a.equals(b) then a.hashCode() == b.hashCode(). MUST override both INTERVIEW
"How does HashMap work internally?" — Array of buckets + LinkedList/TreeMap per bucket. hashCode() finds bucket, equals() finds exact key. Load factor 0.75 triggers resize. Java 8+: bucket becomes TreeMap when size > 8 (O(log n) worst case).
-
Syntax:
(a, b) -> a + bor(a, b) -> { return a + b; } - Built-in functional interfaces: Predicate<T> (boolean test), Function<T,R> (R apply), Consumer<T> (void accept), Supplier<T> (T get), BiFunction, BiConsumer, BiPredicate
-
Method references 4 types:
String::toUpperCase(instance),Math::max(static),list::add(bound),String::new(constructor) -
Function composition:
f.andThen(g)— f then g;f.compose(g)— g then f -
Predicate composition:
p1.and(p2),p1.or(p2),p1.negate() - Effectively final — variables used in lambdas must be final or effectively final INTERVIEW
-
list.stream(),Arrays.stream(arr),Stream.of(a,b,c),Stream.generate(),Stream.iterate(),IntStream.range(0,10)
-
filter(Predicate),map(Function),flatMap(Function),distinct(),sorted(),sorted(Comparator),limit(n),skip(n),peek(Consumer)(debug only)
-
collect(),forEach(),reduce(identity, BinaryOperator),count(),findFirst(),findAny(),anyMatch(),allMatch(),noneMatch(),min(),max(),toArray()
-
Collectors.toList(),toSet(),toMap(key, val),joining(", ") -
groupingBy(classifier)— returnsMap<K, List<T>> -
groupingBy(classifier, counting())— returnsMap<K, Long> -
partitioningBy(Predicate)— returnsMap<Boolean, List<T>> -
collectingAndThen(toList(), Collections::unmodifiableList) -
Collectors.toUnmodifiableList()(Java 10+)
- map vs flatMap: map is 1-to-1, flatMap is 1-to-many and flattens nested streams INTERVIEW
-
Parallel streams:
list.parallelStream()— uses ForkJoinPool. Only faster if work per element is heavy AND order doesn't matter INTERVIEW - Streams are not reusable — once terminal op called, stream is closed
-
Creating:
Optional.of(value)(NPE if null),Optional.ofNullable(value)(safe),Optional.empty() -
Checking:
isPresent(),isEmpty()(Java 11) -
Getting:
get()(throws if empty),orElse(default),orElseGet(() -> compute()),orElseThrow(),orElseThrow(ExceptionSupplier) -
Transforming:
map(Function),flatMap(Function),filter(Predicate) -
Side effects:
ifPresent(Consumer),ifPresentOrElse(Consumer, Runnable)(Java 9) - Don't use Optional as: method parameter, field, or collection element INTERVIEW
-
Spring Data JPA returns
Optional<T>fromfindById()— chain.orElseThrow(() -> new NotFoundException(...))
- Thread lifecycle: NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED INTERVIEW
-
Runnable — no return value:
new Thread(() -> doWork()).start() - Callable<T> — returns value + throws checked exceptions
-
Future<T> — handle for async result:
future.get()(blocks),future.isDone() - Race condition — two threads access shared mutable state without synchronization INTERVIEW
- Deadlock — thread A holds lock X waits for Y, thread B holds Y waits for X INTERVIEW
- synchronized keyword — only one thread at a time enters block. Works on object monitor
- volatile — guarantees visibility (changes visible to all threads), not atomicity INTERVIEW
- AtomicInteger, AtomicLong, AtomicReference — lock-free thread-safe operations
-
ExecutorService — manages a pool of
threads:
Executors.newFixedThreadPool(10),newCachedThreadPool(),newSingleThreadExecutor() - submit() vs execute() — submit returns Future, execute is fire-and-forget
- shutdown() vs shutdownNow() — graceful vs immediate
- CompletableFuture — async pipelines without blocking
-
Creating:
CompletableFuture.supplyAsync(() -> fetch()),runAsync(() -> doWork()) -
Chaining:
thenApply(Function)(same thread),thenApplyAsync(Function)(new thread) -
Completion:
thenAccept(Consumer),thenRun(Runnable) -
Combining:
thenCompose()(flat-map),thenCombine()(combine two futures) -
Error handling:
exceptionally(ex -> fallback),handle(BiFunction) - Virtual Threads (Java 21) — lightweight threads managed by JVM, not OS. Spring Boot 3.2+ supports them. Fix for thread-per-request scalability INTERVIEW
-
Records (Java 16) — immutable data
carriers.
record Point(int x, int y) {}auto-generates constructor, getters, equals, hashCode, toString. Use for DTOs INTERVIEW - Record limitations: no field mutation, can't extend classes, can't be JPA @Entity (needs no-arg constructor)
- Compact constructors in records: validate in constructor body without repeating parameters
-
Sealed classes (Java 17):
sealed class Shape permits Circle, Rectangle— restricts inheritance to listed classes -
Pattern matching instanceof (Java 16):
if (obj instanceof String s) { s.length(); }— no cast needed -
Switch expressions (Java 14):
String result = switch(day) { case MON -> "Mon"; default -> "Other"; } -
Text blocks (Java 15): multiline strings
with
"""..."""— great for JSON in tests -
var (Java 10): local variable type
inference —
var list = new ArrayList<String>()
- pom.xml coordinates: groupId (com.company), artifactId (project-name), version (1.0.0)
-
Parent POM:
spring-boot-starter-parentmanages all dependency versions — no version conflicts - Dependency scopes: compile (default, in classpath always), test (test only), provided (container provides it), runtime (not for compile)
- Maven lifecycle phases: validate → compile → test → package → verify → install → deploy
-
Key commands:
mvn clean package,mvn spring-boot:run,mvn test,mvn dependency:tree - Transitive dependencies — if A depends on B which depends on C, A gets C too. Can cause conflicts
- Dependency exclusions — exclude a transitive dep you don't want
-
Profiles:
<profiles>— different configs for dev/prod:mvn package -Pprod - Spring Boot Maven Plugin: creates executable fat JAR with embedded Tomcat
- JOINs: INNER (matched both), LEFT (all left + matched right), RIGHT, FULL OUTER, CROSS, SELF JOIN
- Aggregates: COUNT(*), COUNT(DISTINCT col), SUM, AVG, MAX, MIN — with GROUP BY and HAVING
- Subqueries: in WHERE (correlated), in FROM (derived table), in SELECT (scalar)
- CTEs (WITH clause): readable alternative to subqueries, can be recursive
- Window functions: ROW_NUMBER(), RANK(), DENSE_RANK(), NTILE(), LAG(), LEAD(), FIRST_VALUE(), SUM() OVER() INTERVIEW
- PARTITION BY: divide rows into groups for window functions
-
OVER(): defines the window:
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) -
LIMIT + OFFSET:
LIMIT 10 OFFSET 20— page 3 of size 10 - DISTINCT, ORDER BY, NULLS FIRST/LAST
- String functions: LOWER(), UPPER(), TRIM(), CONCAT(), SUBSTRING(), LIKE, ILIKE (postgres case-insensitive)
- Date functions: NOW(), CURRENT_DATE, EXTRACT, DATE_TRUNC, AGE()
- CASE WHEN: inline conditional logic in SELECT
- COALESCE(a, b, c): first non-null value
- Relationships: One-to-One, One-to-Many (most common), Many-to-Many (via junction table)
- Primary key: uniquely identifies a row. Integer SERIAL/BIGSERIAL or UUID
- Foreign key: references PK of another table. ON DELETE CASCADE vs RESTRICT vs SET NULL
- Composite keys: multiple columns together form the PK (e.g. user_id + role_id in user_roles)
- Normalization: 1NF (atomic values, no repeating groups) → 2NF (no partial dependency on composite key) → 3NF (no transitive dependency) INTERVIEW
- Denormalization: intentionally duplicate data for read performance (e.g. reporting tables)
- UUID vs auto-increment: UUID = globally unique, good for distributed systems; auto-increment = sequential, smaller, faster for indexing
-
B-tree index (default): best for equality
and range queries.
CREATE INDEX idx_email ON users(email) - When to add index: columns in WHERE, JOIN ON, ORDER BY, high cardinality (many unique values)
-
Composite indexes:
CREATE INDEX ON orders(user_id, created_at)— order matters (leftmost prefix rule) -
Partial index:
CREATE INDEX ON users(email) WHERE active = true - EXPLAIN ANALYZE: shows query plan + actual execution time. Look for Seq Scan (bad on large tables) vs Index Scan (good)
- Constraints: NOT NULL, UNIQUE, CHECK, FOREIGN KEY
- JSONB: PostgreSQL's binary JSON column type. Can index into it with GIN index
- Partial vs full table scan: understand when Postgres chooses index vs sequential scan
- Atomicity — all operations in transaction succeed, or all fail (rolled back)
- Consistency — transaction brings DB from one valid state to another (constraints must hold)
- Isolation — concurrent transactions don't see each other's intermediate state
- Durability — committed data persists even after crash (written to WAL/disk)
- Isolation levels: READ UNCOMMITTED (dirty reads) → READ COMMITTED (default PostgreSQL) → REPEATABLE READ → SERIALIZABLE INTERVIEW
- Read phenomena: Dirty read (uncommitted data), Non-repeatable read (data changed between reads), Phantom read (new rows appear between reads)
- Optimistic locking: no lock, use @Version field, fail if stale. Good for low contention INTERVIEW
- Pessimistic locking: SELECT FOR UPDATE — acquires row lock. Good for high contention
- Deadlock: A waits for B's lock, B waits for A's. PostgreSQL detects and aborts one
- HikariCP (Spring default): connection pool — reuse DB connections. Config: maximumPoolSize, minimumIdle, connectionTimeout
"Explain ACID" and "What's the difference between optimistic and pessimistic locking?" — Answer optimistic: use @Version annotation in JPA, @Lock(LockModeType.OPTIMISTIC). Pessimistic: @Lock(LockModeType.PESSIMISTIC_WRITE).
- IoC (Inversion of Control) — you don't create objects, you describe what you need and Spring creates/wires them INTERVIEW
- DI types: Constructor injection ✅ (immutable, testable, recommended), Setter injection, Field injection (@Autowired on field — avoid in new code)
- @Component, @Service, @Repository, @Controller, @RestController — stereotype annotations, component-scan picks them up
- @Bean + @Configuration — explicit bean creation for third-party classes you can't annotate
- @Autowired — inject by type. With constructor injection, @Autowired is optional (implicit when one constructor)
- @Qualifier("beanName") — disambiguate when multiple beans of same type
- @Primary — mark one bean as default when multiple of same type
- Bean scopes: Singleton (one per ApplicationContext, default), Prototype (new on each injection), Request (per HTTP request), Session (per HTTP session) INTERVIEW
- Bean lifecycle: instantiate → inject dependencies → @PostConstruct → in use → @PreDestroy → destroyed
- @PostConstruct / @PreDestroy — lifecycle callbacks
- ApplicationContext vs BeanFactory — ApplicationContext extends BeanFactory, adds i18n, events, AOP, etc. Always use ApplicationContext
- ApplicationEvent + @EventListener — loose coupling via events within the app
- @Lazy — don't create bean until first use (saves startup time)
- @ConditionalOnProperty, @ConditionalOnClass — create beans conditionally
"Explain Spring Bean lifecycle" — Instantiated via constructor → Dependencies injected → @PostConstruct called → ready → @PreDestroy called → destroyed. Also: "What's the difference between Singleton and Prototype scope?" Singleton = shared state risk, Prototype = new instance each time.
- @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan on current package
-
Auto-configuration magic: add
spring-boot-starter-web→ Tomcat, Jackson, Spring MVC auto-configured. Reads META-INF/spring/auto-configuration files INTERVIEW - Starters: spring-boot-starter-web, data-jpa, security, test, validation, actuator, mail, cache, data-redis
- application.yml (prefer over .properties): server.port, spring.datasource.url, spring.jpa.show-sql
- @Value("${app.name:default}") — inject single property with default
- @ConfigurationProperties(prefix="app") — bind group of properties to a typed class. Better than @Value for groups
-
Profiles: application-dev.yml,
application-prod.yml. Activate with
spring.profiles.active=devor env var SPRING_PROFILES_ACTIVE=prod - @Profile("dev") — only create bean in this profile
-
Environment variables — override any
property:
SPRING_DATASOURCE_URL=...(dots become underscores, uppercase) - spring.factories / spring.auto-configuration.imports — how auto-config is discovered
- @RestController = @Controller + @ResponseBody — every method returns data, not view
- @RequestMapping("/api/v1/users") — base path on class
- Method annotations: @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping
- @PathVariable Long id — extract from URL path /users/{id}
- @RequestParam(defaultValue="0") int page — query parameter /users?page=0&size=10
- @RequestBody UserRequest req — deserializes JSON body via Jackson
- @RequestHeader("Authorization") String token — extract header
-
ResponseEntity<T> — full control:
body + status + headers:
ResponseEntity.created(uri).body(dto) - DispatcherServlet — front controller pattern. All requests hit it, routed to handlers, response assembled INTERVIEW
- Jackson — JSON serialization library. @JsonIgnore, @JsonProperty("name"), @JsonFormat
- Content negotiation: produces/consumes on @RequestMapping — Accept and Content-Type headers
- @CrossOrigin — enable CORS on specific endpoints (or configure globally)
- @Entity + @Table(name="users") — maps class to DB table
-
@Id + @GeneratedValue: IDENTITY (DB
auto-increment), SEQUENCE (DB sequence), UUID
(
@UuidGenerator) - @Column(name="email_address", unique=true, nullable=false, length=255)
- @Transient — field not persisted to DB
- @Enumerated(EnumType.STRING) — store enum as string (not ordinal!)
- Auditing: @CreatedDate, @LastModifiedDate, @CreatedBy — extend AbstractAuditingEntity or use @EnableJpaAuditing
- @Embedded + @Embeddable — embed value objects (Address, Money) into entity
- @Version — optimistic locking: auto-incremented int on each update, throws OptimisticLockException on conflict
- JpaRepository<Entity, ID>: save(), saveAll(), findById(), findAll(), delete(), existsById(), count()
- Derived queries: Spring generates SQL from method name: findByEmailIgnoreCase(), findByAgeGreaterThan(), findByStatusIn(List)
- findBy + And/Or/Between/Like/LessThan/GreaterThan/OrderBy
-
@Query JPQL:
@Query("SELECT u FROM User u WHERE u.email = :email")— uses entity/field names - @Query nativeQuery=true: raw SQL — use when JPQL can't express the query
- @Modifying + @Query: for UPDATE/DELETE queries. Requires @Transactional
-
Pagination:
Page<User> findAll(Pageable pageable), passPageRequest.of(page, size, Sort.by("name")) - Slice<T> — like Page but no total count query (faster for infinite scroll)
- Custom Repository: interface MyCustomRepo + MyCustomRepoImpl class for complex queries
- Specifications: type-safe dynamic WHERE clauses — use when filters are optional
- @OneToMany(mappedBy="user", cascade=CascadeType.ALL, orphanRemoval=true) — on parent side
- @ManyToOne(fetch=FetchType.LAZY) — on child side, always make LAZY
- @ManyToMany @JoinTable — creates junction table
- Default fetch types: @OneToMany = LAZY, @ManyToOne = EAGER, @OneToOne = EAGER. Change EAGER to LAZY everywhere! INTERVIEW
- N+1 Problem: findAll() loads 100 users (1 query) then accesses user.getOrders() on each (100 queries) = 101 queries INTERVIEW
-
Fix N+1:
@Query("SELECT u FROM User u JOIN FETCH u.orders")or@EntityGraph(attributePaths={"orders"}) - CascadeType: PERSIST (save child with parent), MERGE, REMOVE (delete child with parent), REFRESH, ALL
- orphanRemoval=true — delete child when removed from parent's collection
- @BatchSize(size=50) — batch load lazy collections to reduce queries
- spring.jpa.show-sql=true — see what queries JPA generates (essential for debugging)
"What is the N+1 problem and how do you fix it?" — Critical question. Also be ready for "What's the difference between LAZY and EAGER loading?" and "When would you use @EntityGraph?"
- Why migrations: schema changes tracked in version control alongside code. Team members get same DB state
- Naming: V1__create_users.sql, V2__add_email_index.sql, V3__create_orders.sql
- Location: src/main/resources/db/migration/
- Spring Boot auto-runs pending migrations at startup
- Golden rule: never edit an already-applied migration. Create a new one instead
- Baseline: for existing databases, create V1 as baseline
- Repeatable migrations: R__seed_data.sql — re-runs when checksum changes
- Undo scripts (paid feature): V2__undo.sql
- Flyway tracks history in flyway_schema_history table
- DTO pattern: separate API shape from @Entity — avoids exposing DB structure, allows different shapes per endpoint INTERVIEW
-
Record DTOs:
public record UserResponse(Long id, String email, String name) {}— immutable, auto equals/hashCode - Request vs Response DTO: UserCreateRequest (what comes in), UserResponse (what goes out), UserUpdateRequest (partial update)
- @NotNull, @NotBlank, @NotEmpty — null/empty checks
- @Size(min=2, max=50) — string length
- @Email — email format validation
- @Pattern(regexp="...") — regex validation
- @Min, @Max, @Positive, @PositiveOrZero — numeric validation
- @Valid on @RequestBody parameter — triggers validation. Returns 400 with errors if invalid
- @Validated (class-level) — enables group validation
- Custom validator: implement ConstraintValidator<Annotation, Type>
-
MapStruct: annotation processor generates
mapper at compile time —
@Mapper(componentModel = "spring")
- @ControllerAdvice — one class catches exceptions from all controllers globally
- @ExceptionHandler(UserNotFoundException.class) — maps exception type to HTTP response
- ProblemDetail (RFC 9457, Spring 6) — standardized error response: type, title, status, detail, instance INTERVIEW
- ResponseEntityExceptionHandler — extend this to handle Spring MVC built-in exceptions
-
Custom exception hierarchy:
BaseException (RuntimeException) → NotFoundException (404), ConflictException (409), ForbiddenException (403) - MethodArgumentNotValidException — thrown when @Valid fails. Extract field errors from BindingResult
- HttpMessageNotReadableException — invalid JSON in request body
- AOP purpose: apply cross-cutting concerns (logging, security, transactions) without modifying business code
-
Aspect — class with advice:
@Aspect @Component class LoggingAspect -
Pointcut — expression defining where advice
runs:
@Pointcut("execution(* com.app.service.*.*(..))") - Advice types: @Before (before method), @After (after regardless), @AfterReturning (after success), @AfterThrowing (after exception), @Around (wrap method — most powerful)
- JoinPoint — represents the method being intercepted. Access method name, args, return value
-
ProceedingJoinPoint — used in @Around:
Object result = pjp.proceed() - Spring AOP uses JDK Proxy (interface-based) or CGLIB (class subclassing) INTERVIEW
- Self-invocation problem: calling annotated method from same class bypasses proxy — AOP doesn't apply INTERVIEW
- @Transactional — Spring wraps method in DB transaction. Commit on success, rollback on RuntimeException
- Propagation.REQUIRED (default) — join existing transaction or create new one INTERVIEW
- Propagation.REQUIRES_NEW — always new transaction, suspends outer
- Propagation.SUPPORTS — run in transaction if one exists, else without
- Propagation.NOT_SUPPORTED — suspend any existing transaction
- rollbackFor=Exception.class — by default, ONLY rolls back on RuntimeException! Add this for checked exceptions INTERVIEW
- noRollbackFor — don't rollback for this specific exception
- readOnly=true — performance hint for SELECT-only methods (Hibernate optimizations)
- timeout=30 — transaction timeout in seconds
-
Isolation level — set per transaction:
@Transactional(isolation=Isolation.READ_COMMITTED) - Self-invocation problem — method A calls @Transactional method B in same class → B's @Transactional is ignored because proxy is bypassed INTERVIEW
"What happens when @Transactional method throws a checked
exception?" — No rollback by default. This surprises many.
Fix:
@Transactional(rollbackFor = Exception.class).
Also: "Explain the self-invocation problem with
@Transactional."
- Resource naming: nouns plural: /users, /orders, /products. Not /getUsers, not /createProduct
- HTTP semantics: GET (read, safe, idempotent), POST (create, not idempotent), PUT (full replace, idempotent), PATCH (partial update), DELETE (idempotent) INTERVIEW
- Status codes: 200 OK, 201 Created (+ Location header), 204 No Content (DELETE/PUT success), 400 Bad Request, 401 Unauthorized (not logged in), 403 Forbidden (logged in, no permission), 404 Not Found, 409 Conflict (duplicate), 422 Unprocessable Entity (validation), 429 Too Many Requests, 500 Internal Server Error INTERVIEW
- Versioning: /api/v1/users — in URL path (most common), or Accept: application/vnd.app.v1+json header
-
Pagination response envelope:
{ "data": [...], "page": 0, "size": 10, "totalElements": 100, "totalPages": 10 } - Cursor-based pagination — better for large datasets, uses last-seen ID: ?after=abc123
- Filtering + sorting: ?status=ACTIVE&sort=name,asc&sort=createdAt,desc
- Idempotency key — Idempotency-Key header for POST requests to prevent duplicate operations
- HATEOAS — include links in response to related resources (optional, used in REST Level 3)
- Springdoc OpenAPI — add dependency, get Swagger UI at /swagger-ui.html automatically
- @Operation(summary="...", description="...") — document an endpoint
- @Parameter(description="...", example="...") — document a parameter
- @ApiResponse(responseCode="200", description="...")
- @Schema(description="...", example="...") — document DTO fields
- OpenAPI spec — generated at /v3/api-docs (JSON), use to generate client SDKs
- Spring Boot Actuator — add spring-boot-starter-actuator, get: /actuator/health, /actuator/info, /actuator/metrics, /actuator/env INTERVIEW
- Actuator: expose endpoints selectively in production (never expose /actuator/env publicly)
- Custom health indicators — implement HealthIndicator for custom /health checks
Build a complete Blog API: Users, Posts (with tags), Comments. Use PostgreSQL + Flyway + DTOs with validation + global exception handler + pagination + OpenAPI docs. This project alone touches 90% of Spring Boot. Push to GitHub.
- SecurityFilterChain — ordered list of filters. Every HTTP request passes through all filters INTERVIEW
- Key filters: UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter, JwtAuthenticationFilter (custom)
- SecurityContext / SecurityContextHolder — stores current user's Authentication. ThreadLocal by default
- Authentication object — principal (UserDetails), credentials, authorities (roles)
- AuthenticationManager — delegates to AuthenticationProvider chain to authenticate
- UserDetails interface — getUsername(), getPassword(), getAuthorities(), isEnabled(), isAccountNonLocked()
-
UserDetailsService —
loadUserByUsername(String username)— load user from DB - PasswordEncoder — always BCryptPasswordEncoder. NEVER store plain text. NEVER MD5/SHA INTERVIEW
- SecurityFilterChain @Bean config: .csrf().disable() for REST APIs, .sessionManagement(stateless), .authorizeHttpRequests()
- CSRF — disable for stateless REST (no cookies/sessions). Keep enabled for form-based apps
- CORS — configure allowed origins, methods, headers via CorsConfigurationSource bean
- DaoAuthenticationProvider — wires UserDetailsService + PasswordEncoder together
- @EnableMethodSecurity — MUST add to config class to enable method-level annotations INTERVIEW
- @PreAuthorize("hasRole('ADMIN')") — check before method runs
- @PostAuthorize("returnObject.owner == authentication.name") — check after method returns
-
Roles vs Authorities: roles are prefixed
with ROLE_ internally.
hasRole('ADMIN')=hasAuthority('ROLE_ADMIN')INTERVIEW - URL-level security in config: .requestMatchers("/admin/**").hasRole("ADMIN")
- Method expressions: hasRole(), hasAnyRole(), hasAuthority(), isAuthenticated(), isAnonymous(), permitAll(), denyAll()
-
Get current user:
@AuthenticationPrincipal UserDetails userin controller method parameter - SecurityContextHolder.getContext().getAuthentication() — get current auth anywhere
- Custom AccessDeniedHandler — return clean 403 JSON response
- Custom AuthenticationEntryPoint — return clean 401 JSON response
- JWT structure: header.payload.signature (base64url-encoded) INTERVIEW
- Header: algorithm (HS256, RS256) + token type
- Payload claims: sub (subject/userId), iat (issued at), exp (expiry), custom: roles, email
- Signature: HMAC-SHA256(base64(header).base64(payload), secret). Tamper-proof
- Stateless auth flow: POST /auth/login → validate credentials → sign JWT → return access token + refresh token in response
- Client sends: Authorization: Bearer <access_token> on every request
- JwtAuthenticationFilter (extends OncePerRequestFilter): extract token → validate → build Authentication → set in SecurityContext
- Access token — short-lived (15 min). Stateless. Can't be revoked without blacklist
- Refresh token — long-lived (7–30 days). Stored in DB. Can be revoked. Used to get new access token INTERVIEW
- Rotation strategy — issue new refresh token on each use, invalidate old one
- Token storage: HttpOnly cookie (XSS-safe, CSRF risk) vs localStorage (CSRF-safe, XSS risk)
- JWT libraries: io.jsonwebtoken:jjwt or Spring's built-in OAuth2 Resource Server (preferred)
- RS256 vs HS256 — RS256 uses asymmetric keys (public key to verify, private to sign) — better for microservices
"How does JWT authentication work?" "What's access token vs refresh token?" "Can JWT be revoked?" (No, unless you maintain a blacklist or use short expiry + refresh tokens). These are standard questions.
- OAuth2 purpose — delegate authentication to trusted provider (Google, GitHub, Azure AD)
- Authorization Code Flow: User → App → Auth Server (login page) → code → App exchanges code for token → App uses token for API calls INTERVIEW
- PKCE (Proof Key for Code Exchange) — required for public clients (mobile, SPA). Prevents auth code interception
- OpenID Connect (OIDC) — adds identity layer on top of OAuth2. Returns ID token (JWT) with user info (sub, email, name)
- OAuth2 Client (Spring) — configure providers in application.yml: google, github, okta. Spring handles the flow
- Roles: Resource Owner (user), Client (your app), Authorization Server (Google), Resource Server (API)
- Keycloak — self-hosted Identity Provider. Good for enterprise: users, roles, realms, clients
- Token types in OAuth2: Access Token (for API), ID Token (user identity), Refresh Token
- @Test — test method. @DisplayName("readable name")
- @BeforeEach / @AfterEach — setup/teardown per test
- @BeforeAll / @AfterAll — run once per class (must be static)
- Assertions: assertEquals(expected, actual), assertNotNull(), assertTrue(), assertFalse(), assertThrows(Exception.class, () → ...), assertDoesNotThrow(), assertAll()
-
assertThrows:
UserNotFoundException ex = assertThrows(UserNotFoundException.class, () -> service.findById(-1L)); assertEquals("User not found", ex.getMessage()) -
@ParameterizedTest + @ValueSource:
@ValueSource(strings = {"", " ", "invalid@"}) - @MethodSource("provideTestData") — static method returns Stream<Arguments>
- @CsvSource({"1, one", "2, two"}) — tabular test data
- @Nested — inner class for grouping related tests
- @Disabled("reason") — skip test temporarily
- @Tag("integration") — tag tests for selective running
- AAA pattern: Arrange (setup) → Act (call) → Assert (verify) — structure every test INTERVIEW
- Test naming: methodName_StateUnderTest_ExpectedBehavior — e.g. findById_WhenUserNotFound_ThrowsException
- @ExtendWith(MockitoExtension.class) — required on test class for JUnit 5
- @Mock — create mock of dependency. @Spy — real object but can stub specific methods
- @InjectMocks — create instance of class under test and inject @Mocks
- when(mock.method(arg)).thenReturn(value) — stub method call
- when(mock.method(any())).thenThrow(new RuntimeException()) — simulate failure
- doReturn, doThrow, doAnswer — for void methods or Spy
- verify(mock).method(arg) — assert method was called exactly once
- verify(mock, times(2)).method() — called exactly N times
- verify(mock, never()).method() — never called
-
ArgumentCaptor<T> — capture argument
passed to mock:
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class); verify(repo).save(captor.capture()); assertEquals("John", captor.getValue().getName()) - ArgumentMatchers: any(), anyString(), anyLong(), eq("exact"), argThat(predicate)
- Mockito.mockStatic() — mock static methods (Mockito 3.4+)
- @SpringBootTest — loads full ApplicationContext. Slow. Use for integration tests only
- @SpringBootTest(webEnvironment=RANDOM_PORT) — starts real server on random port. Use with TestRestTemplate
- @WebMvcTest(UserController.class) — loads only web layer (controllers, filters). Fast. Mock services with @MockBean
- @DataJpaTest — loads only JPA layer. Uses in-memory H2. Good for repository tests
- @MockBean — replace a Spring bean in context with Mockito mock. Different from @Mock (no Spring context)
-
MockMvc — test HTTP requests without a real
server:
mockMvc.perform(get("/users/1")).andExpect(status().isOk()).andExpect(jsonPath("$.email").value("[email protected]")) - @AutoConfigureMockMvc — auto-configure MockMvc in @SpringBootTest
- TestRestTemplate — makes real HTTP calls in @SpringBootTest with RANDOM_PORT
- WebTestClient — reactive alternative to TestRestTemplate, fluent API
-
@Sql — run SQL scripts before/after test:
@Sql("/test-data.sql") - @Transactional on test class — rolls back all DB changes after each test
- jsonPath assertions: $.fieldName, $.list[0].name, $.list.length()
- Testcontainers — spin up real Docker containers (PostgreSQL, Redis, Kafka) during tests. Tests against real infrastructure, not mocks INTERVIEW
-
@Testcontainers + @Container — JUnit 5
integration:
@Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16") - @DynamicPropertySource — inject container connection URL into Spring context
-
Singleton containers — reuse container
across tests for speed:
static { container.start(); } -
WireMock — mock external HTTP APIs your
service calls. Stub with:
stubFor(get("/api/users/1").willReturn(okJson(userJson))) - @WireMockTest — Spring Boot WireMock integration
- Test coverage with JaCoCo — add plugin to Maven, generates coverage report. Aim for 70%+ on business logic
- Mutation testing with PIT — checks if your tests actually catch bugs
-
Spring Boot Dockerfile:
FROM eclipse-temurin:21-jre-alpine→COPY target/*.jar app.jar→ENTRYPOINT ["java","-jar","app.jar"] - Multi-stage build — Stage 1: FROM maven:3.9-jdk-21 to build. Stage 2: FROM eclipse-temurin:21-jre-alpine to run. Final image has no build tools (smaller) INTERVIEW
- Layer caching: COPY pom.xml → RUN mvn dependency:go-offline → COPY src → RUN mvn package. Dependencies cached if pom.xml unchanged
- Key commands: docker build -t myapp:1.0 ., docker run -p 8080:8080 -e SPRING_PROFILES_ACTIVE=prod myapp, docker ps, docker logs -f, docker exec -it container bash, docker stop, docker rm
- docker-compose.yml — define app + postgres + redis together. Startup order with depends_on + healthcheck
- Environment variables: use env_file: .env in compose for secrets
-
Volumes: persist PostgreSQL data:
volumes: - postgres_data:/var/lib/postgresql/data -
Docker networking: containers in same
compose network communicate by service name:
jdbc:postgresql://postgres:5432/mydb - .dockerignore — exclude target/, .git, etc. from build context
- JVM in containers: always set -Xmx in JAVA_OPTS. Container memory limits != JVM heap
- Workflow file: .github/workflows/ci.yml. Triggers: push, pull_request, schedule, workflow_dispatch
- Jobs and steps: checkout → setup-java → cache maven deps → mvn test → docker build → push to registry → deploy
- Matrix strategy — run same job across multiple Java versions
-
GitHub Secrets — store DB_PASSWORD,
DOCKER_HUB_TOKEN, AWS credentials. Access as
${{ secrets.MY_SECRET }} - Service containers — run PostgreSQL in CI for integration tests
- Caching Maven dependencies: actions/cache for ~/.m2/repository → faster builds
- Branch protection rules — require CI pass before merge to main
- Environments: define staging and prod environments with required reviewers
- Reusable workflows — define once, call from multiple workflows
- Artifact upload/download — pass JAR between jobs
- IAM — users, roles, policies. Least privilege principle. Never use root account. IAM roles for EC2/ECS INTERVIEW
- EC2 — virtual machine. Security groups (firewall rules). SSH key pair. t2.micro is free tier
- RDS — managed PostgreSQL. Multi-AZ for HA. Read replicas for scale. Automated backups
- S3 — object storage. Buckets, objects, keys. Use for file uploads, static assets, backups. Pre-signed URLs for secure upload from client
- ECR — Docker image registry. Push images from CI, pull in ECS
- ECS Fargate — serverless containers. Task definition, Service, Cluster. No EC2 management
- VPC — private network. Public subnets (load balancer), private subnets (app, DB). NAT Gateway for outbound from private
- Application Load Balancer — HTTPS termination, route traffic to ECS tasks, health checks
- AWS Secrets Manager — store DB credentials, API keys. Spring Boot can read from Secrets Manager
- AWS Free Tier: EC2 t2.micro 750hrs/month, RDS 750hrs. Set billing alarm at $10!
- SQS — managed message queue (alternative to Kafka for simple cases)
- Parameter Store — store config values (cheaper than Secrets Manager for non-secrets)
- Three pillars of observability: Logs (what happened), Metrics (how much/often), Traces (how long, where) INTERVIEW
- Logback (Spring default): logback-spring.xml config. Log levels: TRACE < DEBUG < INFO < WARN < ERROR
- Structured JSON logging — use logstash-logback-encoder. Parse-able by Elasticsearch/CloudWatch
- MDC (Mapped Diagnostic Context) — add requestId, userId to all log lines in a request automatically
- Correlation ID — add X-Correlation-ID header, propagate through services for request tracing
- Micrometer — Spring's metrics facade. Exports to Prometheus, DataDog, etc. via Spring Actuator
- Prometheus + Grafana — scrape metrics from /actuator/prometheus, visualize dashboards
- Micrometer Tracing (Spring 3) — distributed tracing. Integrates with Zipkin or Jaeger
- @Timed — annotate methods to automatically collect timing metrics
- Custom metrics: Counter, Gauge, Timer, DistributionSummary via MeterRegistry
- AWS CloudWatch — logs and metrics in AWS. CloudWatch Alarms for alerts
- Redis data structures: String (simple key-value), Hash (object fields), List (queue/stack), Set (unique members), Sorted Set (scored ranking), HyperLogLog (cardinality), Pub/Sub (messaging)
-
TTL (Time To Live):
EXPIRE key 3600— auto-expire keys - Atomic operations: INCR, INCRBY, SETNX (set if not exists), GETSET
- Spring Cache abstraction: @EnableCaching → @Cacheable("users") on service method → @CacheEvict on update/delete → @CachePut to update cache
- RedisCacheManager — configure TTL per cache, serialization (use JSON not Java serialization)
- Cache-aside pattern: check cache → miss → query DB → populate cache → return INTERVIEW
- Write-through: write to cache and DB simultaneously
- Cache stampede — many requests miss cache simultaneously → DB overload. Fix: locking or probabilistic early expiry
- Cache invalidation strategies: TTL expiry, event-driven eviction, cache aside INTERVIEW
- Session storage: Spring Session + Redis — store HTTP sessions in Redis for horizontal scaling
- Rate limiting with Redis: sliding window using ZADD + ZRANGE, or token bucket with INCR
- Bucket4j — rate limiting library with Redis backend for distributed limiting
- Redis pub/sub — lightweight messaging: PUBLISH channel message, SUBSCRIBE channel
- RedisTemplate vs StringRedisTemplate — StringRedisTemplate for String keys/values (simpler)
"How would you implement caching in Spring?" and "What's the cache-aside pattern?" and "What are the challenges of caching?" (invalidation, stampede, consistency with DB). Common system design questions too.
- Why Kafka: decouple producers from consumers. Async processing. Replay events. Fan-out to multiple consumers
- Core concepts: Topic (category), Partition (parallelism unit), Offset (position in partition), Consumer Group (parallel consumers) INTERVIEW
- Producer — sends to topic. Key determines partition (same key → same partition → order guaranteed)
- Consumer Group — each partition consumed by one consumer in group. Scale consumers by adding to group
- Broker — Kafka server. Replication factor for fault tolerance
- Retention — messages kept for configurable period (7 days default). Can replay
-
KafkaTemplate — send messages:
kafkaTemplate.send("orders", key, orderEvent) -
@KafkaListener — consume:
@KafkaListener(topics="orders", groupId="order-service") - Serialization: use JSON serializer (JsonSerializer/JsonDeserializer). Configure via @KafkaListener or properties
- At-least-once delivery — default. Consumer may reprocess. Make consumers idempotent INTERVIEW
- Exactly-once (transactions) — Kafka transactions + idempotent producer. Complex
- Dead Letter Topic (DLT) — failed messages go to orders-dlt after retry exhausted
- Retry + backoff: @RetryableTopic for automatic retry with exponential backoff
- Outbox Pattern — write event to outbox table in same transaction as business data. Separate process publishes to Kafka. Guarantees consistency INTERVIEW
- Consumer lag — how far behind consumers are from latest offset. Monitor this in production
"How does Kafka differ from RabbitMQ?" — Kafka = log-based, durable, replayable, high throughput. RabbitMQ = message-broker, routed, messages deleted on consume, complex routing rules. Also: "Explain the outbox pattern."
- Monolith vs microservices: start monolith, extract services when you hit clear boundaries. Don't over-engineer from day one INTERVIEW
- API Gateway — single entry point. Handles: routing, auth, rate limiting, SSL termination. Spring Cloud Gateway
- Service Discovery — services register themselves, others find them by name. Eureka (Spring Cloud), Consul, Kubernetes DNS
- Load balancing — Spring Cloud LoadBalancer (replaces Ribbon). Client-side load balancing
- Circuit Breaker (Resilience4j) — prevent cascade failures. States: CLOSED (normal) → OPEN (failing, reject requests) → HALF_OPEN (test one request). @CircuitBreaker annotation INTERVIEW
- Fallback — return cached data or default response when circuit is open
- Retry with @Retry — retry failed calls with exponential backoff
- Rate Limiter with @RateLimiter — protect downstream services
-
RestTemplate vs WebClient vs OpenFeign —
Feign is declarative HTTP client:
@FeignClient("user-service")INTERVIEW - Distributed tracing — trace ID propagated through services. Micrometer Tracing + Zipkin/Jaeger
- Saga pattern — manage distributed transactions across services. Choreography (events) vs Orchestration (coordinator) INTERVIEW
- CQRS — Command Query Responsibility Segregation: separate read/write models. Read from replica, write to primary
- Event Sourcing — store events not state. Rebuild state by replaying events
- gRPC — binary protocol, faster than REST for inter-service. Protocol Buffers for schema
- CAP Theorem: Consistency, Availability, Partition Tolerance — can only guarantee 2 of 3 in distributed system INTERVIEW
- Horizontal scaling (scale out) — add more servers. vs Vertical scaling (scale up) — bigger server
- Stateless services — any instance can handle any request. Required for horizontal scaling. Store state in Redis/DB not in-memory
- Load balancing algorithms: Round Robin, Least Connections, IP Hash (sticky sessions), Weighted
- CDN — cache static assets at edge. Reduce latency globally
- Rate limiting: Token Bucket (burst allowed), Fixed Window, Sliding Window, Leaky Bucket INTERVIEW
- Database scaling: Read replicas (read from replica, write to primary), Sharding (partition data by key), Connection pooling
- Consistent hashing — distribute keys across nodes with minimal redistribution when nodes added/removed
- Message queues — decouple components, handle traffic spikes, async processing
- Design URL shortener: Base62 encoding, Redis cache for popular URLs, consistent hashing for distribution INTERVIEW
- Design Twitter feed: Fan-out on write vs fan-out on read. Celebrity problem. Hybrid approach INTERVIEW
- Design notification service: Kafka for async, multiple consumers per channel (email, SMS, push) INTERVIEW
Build an e-commerce backend: Products, Orders, Users, Payments. Use: Spring Boot + PostgreSQL + Redis (cart/sessions) + Kafka (order events) + Docker + GitHub Actions CI/CD + JWT auth + Testcontainers. This project is your portfolio anchor.