Coding Round

Build a tiny DI container that does constructor injection.

Interviewers ask this to check you understand what Spring does. The container finds the constructor, resolves each parameter recursively and caches singletons.

Example

Constructor injection with reflection, singletons cached in a map.

import java.lang.reflect.Constructor;
import java.util.*;

public class Main {
    static final Map<Class<?>, Object> singletons = new LinkedHashMap<>();

    static <T> T getBean(Class<T> type) throws Exception {
        if (singletons.containsKey(type)) return type.cast(singletons.get(type));
        Constructor<?> ctor = type.getDeclaredConstructors()[0];
        Object[] args = new Object[ctor.getParameterCount()];
        Class<?>[] params = ctor.getParameterTypes();
        for (int i = 0; i < params.length; i++) args[i] = getBean(params[i]); // resolve dependencies first
        T bean = type.cast(ctor.newInstance(args));
        singletons.put(type, bean);
        System.out.println("Created " + type.getSimpleName());
        return bean;
    }

    public static void main(String[] args) throws Exception {
        OrderService service = getBean(OrderService.class);
        System.out.println(service.placeOrder("Redmi Note 13"));
        System.out.println("Same repo instance: " + (getBean(ProductRepository.class) == service.repo));
        System.out.println("Beans: " + singletons.keySet().stream().map(Class::getSimpleName).toList());
    }
}

class ProductRepository {
    String find(String name) { return name + " (in stock)"; }
}

class NotificationService {
    String send(String msg) { return "SMS: " + msg; }
}

class OrderService {
    final ProductRepository repo;
    final NotificationService notifier;
    OrderService(ProductRepository repo, NotificationService notifier) {
        this.repo = repo;
        this.notifier = notifier;
    }
    String placeOrder(String product) { return notifier.send("Order placed for " + repo.find(product)); }
}

Output

Created ProductRepository
Created NotificationService
Created OrderService
SMS: Order placed for Redmi Note 13 (in stock)
Same repo instance: true
Beans: [ProductRepository, NotificationService, OrderService]

Show why self-invocation skips @Transactional (proxy demo).

Spring wraps a bean in a proxy. Calls coming through the proxy get the advice; a call from inside the bean (this.method()) does not. This JDK dynamic proxy shows the same behaviour.

Example

Only the outer call goes through the "transaction" advice.

import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        OrderService target = new OrderServiceImpl();
        OrderService proxy = (OrderService) Proxy.newProxyInstance(
                OrderService.class.getClassLoader(),
                new Class<?>[]{OrderService.class},
                (p, method, margs) -> {
                    System.out.println("[tx] BEGIN " + method.getName());
                    Object result = method.invoke(target, margs);
                    System.out.println("[tx] COMMIT " + method.getName());
                    return result;
                });

        System.out.println("-- call saveOrder via proxy --");
        proxy.saveOrder();
        System.out.println("-- call checkout via proxy (calls this.saveOrder inside) --");
        proxy.checkout();
    }
}

interface OrderService {
    void checkout();
    void saveOrder();
}

class OrderServiceImpl implements OrderService {
    public void checkout() {
        System.out.println("checkout: validating cart");
        this.saveOrder();   // self-invocation: no proxy, no new transaction
    }
    public void saveOrder() {
        System.out.println("saveOrder: inserting order row");
    }
}

Output

-- call saveOrder via proxy --
[tx] BEGIN saveOrder
saveOrder: inserting order row
[tx] COMMIT saveOrder
-- call checkout via proxy (calls this.saveOrder inside) --
[tx] BEGIN checkout
checkout: validating cart
saveOrder: inserting order row
[tx] COMMIT checkout

Write a layered Product service with a not-found error mapped to 404.

Controller → service → repository, with an exception that a global handler turns into a status code. The "controller" here returns a status and body like ResponseEntity would.

Example

Plain-Java version of a Spring CRUD flow.

import java.math.BigDecimal;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        ProductController controller = new ProductController(new ProductService(new InMemoryProductRepository()));
        System.out.println(controller.create(new Product(null, "Boat Airdopes 141", new BigDecimal("1299"), 50)));
        System.out.println(controller.create(new Product(null, "Redmi Note 13", new BigDecimal("15999"), 0)));
        System.out.println(controller.get(2L));
        System.out.println(controller.get(99L));
    }
}

record Product(Long id, String name, BigDecimal price, int stock) { }
record Response(int status, Object body) { }

class ProductNotFoundException extends RuntimeException {
    ProductNotFoundException(Long id) { super("Product " + id + " not found"); }
}

class InMemoryProductRepository {
    private final Map<Long, Product> store = new TreeMap<>();
    private long nextId = 1;
    Product save(Product p) {
        Product saved = new Product(nextId++, p.name(), p.price(), p.stock());
        store.put(saved.id(), saved);
        return saved;
    }
    Optional<Product> findById(Long id) { return Optional.ofNullable(store.get(id)); }
}

