Back to Stream Practice: Employee Analysis
Q17Question 17 of 43

Find all employees who joined in the last 5 years using LocalDate.now().minusYears(5)

Key Concepts
  • LocalDate.now().minusYears(5) computes the cutoff date, correctly handling leap years
  • !joinDate.isBefore(cutoff) means 'on or after cutoff' (inclusive boundary)
  • isAfter(cutoff) alone excludes the exact cutoff day, which may be an off-by-one bug
  • Compute the cutoff once outside the stream, not inside the predicate for every element
  • Inject a Clock for testability so unit tests do not depend on the system clock
  • For database-backed data, push the date comparison into a SQL WHERE clause instead of loading all rows

Definition

Compute a cutoff date with LocalDate.now().minusYears(5) and filter employees whose joining date is on or after that cutoff using !isBefore.
  • 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?

JAVA
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()));
    }
}
TEXT
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) |

JAVA
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?

JAVA
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.

JAVA
// 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.

JAVA
// 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?

JAVA
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"));
TEXT
2021: 1 joiners
2022: 1 joiners
2023: 1 joiners
2024: 1 joiners

What about Period-based calculations?

JAVA
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate