Coding Scenarios & Challenges — Java
Beyond LeetCode: real-world coding scenarios that test production thinking. These are the questions that separate 5-year engineers from 10-year engineers.
"What's Wrong With This Code?"
These are subtle bugs that look correct at first glance. Senior interviews love these because they test production instinct.
Challenge 1: ConcurrentModificationException
public void removeExpired(List<Session> sessions) {
for (Session session : sessions) {
if (session.isExpired()) {
sessions.remove(session); // What happens here?
}
}
}
Bug & Fix
Bug: Modifying a collection while iterating with enhanced for-loop throws ConcurrentModificationException.
Fix:
Challenge 2: HashMap with Mutable Keys
public class Employee {
private String name;
private int department;
public int hashCode() { return Objects.hash(name, department); }
public boolean equals(Object o) { /* standard equals */ }
}
Map<Employee, String> map = new HashMap<>();
Employee emp = new Employee("Alice", 42);
map.put(emp, "Engineering");
emp.setDepartment(99); // Manager moves Alice to another dept
String team = map.get(emp); // What does this return?
Bug & Fix
Bug: Returns null. After mutation, hashCode() returns a different value, so get() looks in the wrong bucket. The entry exists but is unreachable — a memory leak.
Fix: Make keys immutable. Use record types or make all fields final with no setters.
Challenge 3: Double-Checked Locking (Broken)
public class ConnectionPool {
private static ConnectionPool instance;
public static ConnectionPool getInstance() {
if (instance == null) { // First check (no lock)
synchronized (ConnectionPool.class) {
if (instance == null) { // Second check (with lock)
instance = new ConnectionPool(); // What's wrong?
}
}
}
return instance;
}
}
Bug & Fix
Bug: Without volatile, the JVM may reorder the constructor. Thread B may see a non-null instance that hasn't finished construction (partially constructed object).
Fix: Add volatile:
Challenge 4: Integer Cache Boundary
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // What does this print?
Bug & Fix
Bug: Prints false. Java caches Integer objects for values -128 to 127. Beyond that range, == compares references (different objects), not values.
Fix: Always use .equals() for object comparison:
Challenge 5: SimpleDateFormat Thread Safety
public class DateUtil {
private static final SimpleDateFormat SDF =
new SimpleDateFormat("yyyy-MM-dd");
public static String format(Date date) {
return SDF.format(date); // Called from multiple threads
}
}
Bug & Fix
Bug: SimpleDateFormat is NOT thread-safe. Concurrent access corrupts internal state, producing garbled dates or ArrayIndexOutOfBoundsException.
Fix: Use DateTimeFormatter (Java 8+) which is immutable and thread-safe:
Challenge 6: Resource Close Order
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
process(rs);
}
} // What's the close order? What if ps.close() throws?
Bug & Fix
Bug: This code is actually CORRECT. Try-with-resources closes in reverse order (rs → ps → conn) and suppresses exceptions from close().
The bug is when people write it manually:
Challenge 7: Comparator Transitivity Violation
Comparator<Task> byPriority = (a, b) -> {
if (Math.abs(a.getPriority() - b.getPriority()) < 5) {
return 0; // "close enough" — treat as equal
}
return Integer.compare(a.getPriority(), b.getPriority());
};
Collections.sort(tasks, byPriority); // What can happen?
Bug & Fix
Bug: Violates transitivity. If A≈B and B≈C, it doesn't guarantee A≈C. TimSort throws IllegalArgumentException: Comparison method violates its general contract.
Example: priorities 1, 4, 8. compare(1,4)=0, compare(4,8)=0, but compare(1,8)≠0.
Fix: Comparators MUST be transitive. Remove the fuzzy equality:
Challenge 8: Race Condition in Lazy Singleton
public class Registry {
private Map<String, Service> services;
public Map<String, Service> getServices() {
if (services == null) {
services = loadServices(); // expensive, called once
}
return services;
}
}
Bug & Fix
Bug: Two threads can both see services == null, both call loadServices(), and one overwrites the other's result. Worse: thread A may see a partially-constructed Map from thread B.
Fix: Use holder pattern (lazy, thread-safe, no synchronization cost):
Challenge 9: BigDecimal equals vs compareTo
Set<BigDecimal> prices = new HashSet<>();
prices.add(new BigDecimal("1.0"));
prices.add(new BigDecimal("1.00"));
System.out.println(prices.size()); // What does this print?
Bug & Fix
Bug: Prints 2. BigDecimal.equals() considers scale: 1.0 (scale=1) ≠ 1.00 (scale=2). But compareTo() returns 0 (they're numerically equal).
Fix: Use TreeSet (uses compareTo) or normalize with stripTrailingZeros():
Challenge 10: Stream Short-Circuit Surprise
List<String> result = Stream.of("a", "b", "c", "d")
.peek(s -> System.out.print(s + " "))
.filter(s -> !s.equals("b"))
.peek(s -> System.out.print(s.toUpperCase() + " "))
.limit(2)
.collect(Collectors.toList());
// What gets printed?
Bug & Fix
Bug: Many expect all elements processed. Actually prints: a A b c C — stream stops after collecting 2 elements ("a" and "c"). Element "d" is never touched due to limit(2) short-circuiting.
Lesson: Don't use peek() for side effects in production. Stream processing order and short-circuiting make it unpredictable.
Real-World Coding Scenarios
Scenario 1: Thread-Safe LRU Cache
Requirements: TTL expiration, max size, LRU eviction, O(1) operations
public class LRUCache<K, V> {
private final int maxSize;
private final long ttlMillis;
private final Map<K, Node<K, V>> map;
private final Node<K, V> head, tail;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public LRUCache(int maxSize, Duration ttl) {
this.maxSize = maxSize;
this.ttlMillis = ttl.toMillis();
this.map = new HashMap<>(maxSize * 4 / 3 + 1);
this.head = new Node<>(null, null, 0);
this.tail = new Node<>(null, null, 0);
head.next = tail;
tail.prev = head;
}
public V get(K key) {
lock.readLock().lock();
try {
Node<K, V> node = map.get(key);
if (node == null) return null;
if (isExpired(node)) {
lock.readLock().unlock();
lock.writeLock().lock();
try {
removeNode(node);
map.remove(key);
return null;
} finally {
lock.writeLock().unlock();
lock.readLock().lock();
}
}
return node.value;
} finally {
lock.readLock().unlock();
}
}
public void put(K key, V value) {
lock.writeLock().lock();
try {
Node<K, V> existing = map.get(key);
if (existing != null) {
removeNode(existing);
}
Node<K, V> node = new Node<>(key, value, System.currentTimeMillis());
addToFront(node);
map.put(key, node);
if (map.size() > maxSize) {
Node<K, V> victim = tail.prev;
removeNode(victim);
map.remove(victim.key);
}
} finally {
lock.writeLock().unlock();
}
}
private boolean isExpired(Node<K, V> node) {
return System.currentTimeMillis() - node.timestamp > ttlMillis;
}
private void addToFront(Node<K, V> node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
private void removeNode(Node<K, V> node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private static class Node<K, V> {
K key;
V value;
long timestamp;
Node<K, V> prev, next;
Node(K key, V value, long timestamp) {
this.key = key;
this.value = value;
this.timestamp = timestamp;
}
}
}
Follow-up questions interviewers ask:
- "How would you make this distributed?" → Consistent hashing + Redis
- "What about cache stampede?" → Probabilistic early expiration or locking on miss
- "How do you handle 100K concurrent reads?" → Segmented/striped locks or lock-free with ConcurrentHashMap
Scenario 2: Token Bucket Rate Limiter
public class TokenBucketRateLimiter {
private final int maxTokens;
private final double refillRate; // tokens per second
private double tokens;
private long lastRefillTimestamp;
public TokenBucketRateLimiter(int maxTokens, double refillRate) {
this.maxTokens = maxTokens;
this.refillRate = refillRate;
this.tokens = maxTokens;
this.lastRefillTimestamp = System.nanoTime();
}
public synchronized boolean tryAcquire() {
refill();
if (tokens >= 1) {
tokens -= 1;
return true;
}
return false;
}
private void refill() {
long now = System.nanoTime();
double elapsed = (now - lastRefillTimestamp) / 1_000_000_000.0;
tokens = Math.min(maxTokens, tokens + elapsed * refillRate);
lastRefillTimestamp = now;
}
}
Scenario 3: Producer-Consumer with Graceful Shutdown
public class EventProcessor {
private final BlockingQueue<Event> queue;
private final ExecutorService consumers;
private volatile boolean running = true;
public EventProcessor(int queueSize, int consumerCount) {
this.queue = new ArrayBlockingQueue<>(queueSize);
this.consumers = Executors.newFixedThreadPool(consumerCount);
for (int i = 0; i < consumerCount; i++) {
consumers.submit(this::consumeLoop);
}
}
public boolean publish(Event event) {
if (!running) return false;
return queue.offer(event, 100, TimeUnit.MILLISECONDS);
}
private void consumeLoop() {
while (running || !queue.isEmpty()) {
try {
Event event = queue.poll(500, TimeUnit.MILLISECONDS);
if (event != null) {
process(event);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
public void shutdown() {
running = false;
consumers.shutdown();
try {
if (!consumers.awaitTermination(30, TimeUnit.SECONDS)) {
consumers.shutdownNow();
}
} catch (InterruptedException e) {
consumers.shutdownNow();
}
}
}
Big O with Java Collections
Complexity Reference Table
| Operation | ArrayList | LinkedList | HashMap | TreeMap | ConcurrentHashMap |
|---|---|---|---|---|---|
| get(index) | O(1) | O(n) | — | — | — |
| get(key) | — | — | O(1)* | O(log n) | O(1)* |
| add(end) | O(1)† | O(1) | — | — | — |
| add(index) | O(n) | O(n)‡ | — | — | — |
| put(key) | — | — | O(1)* | O(log n) | O(1)* |
| remove | O(n) | O(1)§ | O(1)* | O(log n) | O(1)* |
| contains | O(n) | O(n) | O(1)* | O(log n) | O(1)* |
| iteration | O(n) | O(n) | O(capacity) | O(n) | O(n) |
* Amortized, degrades to O(log n) with many collisions (red-black tree in bucket)
† Amortized, O(n) when resize triggers
‡ O(1) if you already have the node reference
§ O(1) if you have the iterator positioned
When O(1) HashMap Becomes O(n)
// Hash collision attack — all keys hash to same bucket
class EvilKey {
private final int id;
public int hashCode() { return 42; } // always same bucket!
// ...
}
// Java 8+ mitigation: when bucket size > 8, converts to red-black tree
// Worst case becomes O(log n) instead of O(n)
ArrayList vs LinkedList — Why ArrayList Almost Always Wins
| Factor | ArrayList | LinkedList |
|---|---|---|
| Cache locality | Sequential memory, CPU prefetch works | Scattered nodes, cache misses |
| Memory overhead | ~4 bytes per element (reference) | ~24 bytes per element (node object + pointers) |
| Random access | O(1) direct index | O(n) traversal |
| Iteration speed | 10-100x faster (CPU cache) | Pointer chasing |
| Practical insert (middle) | Fast for <1000 elements (System.arraycopy is native) | Theoretically better, practically slower due to cache |
Rule of thumb
Use ArrayList unless you have a specific, measured need for LinkedList (which is almost never in practice).
Take-Home Assignment Best Practices
What Interviewers Evaluate
| Criteria | Weight | What They Look For |
|---|---|---|
| Correctness | 30% | Does it work? Edge cases handled? |
| Code quality | 25% | Clean, readable, well-structured |
| Testing | 20% | Unit tests, edge cases, integration |
| Design decisions | 15% | Why this approach? Trade-offs documented? |
| Documentation | 10% | README, API docs, how to run |
Project Structure Template
my-assignment/
├── src/
│ ├── main/java/com/example/
│ │ ├── controller/
│ │ ├── service/
│ │ ├── repository/
│ │ ├── model/
│ │ ├── exception/
│ │ └── config/
│ └── test/java/com/example/
│ ├── controller/ (integration tests)
│ ├── service/ (unit tests)
│ └── TestData.java (test fixtures)
├── docker-compose.yml
├── Makefile
├── README.md
└── DECISIONS.md (trade-off documentation)
README Template for Take-Homes
# Project Name
## Quick Start
docker-compose up -d
./gradlew bootRun
# API available at http://localhost:8080
## Design Decisions
- Chose PostgreSQL over MongoDB because [reason]
- Used event-driven approach for [component] because [reason]
- Trade-off: chose consistency over availability for [feature]
## API Endpoints
| Method | Path | Description |
|--------|------|-------------|
| POST | /api/v1/orders | Create new order |
| GET | /api/v1/orders/{id} | Get order by ID |
## Testing
./gradlew test # unit tests
./gradlew integrationTest # requires Docker
## What I Would Add With More Time
- [ ] Caching layer for read-heavy endpoints
- [ ] Rate limiting
- [ ] Pagination for list endpoints
Stream API Interview Challenges
Challenge: Group and Aggregate
// Given: List<Employee> with name, department, salary
// Task: Find the highest-paid employee in each department
Map<String, Optional<Employee>> topEarners = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparingDouble(Employee::getSalary))
));
// Better: avoid Optional in result
Map<String, Employee> topEarners = employees.stream()
.collect(Collectors.toMap(
Employee::getDepartment,
Function.identity(),
BinaryOperator.maxBy(Comparator.comparingDouble(Employee::getSalary))
));
Challenge: Custom Collector
// Implement a collector that partitions a list into chunks of size N
public static <T> Collector<T, ?, List<List<T>>> chunked(int size) {
return Collector.of(
ArrayList::new,
(list, item) -> {
if (list.isEmpty() || list.get(list.size() - 1).size() >= size) {
list.add(new ArrayList<>());
}
list.get(list.size() - 1).add(item);
},
(left, right) -> { left.addAll(right); return left; }
);
}
// Usage
List<List<Integer>> chunks = IntStream.rangeClosed(1, 10)
.boxed()
.collect(chunked(3));
// [[1,2,3], [4,5,6], [7,8,9], [10]]
Concurrency Challenges
Implement a Simple Thread Pool
public class SimpleThreadPool {
private final BlockingQueue<Runnable> taskQueue;
private final List<Worker> workers;
private volatile boolean isShutdown = false;
public SimpleThreadPool(int poolSize, int queueSize) {
taskQueue = new ArrayBlockingQueue<>(queueSize);
workers = new ArrayList<>(poolSize);
for (int i = 0; i < poolSize; i++) {
Worker worker = new Worker();
workers.add(worker);
worker.start();
}
}
public void submit(Runnable task) {
if (isShutdown) throw new IllegalStateException("Pool is shut down");
try {
taskQueue.put(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public void shutdown() {
isShutdown = true;
workers.forEach(Thread::interrupt);
}
private class Worker extends Thread {
public void run() {
while (!isShutdown || !taskQueue.isEmpty()) {
try {
Runnable task = taskQueue.poll(500, TimeUnit.MILLISECONDS);
if (task != null) task.run();
} catch (InterruptedException e) {
break;
} catch (Exception e) {
// Log and continue — don't kill the worker
}
}
}
}
}
Implement a CountDownLatch
public class SimpleCountDownLatch {
private int count;
public SimpleCountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException();
this.count = count;
}
public synchronized void countDown() {
if (count > 0) {
count--;
if (count == 0) {
notifyAll();
}
}
}
public synchronized void await() throws InterruptedException {
while (count > 0) {
wait();
}
}
public synchronized void await(long timeout, TimeUnit unit)
throws InterruptedException {
long millis = unit.toMillis(timeout);
long deadline = System.currentTimeMillis() + millis;
while (count > 0) {
long remaining = deadline - System.currentTimeMillis();
if (remaining <= 0) return;
wait(remaining);
}
}
}
Code Review Interview Practice
Review This Code (Find 6 Issues)
public class UserService {
private Logger log = LoggerFactory.getLogger(UserService.class);
private Map<String, User> cache = new HashMap<>();
@Autowired
private UserRepository userRepository;
public User findUser(String email) {
if (cache.containsKey(email)) {
return cache.get(email);
}
User user = userRepository.findByEmail(email);
if (user != null) {
cache.put(email, user);
}
return user;
}
public void deleteUser(String id) {
userRepository.deleteById(id);
log.info("Deleted user: " + id);
}
public User updateEmail(String id, String newEmail) {
User user = userRepository.findById(id).get();
user.setEmail(newEmail);
userRepository.save(user);
return user;
}
}
Issues Found
- Thread safety:
HashMapcache accessed concurrently without synchronization → useConcurrentHashMap - Race condition:
containsKey+getis not atomic → usecomputeIfAbsent - Cache invalidation:
updateEmailchanges email but doesn't invalidate old cache entry - Unbounded cache: No eviction policy → memory leak over time
- NoSuchElementException:
.get()on Optional withoutisPresentcheck → useorElseThrow - String concatenation in log:
"Deleted user: " + idallocates even if log level is higher → uselog.info("Deleted user: {}", id) - Bonus — Logger should be
private static finalfor proper singleton behavior