class ProductService {
    private final InMemoryProductRepository repo;
    ProductService(InMemoryProductRepository repo) { this.repo = repo; }
    Product create(Product p) {
        if (p.price().signum() <= 0) throw new IllegalArgumentException("price must be positive");
        return repo.save(p);
    }
    Product get(Long id) { return repo.findById(id).orElseThrow(() -> new ProductNotFoundException(id)); }
}

class ProductController {
    private final ProductService service;
    ProductController(ProductService service) { this.service = service; }
    Response create(Product p) { return handle(() -> new Response(201, service.create(p))); }
    Response get(Long id) { return handle(() -> new Response(200, service.get(id))); }

    // acts like @RestControllerAdvice
    private Response handle(java.util.function.Supplier<Response> action) {
        try { return action.get(); }
        catch (ProductNotFoundException e) { return new Response(404, Map.of("detail", e.getMessage())); }
        catch (IllegalArgumentException e) { return new Response(400, Map.of("detail", e.getMessage())); }
    }
}

Output

Response[status=201, body=Product[id=1, name=Boat Airdopes 141, price=1299, stock=50]]
Response[status=201, body=Product[id=2, name=Redmi Note 13, price=15999, stock=0]]
Response[status=200, body=Product[id=2, name=Redmi Note 13, price=15999, stock=0]]
Response[status=404, body={detail=Product 99 not found}]

Implement pagination and sorting like Pageable.

Example

Sort by price descending, then return page 1 (0-based) of size 2 with page metadata.

import java.util.*;

public class Main {
    record Product(long id, String name, String category, int price) { }
    record Page<T>(List<T> content, int page, int size, long totalElements, int totalPages) { }

    static Page<Product> findAll(List<Product> all, String category, int page, int size, Comparator<Product> sort) {
        List<Product> filtered = all.stream()
                .filter(p -> category == null || p.category().equals(category))
                .sorted(sort)
                .toList();
        int from = Math.min(page * size, filtered.size());
        int to = Math.min(from + size, filtered.size());
        int totalPages = (int) Math.ceil((double) filtered.size() / size);
        return new Page<>(filtered.subList(from, to), page, size, filtered.size(), totalPages);
    }

    public static void main(String[] args) {
        List<Product> products = List.of(
                new Product(1, "Redmi Note 13", "mobiles", 15999),
                new Product(2, "iPhone 15", "mobiles", 69999),
                new Product(3, "Boat Airdopes", "audio", 1299),
                new Product(4, "Samsung M14", "mobiles", 11999),
                new Product(5, "OnePlus Nord", "mobiles", 24999));

        // GET /api/products?category=mobiles&page=1&size=2&sort=price,desc
        Page<Product> result = findAll(products, "mobiles", 1, 2,
                Comparator.comparingInt(Product::price).reversed());
        result.content().forEach(p -> System.out.println(p.name() + " - Rs " + p.price()));
        System.out.println("page=" + result.page() + " size=" + result.size()
                + " totalElements=" + result.totalElements() + " totalPages=" + result.totalPages());
    }
}

Output

Redmi Note 13 - Rs 15999
Samsung M14 - Rs 11999
page=1 size=2 totalElements=4 totalPages=2

Group orders and compute revenue per category with streams.

A common service-layer task (and a common "write the stream" question).

Example

groupingBy with a TreeMap for a stable order, plus the top product by quantity.

import java.util.*;
import java.util.stream.*;

public class Main {
    record OrderItem(String product, String category, int qty, int unitPrice) {
        int total() { return qty * unitPrice; }
    }

    public static void main(String[] args) {
        List<OrderItem> items = List.of(
                new OrderItem("Redmi Note 13", "mobiles", 2, 15999),
                new OrderItem("Boat Airdopes", "audio", 5, 1299),
                new OrderItem("iPhone 15", "mobiles", 1, 69999),
                new OrderItem("JBL Flip 6", "audio", 1, 9999),
                new OrderItem("Prestige Kettle", "home", 3, 899));

        Map<String, Integer> revenue = items.stream()
                .collect(Collectors.groupingBy(OrderItem::category, TreeMap::new,
                        Collectors.summingInt(OrderItem::total)));
        revenue.forEach((cat, total) -> System.out.println(cat + " -> Rs " + total));

        String topSeller = items.stream()
                .max(Comparator.comparingInt(OrderItem::qty))
                .map(OrderItem::product)
                .orElse("none");
        System.out.println("Top seller by quantity: " + topSeller);
        System.out.println("Grand total: Rs " + items.stream().mapToInt(OrderItem::total).sum());
    }
}

Output

audio -> Rs 16494
home -> Rs 2697
mobiles -> Rs 101997
Top seller by quantity: Boat Airdopes
Grand total: Rs 121188

