Code review · Java

Review a Java Order Service

Hard Code review

This service is a singleton bean in a multi-threaded web application: many requests call it at the same time.

Review it: list the problems, what would happen under load, and how to fix each one.

The code to review

There are 6 issues to find.

public class OrderService {
    private final DataSource dataSource;
    private final Map<Long, Order> cache = new HashMap<>();
    private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    private PriceTable prices;

    public OrderService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Order find(long id) {
        if (cache.containsKey(id)) return cache.get(id);
        try {
            Connection conn = dataSource.getConnection();
            Statement st = conn.createStatement();
            ResultSet rs = st.executeQuery("SELECT * FROM orders WHERE id = " + id);
            Order order = rs.next() ? Order.from(rs) : null;
            cache.put(id, order);
            return order;
        } catch (Exception e) {
            return null;
        }
    }

    public String label(Order order) {
        return order.getId() + " / " + FORMAT.format(order.getCreatedAt());
    }

    public PriceTable prices() {
        if (prices == null) {
            synchronized (this) {
                if (prices == null) prices = PriceTable.load();
            }
        }
        return prices;
    }
}

Write your review

Loading the editor… (JavaScript is required to run code in your browser.)

The issues and how to fix them

Show the issues

1. JDBC resources are never closed

Critical, line 14-16

The connection, statement and result set are not closed, even on success. Each call leaks a pooled connection; under load the pool is exhausted within minutes and every request hangs waiting for a connection.

Fix: Use try-with-resources for Connection, PreparedStatement and ResultSet.

2. HashMap shared across threads

Major, line 3, 12, 18

HashMap is not thread-safe. Concurrent puts can corrupt it (lost entries, and in old JDKs infinite loops). containsKey then get is also a check-then-act race. The cache also grows forever and never sees updates to orders.

Fix: Use ConcurrentHashMap.computeIfAbsent, or better a bounded cache with expiry (Caffeine), and invalidate on writes.

3. Shared SimpleDateFormat

Major, line 4, 26

SimpleDateFormat keeps internal state and is not thread-safe; concurrent calls produce wrong dates or exceptions.

Fix: Use java.time with the immutable, thread-safe DateTimeFormatter.ISO_LOCAL_DATE.

4. Double-checked locking without volatile

Major, line 5, 30-35

Without volatile, another thread can see a non-null prices reference before the object is fully constructed (the Java Memory Model allows the reordering).

Fix: Declare the field volatile, or use the holder-class idiom, or load it eagerly in the constructor.

5. Exceptions swallowed

Major, line 20-22

Every failure (SQL error, pool timeout, bug) becomes null, which looks the same as "order not found" and is often cached. Nothing is logged.

Fix: Catch SQLException, log it, and rethrow a meaningful exception; do not cache failures.

6. String-built SQL and SELECT *

Minor, line 16

Concatenation prevents statement caching and is an injection habit, even with a long. SELECT * breaks when columns change.

Fix: Use a PreparedStatement with ? and name the columns.

Java reviews for senior roles focus on concurrency in singletons: shared mutable collections, non-thread-safe utility objects, safe publication (volatile), and resource management. Leaked connections are the bug most likely to cause an outage, so lead with it.

Fixed version

public class OrderService {
    private static final DateTimeFormatter FORMAT = DateTimeFormatter.ISO_LOCAL_DATE;
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    private final DataSource dataSource;
    private final Cache<Long, Order> cache = Caffeine.newBuilder()
        .maximumSize(10_000).expireAfterWrite(Duration.ofMinutes(5)).build();
    private volatile PriceTable prices;

    public OrderService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public Optional<Order> find(long id) {
        return Optional.ofNullable(cache.get(id, this::load));
    }

    private Order load(long id) {
        String sql = "SELECT id, customer_id, total, created_at FROM orders WHERE id = ?";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement st = conn.prepareStatement(sql)) {
            st.setLong(1, id);
            try (ResultSet rs = st.executeQuery()) {
                return rs.next() ? Order.from(rs) : null;
            }
        } catch (SQLException e) {
            log.error("Could not load order {}", id, e);
            throw new DataAccessException("Could not load order " + id, e);
        }
    }

    public String label(Order order) {
        return order.getId() + " / " + FORMAT.format(order.getCreatedAt());
    }

    public PriceTable prices() {
        PriceTable p = prices;
        if (p == null) {
            synchronized (this) {
                p = prices;
                if (p == null) prices = p = PriceTable.load();
            }
        }
        return p;
    }
}

Follow-up questions

  • What does the JMM's happens-before guarantee for volatile writes?
  • How would you detect a connection leak in production? (pool metrics, leak detection threshold)