JAVA DEVELOPER KNOWLEDGE
BOOK
Everything You Need to Build the Smart Job Tracker Project
10 Chapters | JWT | Scheduling | JavaMail | JPA | DTOs | Testing
Exception Handling | Analytics Queries | Soft Delete | REST Design
Prepared for: Devansh Dayama — Java Developer Intern
TABLE OF CONTENTS
Ch 1 Spring Security + JWT Complete JWT implementation from scratch
Ch 2 Spring Scheduler Background jobs, @Scheduled, cron expressions
Ch 3 JavaMailSender SMTP config, HTML emails, Gmail App Password
Ch 4 Spring Data JPA Deep Dive Custom JPQL, entity annotations, relationships
Ch 5 DTO Pattern Why DTOs matter, mapping, Lombok @Builder
Ch 6 Global Exception Handling @ControllerAdvice, custom exceptions, error DTOs
Ch 7 Analytics Queries GROUP BY, COUNT, AVG, date functions in JPQL
Ch 8 Soft Delete Pattern @Where, @SQLDelete, data integrity
Ch 9 REST API Design HTTP methods, status codes, best practices
Ch 10 Unit Testing JUnit 5 + Mockito, service testing, mocking
CHAPTER 1
Spring Security + JWT Authentication
What is JWT?
JSON Web Token (JWT) is a compact token encoding a user's identity. It has three dot-separated parts:
Header (algorithm), Payload (user data + expiry), and Signature (cryptographic proof). At login the server
creates it; the client sends it on every request; the server verifies the signature — no database lookup
needed.
JWT Structure
eyJhbGciOiJIUzI1NiJ9 <- Header (base64 encoded)
.eyJzdWIiOiJ1c2VyQGdtYWlsLmNvbSIsImV4cCI6MTcwMDAwfQ
<- Payload: sub=email, exp=expiry timestamp
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
<- Signature: HMAC-SHA256(header+payload, secret)
Maven Dependencies ([Link])
<!-- Spring Security -->
<dependency>
<groupId>[Link]</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JJWT library -->
<dependency>
<groupId>[Link]</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>[Link]</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>[Link]</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
</dependency>
[Link]
@Component
public class JwtTokenProvider {
@Value("${[Link]}") private String secret;
@Value("${[Link]}") private long expiration; // ms, e.g. 86400000 = 24h
public String generateToken(String username) {
return [Link]()
.subject(username)
.issuedAt(new Date())
.expiration(new Date([Link]() + expiration))
.signWith(getKey())
.compact();
public String extractUsername(String token) {
return [Link]().verifyWith(getKey()).build()
.parseSignedClaims(token).getPayload().getSubject();
public boolean validate(String token) {
try { [Link]().verifyWith(getKey()).build().parseSignedClaims(token); return
true; }
catch (Exception e) { return false; }
private SecretKey getKey() {
return [Link]([Link](secret));
[Link] (runs on every request)
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Autowired private JwtTokenProvider jwt;
@Autowired private UserDetailsService uds;
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String header = [Link]("Authorization");
if (header != null && [Link]("Bearer ")) {
String token = [Link](7);
if ([Link](token)) {
String username = [Link](token);
UserDetails ud = [Link](username);
var auth = new UsernamePasswordAuthenticationToken(
ud, null, [Link]());
[Link](new WebAuthenticationDetailsSource().buildDetails(req));
[Link]().setAuthentication(auth);
[Link](req, res);
[Link]
@Configuration @EnableWebSecurity
public class SecurityConfig {
@Autowired private JwtAuthFilter jwtFilter;
@Bean
public SecurityFilterChain chain(HttpSecurity http) throws Exception {
[Link](c -> [Link]())
.sessionManagement(s -> [Link](STATELESS))
.authorizeHttpRequests(a -> a
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, [Link]);
return [Link]();
@Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(); }
KEY: The JwtAuthFilter intercepts every request before Spring's default auth. It reads the Bearer token,
validates it, extracts the username, loads the UserDetails, and sets authentication in SecurityContextHolder.
Controllers can then access the current user via [Link]().getAuthentication().
CHAPTER 2
Spring Scheduler and Cron Jobs
@Scheduled lets a method run automatically on a schedule. Spring manages a background thread pool.
You never call these methods manually — Spring triggers them at the right time. Enable it with
@EnableScheduling on your main class.
Cron Syntax: second minute hour dayOfMonth month dayOfWeek
Expression When It Runs
0 0 9 * * MON-FRI Every weekday at 9:00 AM
000*** Every day at midnight
0 0/30 * * * * Every 30 minutes
0091** First day of month at 9 AM
0 0 8,17 * * * 8 AM and 5 PM every day
[Link]
@Component
public class ReminderScheduler {
@Autowired private JobApplicationRepository repo;
@Autowired private EmailService email;
@Scheduled(cron = "0 0 9 * * MON-FRI")
public void sendReminders() {
LocalDateTime threshold = [Link]().minusDays(7);
List<JobApplication> stale =
[Link](threshold);
for (JobApplication app : stale) {
try {
[Link](
[Link]().getEmail(),
[Link](), [Link]());
[Link](true);
[Link](app);
} catch (Exception e) {
[Link]("Reminder failed: " + [Link]());
// Continue — don't let one failure stop all reminders
}
CHAPTER 3
JavaMailSender and Email Integration
[Link]
[Link]=[Link]
[Link]=587
[Link]=youremail@[Link]
[Link]=xxxx_xxxx_xxxx_xxxx # Gmail App Password (NOT login password)
[Link]=true
[Link]=true
# How to get Gmail App Password:
# Google Account -> Security -> 2-Step Verification -> App passwords -> Generate
[Link]
@Service
public class EmailService {
@Autowired private JavaMailSender mailer;
public void sendReminderEmail(String to, String position, String company) {
try {
MimeMessage msg = [Link]();
MimeMessageHelper h = new MimeMessageHelper(msg, true);
[Link](to);
[Link]("noreply@[Link]");
[Link]("Reminder: Follow up on " + position + " at " + company);
String html = "<div style='font-family:Arial;padding:20px;'>"
+ "<h2 style='color:#1A237E;'>Job Application Reminder</h2>"
+ "<p>Your application for <b>" + position + "</b> at <b>" + company
+ "</b> has had no updates in 7 days. Consider following up!</p>"
+ "</div>";
[Link](html, true); // true = is HTML
[Link](msg);
} catch (MessagingException e) {
throw new RuntimeException("Email failed", e);
}
CHAPTER 4
Spring Data JPA Deep Dive
Key Annotations
Annotation What It Does
@Entity Marks class as database table
@Table(name='...') Specifies exact table name
@Id Primary key field
@GeneratedValue(strategy=IDENTITY) Auto-increment PK
@Column(nullable=false, length=150) Column constraints
@Enumerated([Link]) Store enum as string in DB
@ManyToOne + @JoinColumn Foreign key relationship
@Lob Large text/binary column (TEXT in MySQL)
@CreationTimestamp Automatically set on INSERT
@UpdateTimestamp Automatically updated on every save()
@Where(clause='is_deleted=false') Soft delete auto-filter on all queries
@SQLDelete(sql='UPDATE ... SET is_deleted=true
Override DELETE
WHERE id=?')
with UPDATE
Custom Repository Queries
public interface JobApplicationRepository extends JpaRepository<JobApplication,
Long> {
// Derived query — Spring generates SQL from method name
Page<JobApplication> findByUserId(Long userId, Pageable pageable);
// Derived + multiple conditions
List<JobApplication> findByLastUpdatedBeforeAndReminderSentFalseAndIsDeletedFalse(
LocalDateTime threshold);
// Custom JPQL — count by status for analytics
@Query("SELECT [Link], COUNT(a) FROM JobApplication a "
+ "WHERE [Link] = :uid AND [Link] = false GROUP BY [Link]")
List<Object[]> countByStatus(@Param("uid") Long userId);
// Native SQL for date functions
@Query(value = "SELECT AVG(DATEDIFF(last_updated, applied_date)) "
+ "FROM job_applications WHERE user_id = :uid AND status IN ('INTERVIEW','OFFER')",
nativeQuery = true)
Double avgResponseDays(@Param("uid") Long userId);
}
CHAPTER 5
DTO Pattern and Mapping
Never return your entity directly from a controller. DTOs let you control exactly what data the API exposes,
decouple your API from your database schema, and add validation on inputs. This is a fundamental
pattern in production Java APIs.
Request DTO (what client sends)
@Getter @Setter
public class CreateJobApplicationRequest {
@NotBlank(message = "Company name required")
private String companyName;
@NotBlank private String position;
private String jobDescription;
private LocalDate appliedDate;
private String salaryRange;
private String jobUrl;
// No id, no userId, no status — server controls these
Response DTO (what API returns)
@Getter @Setter @Builder
public class JobApplicationResponse {
private Long id;
private String companyName;
private String position;
private ApplicationStatus status;
private LocalDate appliedDate;
private String salaryRange;
private boolean reminderSent;
private LocalDateTime lastUpdated;
// No user password, no is_deleted — clean and safe
Mapping entity to DTO
// In service — manual mapping using @Builder
private JobApplicationResponse toDto(JobApplication a) {
return [Link]()
.id([Link]()).companyName([Link]())
.position([Link]()).status([Link]())
.appliedDate([Link]()).salaryRange([Link]())
.reminderSent([Link]()).lastUpdated([Link]())
.build();
}
CHAPTER 6
Global Exception Handling
@RestControllerAdvice catches all exceptions thrown anywhere in your application and lets you return a
consistent, clean error response format. No controller needs try-catch blocks — all error handling is
centralized.
ErrorResponse DTO
@Getter @Setter @Builder
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
Custom Exception Classes
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String msg) { super(msg); }
public class InvalidStatusTransitionException extends RuntimeException {
public InvalidStatusTransitionException(String from, String to) {
super("Invalid transition: " + from + " -> " + to);
[Link]
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler([Link])
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException ex,
HttpServletRequest req) {
return [Link]().timestamp([Link]())
.status(404).error("Not Found")
.message([Link]()).path([Link]()).build();
@ExceptionHandler([Link])
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleBadTransition(InvalidStatusTransitionException ex,
HttpServletRequest req) {
return [Link]().timestamp([Link]())
.status(400).error("Bad Request")
.message([Link]()).path([Link]()).build();
@ExceptionHandler([Link])
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleAll(Exception ex, HttpServletRequest req) {
return [Link]().timestamp([Link]())
.status(500).error("Server Error")
.message("Something went wrong").path([Link]()).build();
}
CHAPTER 7
Analytics and Aggregation Queries
Repository Queries
// Count applications by status
@Query("SELECT [Link], COUNT(a) FROM JobApplication a "
+ "WHERE [Link] = :uid AND [Link] = false GROUP BY [Link]")
List<Object[]> countByStatus(@Param("uid") Long userId);
// Average response time (native SQL for DATEDIFF)
@Query(value = "SELECT AVG(DATEDIFF(last_updated, applied_date)) "
+ "FROM job_applications WHERE user_id = :uid "
+ "AND status IN ('INTERVIEW','OFFER') AND is_deleted = 0", nativeQuery = true)
Double avgResponseDays(@Param("uid") Long userId);
// Applications by month
@Query("SELECT YEAR([Link]), MONTH([Link]), COUNT(a) "
+ "FROM JobApplication a WHERE [Link] = :uid AND [Link] = false "
+ "GROUP BY YEAR([Link]), MONTH([Link]) "
+ "ORDER BY YEAR([Link]), MONTH([Link])")
List<Object[]> countByMonth(@Param("uid") Long userId);
// Top companies (Pageable limits to top 5)
@Query("SELECT [Link], COUNT(a) as cnt FROM JobApplication a "
+ "WHERE [Link] = :uid AND [Link] = false "
+ "GROUP BY [Link] ORDER BY cnt DESC")
List<Object[]> topCompanies(@Param("uid") Long userId, Pageable pageable);
Processing in AnalyticsService
public AnalyticsSummaryDTO getSummary(Long userId) {
List<Object[]> rows = [Link](userId);
Map<String, Long> byStatus = new HashMap<>();
long total = 0; long offers = 0;
for (Object[] r : rows) {
String status = r[0].toString();
long count = ((Number) r[1]).longValue();
[Link](status, count);
total += count;
if ("OFFER".equals(status)) offers = count;
double rate = total > 0 ? (offers * 100.0 / total) : 0;
Double avgDays = [Link](userId);
return [Link]()
.totalApplications(total).successRate([Link](rate * 10) / 10.0)
.avgResponseDays(avgDays != null ? [Link]() : 0)
.byStatus(byStatus).build();
}
CHAPTER 8
Soft Delete Pattern
Hard deleting records corrupts analytics history. Soft delete marks records as deleted without removing
them, preserving all historical data. Hibernate handles this transparently with two annotations.
@Entity
@Where(clause = "is_deleted = false") // auto-appended to EVERY query
@SQLDelete(sql = "UPDATE job_applications SET is_deleted=true WHERE id=?")
public class JobApplication {
...
private boolean isDeleted = false;
// Now these work transparently:
[Link](app); // Runs UPDATE SET is_deleted=true (not DELETE)
[Link](); // Auto-filtered: WHERE is_deleted = false
[Link](1L); // Also filtered automatically
RESULT: When you call [Link](app), Hibernate runs UPDATE job_applications SET is_deleted=true
WHERE id=? instead of DELETE. When you query, @Where auto-adds is_deleted=false to every query.
Your analytics queries still see these records when they bypass @Where using nativeQuery=true.
CHAPTER 9
REST API Design Principles
HTTP Method Use For Status on Success
GET Fetch data, never changes state 200 OK
POST Create a new resource 201 Created
PUT Replace entire resource 200 OK
PATCH Update specific fields only 200 OK
DELETE Remove a resource (or soft delete) 204 No Content
Status Code Meaning When to Use
200 OK Successful GET, PUT, PATCH
201 Created POST that creates a new resource
204 No Content Successful DELETE
400 Bad Request Validation error, bad business rule
401 Unauthorized Missing or invalid JWT token
403 Forbidden Valid token but no access to this resource
404 Not Found Resource with given ID doesn't exist
409 Conflict Duplicate — email already registered
500 Internal Server Error Unexpected crash
Getting Current User from JWT in Controller
// Helper — put in a base class or utility
protected Long getCurrentUserId() {
Authentication auth = [Link]().getAuthentication();
String email = ((UserDetails) [Link]()).getUsername();
return [Link](email).orElseThrow().getId();
// CRITICAL: Always get userId from JWT, NEVER from request body
@PostMapping
public ResponseEntity<JobApplicationResponse> create(@RequestBody @Valid
CreateJobApplicationRequest req) {
Long userId = getCurrentUserId(); // From JWT — cannot be forged
return [Link](201).body([Link](userId, req));
}
CHAPTER 10
Unit Testing with JUnit and Mockito
Unit tests test one class in isolation. You mock all dependencies (repositories, services) so you test only
the logic of the class under test. No database, no Spring context, no network — just pure Java logic
verification.
Mockito Key Annotations
Annotation Purpose
@ExtendWith([Link]) Enable Mockito in the test class
@Mock Create a fake (mock) object — returns null by default
@InjectMocks Create the real class and inject @Mock fields into it
when(...).thenReturn(...) Tell a mock what to return when called
verify(mock, times(1)).method(...) Assert a method was called exactly once
assertThrows([Link], () -> ...) Assert that code throws a specific exception
assertThat(result).isEqualTo(expected) Assert values using AssertJ (fluent)
[Link]
@ExtendWith([Link])
class JobApplicationServiceTest {
@Mock private JobApplicationRepository repo;
@Mock private UserRepository userRepo;
@InjectMocks private JobApplicationService service;
@Test
void createApplication_shouldSaveAndReturnDto() {
User user = new User(); [Link](1L); [Link]("d@[Link]");
CreateJobApplicationRequest req = new CreateJobApplicationRequest();
[Link]("Google"); [Link]("Backend Engineer");
[Link]([Link]());
when([Link](1L)).thenReturn([Link](user));
when([Link](any())).thenAnswer(inv -> [Link](0));
JobApplicationResponse result = [Link](1L, req);
assertThat([Link]()).isEqualTo("Google");
assertThat([Link]()).isEqualTo([Link]);
verify(repo, times(1)).save(any([Link]));
@Test
void updateStatus_invalidTransition_shouldThrow() {
JobApplication app = new JobApplication();
[Link]([Link]);
when([Link](1L)).thenReturn([Link](app));
assertThrows([Link],
() -> [Link](1L, 1L, [Link]));
TESTING MINDSET: Each @Test method follows Arrange-Act-Assert. Arrange = set up mocks. Act = call the
method. Assert = verify the result. You are NOT testing the database or repository — you mock those. You
are testing that the SERVICE logic behaves correctly given controlled inputs.
QUICK REFERENCE — Spring Boot Annotations
Annotation Layer Purpose
@RestController Controller Combines @Controller + @ResponseBody — returns JSON
@Service Service Business logic layer bean
@Repository Repository Data access layer + JPA exception translation
@Component Any Generic Spring-managed bean
@Autowired Any Inject dependency (prefer constructor injection)
@Value Any Inject value from [Link]
@Transactional Service Wrap method in DB transaction — auto rollback on exception
@Valid Controller Trigger @NotBlank/@NotNull validation on request body
@PathVariable Controller Extract {id} from /api/items/{id}
@RequestParam Controller Extract ?page=0 query parameter
@RequestBody Controller Map JSON request body to Java object
@CrossOrigin Controller Allow CORS from React frontend origin
@Scheduled Component Auto-run method on schedule
@EnableScheduling Config/Main Enable @Scheduled processing
@NoArgsConstructor Entity Lombok: generate no-args constructor
@Getter @Setter Entity/DTO Lombok: generate all getters and setters
@Builder DTO Lombok: generate builder pattern