Definition
- LocalDate.now().minusYears(5) computes the date exactly five years ago, correctly adjusting for leap years.
- !joinDate.isBefore(cutoff) keeps employees who joined on or after the cutoff day (inclusive boundary).
- isAfter(cutoff) alone would exclude employees who joined on the exact cutoff day.
- The cutoff should be computed once outside the stream, not inside the predicate.
- Clock injection makes
now()deterministic in tests:LocalDate.now(clock).
Priya at the National Bank of India draws a line five years back from today on the calendar. Everyone who joined on or after that line is a recent hire. The line is the cutoff. Anyone whose joining date falls on that line or later stays in the result.
Understanding the Concepts
What does this question ask?
The interviewer wants you to filter by a computed date boundary using LocalDate arithmetic and a stream predicate. This tests date API knowledge, boundary precision (inclusive vs exclusive), and testability with Clock.
How do you find employees who joined in the last 5 years?
package com.bank.hr;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
public class RecentJoiners {
public static void main(String[] args) {
List<Employee> employees = Employee.getSampleEmployees();
LocalDate cutoff = LocalDate.now().minusYears(5);
List<Employee> recentJoiners = employees.stream()
.filter(e -> !e.getJoinDate().isBefore(cutoff))
.toList();
System.out.println("Cutoff: " + cutoff);
recentJoiners.forEach(e ->
System.out.println(e.getName() + " joined " + e.getJoinDate()));
}
}
Cutoff: 2020-01-15
Priya joined 2021-06-01
Neha joined 2022-03-15
Ananya joined 2023-01-10
Meera joined 2024-07-20
The !isBefore predicate means "on or after": it includes employees who joined on the exact cutoff day.
Why use !isBefore instead of isAfter?
| Expression | Meaning | Includes cutoff day? |
|---|---|---|
| !joinDate.isBefore(cutoff) | On or after cutoff | Yes (inclusive) |
| joinDate.isAfter(cutoff) | Strictly after cutoff | No (exclusive) |
| joinDate.isEqual(cutoff) \|\| joinDate.isAfter(cutoff) | On or after | Yes (verbose) |
LocalDate cutoff = LocalDate.of(2020, 1, 15);
LocalDate exact = LocalDate.of(2020, 1, 15);
System.out.println(!exact.isBefore(cutoff)); // true (included)
System.out.println(exact.isAfter(cutoff)); // false (excluded!)
Using isAfter alone creates an off-by-one bug for employees who joined on the exact cutoff day.
How do you make it testable with Clock?
package com.bank.service;
import java.time.*;
import java.util.*;
public class RecentJoinerService {
private final Clock clock;
public RecentJoinerService(Clock clock) {
this.clock = clock;
}
public List<Employee> findRecentJoiners(
List<Employee> employees, int years) {
LocalDate cutoff = LocalDate.now(clock).minusYears(years);
return employees.stream()
.filter(e -> !e.getJoinDate().isBefore(cutoff))
.toList();
}
public static void main(String[] args) {
// Production: system clock
var service = new RecentJoinerService(Clock.systemDefaultZone());
// Test: fixed clock for deterministic results
Clock fixedClock = Clock.fixed(
LocalDate.of(2025, 1, 15).atStartOfDay(ZoneId.systemDefault()).toInstant(),
ZoneId.systemDefault());
var testService = new RecentJoinerService(fixedClock);
}
}
What are common mistakes?
Mistake 1: computing the cutoff inside the predicate.
// WRONG: creates a new LocalDate for every employee
List<Employee> wrong = employees.stream()
.filter(e -> !e.getJoinDate().isBefore(
LocalDate.now().minusYears(5))) // computed N times!
.toList();
// CORRECT: compute once outside
LocalDate cutoff = LocalDate.now().minusYears(5);
List<Employee> correct = employees.stream()
.filter(e -> !e.getJoinDate().isBefore(cutoff))
.toList();
Mistake 2: not handling null joining dates.
// WRONG: NullPointerException if joinDate is null
List<Employee> wrong = employees.stream()
.filter(e -> !e.getJoinDate().isBefore(cutoff))
.toList();
// CORRECT: null guard
List<Employee> correct = employees.stream()
.filter(e -> e.getJoinDate() != null
&& !e.getJoinDate().isBefore(cutoff))
.toList();
How do you group recent joiners by year?
Map<Integer, List<Employee>> byYear = employees.stream()
.filter(e -> !e.getJoinDate().isBefore(cutoff))
.collect(Collectors.groupingBy(
e -> e.getJoinDate().getYear()));
byYear.forEach((year, emps) ->
System.out.println(year + ": " + emps.size() + " joiners"));
2021: 1 joiners
2022: 1 joiners
2023: 1 joiners
2024: 1 joiners
What about Period-based calculations?
import java.time.Period;
// Calculate tenure for each recent joiner
employees.stream()
.filter(e -> !e.getJoinDate().isBefore(cutoff))
.forEach(e -> {
Period tenure = Period.between(e.getJoinDate(), LocalDate.now());
System.out.println(e.getName() + ": "
+ tenure.getYears() + "y " + tenure.getMonths() + "m");
});
What are common interview follow-up questions?
- "How do you find employees who joined in a specific month?" Filter with
getJoinDate().getMonth() == Month.JANUARY. - "How do you find employees who joined on a weekend?" Filter with
getJoinDate().getDayOfWeek().getValue() >= 6. - "How do you parameterize the number of years?" Accept it as a method argument and pass to
minusYears(n).
Visual Overview
Today: 2025-01-15
Cutoff: 2020-01-15 (now minus 5 years)
Timeline:
2018───2019───2020───2021───2022───2023───2024───2025
│ │
cutoff today
│←── last 5 years ──→│
Filter: !joinDate.isBefore(cutoff)
┌──────────────────────────────────────────────┐
│ Rajesh 2015-03-01 isBefore? YES → SKIP │
│ Priya 2021-06-01 isBefore? NO → KEEP │
│ Amit 2018-09-10 isBefore? YES → SKIP │
│ Neha 2022-03-15 isBefore? NO → KEEP │
│ Ananya 2023-01-10 isBefore? NO → KEEP │
└──────────────────────────────────────────────┘
Key Takeaway
Finding employees who joined in the last N years uses LocalDate arithmetic: LocalDate.now().minusYears(5) computes the cutoff date. The filter predicate !joinDate.isBefore(cutoff) keeps employees who joined on or after that cutoff.
The critical detail is the boundary: !isBefore is inclusive (includes the cutoff day), while isAfter alone is exclusive (excludes it). Using the wrong one creates an off-by-one-day bug in eligibility rules.
Compute the cutoff once outside the stream to avoid creating a new LocalDate for every element. Inject a Clock via the constructor so LocalDate.now(clock) is deterministic in tests. Guard against null joining dates if the data comes from a legacy system.
For production systems with database-backed data, push the date comparison into a SQL WHERE clause instead of loading all rows into memory for in-memory filtering.
Remember one sentence: compute the cutoff once with now().minusYears(5) and filter with !isBefore for an inclusive boundary, and inject a Clock for testable date logic.
---