Implement an idempotent payment endpoint.

A retried request with the same Idempotency-Key must return the first result and must not charge again. In production the map is a DB table with a unique key.

Example

Second call with key k-101 returns the stored response.

import java.util.*;

public class Main {
    record PaymentResponse(String paymentId, String orderId, int amount, String status) { }

    static class PaymentService {
        private final Map<String, PaymentResponse> processed = new HashMap<>();
        private int charges = 0;

        synchronized PaymentResponse pay(String idempotencyKey, String orderId, int amount) {
            PaymentResponse existing = processed.get(idempotencyKey);
            if (existing != null) {
                System.out.println("Duplicate key " + idempotencyKey + ": returning stored result");
                return existing;
            }
            charges++;
            PaymentResponse res = new PaymentResponse("pay_" + charges, orderId, amount, "CAPTURED");
            processed.put(idempotencyKey, res);
            System.out.println("Charged Rs " + amount + " for " + orderId);
            return res;
        }
        int charges() { return charges; }
    }

    public static void main(String[] args) {
        PaymentService service = new PaymentService();
        System.out.println(service.pay("k-101", "ORD-1", 15999));
        System.out.println(service.pay("k-101", "ORD-1", 15999)); // client retry after timeout
        System.out.println(service.pay("k-102", "ORD-2", 1299));
        System.out.println("Total charges made: " + service.charges());
    }
}

Output

Charged Rs 15999 for ORD-1
PaymentResponse[paymentId=pay_1, orderId=ORD-1, amount=15999, status=CAPTURED]
Duplicate key k-101: returning stored result
PaymentResponse[paymentId=pay_1, orderId=ORD-1, amount=15999, status=CAPTURED]
Charged Rs 1299 for ORD-2
PaymentResponse[paymentId=pay_2, orderId=ORD-2, amount=1299, status=CAPTURED]
Total charges made: 2

Implement an LRU cache (what @Cacheable with a size limit does).

Example

LinkedHashMap in access order evicts the least recently used entry.

import java.util.*;

public class Main {
    static class LruCache<K, V> extends LinkedHashMap<K, V> {
        private final int capacity;
        LruCache(int capacity) {
            super(16, 0.75f, true);   // true = access order
            this.capacity = capacity;
        }
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > capacity;
        }
    }

    static int dbHits = 0;
    static LruCache<Long, String> cache = new LruCache<>(2);

    static String findProduct(long id) {
        return cache.computeIfAbsent(id, key -> {
            dbHits++;
            return "Product-" + key;          // pretend this is a DB query
        });
    }

    public static void main(String[] args) {
        findProduct(1); findProduct(2);
        findProduct(1);                      // hit, 1 becomes most recent
        findProduct(3);                      // evicts 2
        System.out.println("Cache keys: " + cache.keySet());
        findProduct(2);                      // miss again
        System.out.println("Cache keys: " + cache.keySet());
        System.out.println("DB hits: " + dbHits);
    }
}

Output

Cache keys: [1, 3]
Cache keys: [3, 2]
DB hits: 4

Implement a simple rate limiter for an API (token bucket).

Asked for "limit each user to N requests per second". A fixed fake clock keeps the output deterministic; in Spring you would call it from a HandlerInterceptor or filter and return 429.

Example

Capacity 3, refill 1 token per second.

import java.util.*;

public class Main {
    static class TokenBucket {
        private final int capacity;
        private final double refillPerSecond;
        private double tokens;
        private long lastRefillMillis;

        TokenBucket(int capacity, double refillPerSecond, long nowMillis) {
            this.capacity = capacity;
            this.refillPerSecond = refillPerSecond;
            this.tokens = capacity;
            this.lastRefillMillis = nowMillis;
        }

        synchronized boolean tryConsume(long nowMillis) {
            double elapsedSec = (nowMillis - lastRefillMillis) / 1000.0;
            tokens = Math.min(capacity, tokens + elapsedSec * refillPerSecond);
            lastRefillMillis = nowMillis;
            if (tokens >= 1) { tokens -= 1; return true; }
            return false;
        }
    }

    public static void main(String[] args) {
        Map<String, TokenBucket> buckets = new HashMap<>();
        long[] requestTimes = {0, 100, 200, 300, 1300, 1400, 3500};
        for (long t : requestTimes) {
            TokenBucket bucket = buckets.computeIfAbsent("user-42", u -> new TokenBucket(3, 1.0, 0));
            int status = bucket.tryConsume(t) ? 200 : 429;
            System.out.println("t=" + t + "ms -> HTTP " + status);
        }
    }
}

Output

t=0ms -> HTTP 200
t=100ms -> HTTP 200
t=200ms -> HTTP 200
t=300ms -> HTTP 429
t=1300ms -> HTTP 200
t=1400ms -> HTTP 429
t=3500ms -> HTTP 200