Code · LLD machine coding · PhonePe prep

Hackathon Platform (machine coding round)

Design a coding-contest platform.

23 classes 621 lines StrategyRepositoryCustom exceptionsService layer View on GitHub ↗

Question

Design a coding-contest platform.

  1. Add problems to a question library: description, tags, difficulty, score.
  2. Contestants register with a name and department.
  3. Fetch problems filtered by difficulty or tag and sorted by score, extensible to new filters and sorts.
  4. A contestant solves a problem (with the time taken); list the problems a contestant has solved.
  5. For a problem: how many users solved it and the average time taken.
  6. Scoring is pluggable: a fixed score, or a score adjusted by solve time.
  7. Return the current leader.
  8. Top N most-solved problems for a tag.
  9. Extension: after a solve, recommend 5 problems with similar tags.

Design: repositories + services, Strategy for scoring and recommendations, Predicate / Comparator for filters, custom exceptions, unit tests in src/test. Run HackathonPlatformDemo; it prints each requirement in order.

entities/Contestant.java 22 lines GitHub ↗
package com.practice.phonepe.interview.entities;

import java.util.UUID;

public class Contestant {

    private final String id;
    private final String name;
    private final String department;

    //add a field to store createdat
    public Contestant(String name, String department) {
        this.id = UUID.randomUUID().toString();
        this.name = name;
        this.department = department;

    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getDepartment() {
        return department;
    }
}
entities/Difficulty.java 4 lines GitHub ↗
package com.practice.phonepe.interview.entities;

public enum Difficulty {
    EASY, MEDIUM, HARD
}
entities/Problem.java 32 lines GitHub ↗
package com.practice.phonepe.interview.entities;

import java.util.Set;
import java.util.UUID;

public class Problem {

    private final String id;
    private final String description;
    private final Set<String> tags;
    private final Difficulty difficulty;
    private final int baseScore;

    public Problem(String description, Set<String> tags, Difficulty difficulty, int baseScore) {
        this.id = UUID.randomUUID().toString();
        this.description = description;
        this.tags = tags;
        this.difficulty = difficulty;
        this.baseScore = baseScore;
    }

    public String getId() {
        return id;
    }

    public String getDescription() {
        return description;
    }

    public Set<String> getTags() {
        return tags;
    }

    public Difficulty getDifficulty() {
        return difficulty;
    }

    public int getBaseScore() {
        return baseScore;
    }
}
entities/Submission.java 33 lines GitHub ↗
package com.practice.phonepe.interview.entities;

import java.time.Instant;

public class Submission {

    private final String contestantId;
    private final String problemId;
    private final int timeTakenSeconds;
    private final int scoreAwarded;
    private final Instant solvedAt;

    // scoreAwarded is captured at solve time rather than recomputed later, so a contestant's
    // total score stays correct even if the scoring strategy is swapped mid-contest.
    public Submission(String contestantId, String problemId, int timeTakenSeconds, int scoreAwarded) {
        this.contestantId = contestantId;
        this.problemId = problemId;
        this.timeTakenSeconds = timeTakenSeconds;
        this.scoreAwarded = scoreAwarded;
        this.solvedAt = Instant.now();
    }

    public String getContestantId() {
        return contestantId;
    }

    public String getProblemId() {
        return problemId;
    }

    public int getTimeTakenSeconds() {
        return timeTakenSeconds;
    }

    public int getScoreAwarded() {
        return scoreAwarded;
    }

    public Instant getSolvedAt() {
        return solvedAt;
    }
}
exception/AlreadySolvedException.java 6 lines GitHub ↗
package com.practice.phonepe.interview.exception;

public class AlreadySolvedException extends RuntimeException {

    public AlreadySolvedException(String message) {
        super(message);
    }
}
exception/EntityNotFoundException.java 6 lines GitHub ↗
package com.practice.phonepe.interview.exception;

public class EntityNotFoundException extends RuntimeException {

    public EntityNotFoundException(String message) {
        super(message);
    }
}
repository/ContestantRepository.java 9 lines GitHub ↗
package com.practice.phonepe.interview.repository;

import com.practice.phonepe.interview.entities.Contestant;

import java.util.List;
import java.util.Optional;

public interface ContestantRepository {

    void save(Contestant contestant);

    Optional<Contestant> findById(String contestantId);

    List<Contestant> findAll();
}
repository/InMemoryContestantRepository.java 22 lines GitHub ↗
package com.practice.phonepe.interview.repository;

import com.practice.phonepe.interview.entities.Contestant;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class InMemoryContestantRepository implements ContestantRepository {

    private final Map<String, Contestant> contestantsById = new LinkedHashMap<>();

    @Override
    public void save(Contestant contestant) {
        contestantsById.put(contestant.getId(), contestant);
    }

    @Override
    public Optional<Contestant> findById(String contestantId) {
        return Optional.ofNullable(contestantsById.get(contestantId));
    }

    @Override
    public List<Contestant> findAll() {
        return new ArrayList<>(contestantsById.values());
    }
}
repository/InMemoryProblemRepository.java 22 lines GitHub ↗
package com.practice.phonepe.interview.repository;

import com.practice.phonepe.interview.entities.Problem;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class InMemoryProblemRepository implements ProblemRepository {

    private final Map<String, Problem> problemsById = new LinkedHashMap<>();

    @Override
    public void save(Problem problem) {
        problemsById.put(problem.getId(), problem);
    }

    @Override
    public Optional<Problem> findById(String problemId) {
        return Optional.ofNullable(problemsById.get(problemId));
    }

    @Override
    public List<Problem> findAll() {
        return new ArrayList<>(problemsById.values());
    }
}
repository/InMemorySubmissionRepository.java 28 lines GitHub ↗
package com.practice.phonepe.interview.repository;

import com.practice.phonepe.interview.entities.Submission;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class InMemorySubmissionRepository implements SubmissionRepository {

    private final List<Submission> submissions = new ArrayList<>();

    @Override
    public void save(Submission submission) {
        submissions.add(submission);
    }

    @Override
    public List<Submission> findByContestant(String contestantId) {
        return submissions.stream()
                .filter(submission -> submission.getContestantId().equals(contestantId))
                .collect(Collectors.toList());
    }

    @Override
    public List<Submission> findByProblem(String problemId) {
        return submissions.stream()
                .filter(submission -> submission.getProblemId().equals(problemId))
                .collect(Collectors.toList());
    }

    @Override
    public List<Submission> findAll() {
        return new ArrayList<>(submissions);
    }
}
repository/ProblemRepository.java 9 lines GitHub ↗
package com.practice.phonepe.interview.repository;

import com.practice.phonepe.interview.entities.Problem;

import java.util.List;
import java.util.Optional;

public interface ProblemRepository {

    void save(Problem problem);

    Optional<Problem> findById(String problemId);

    List<Problem> findAll();
}
repository/SubmissionRepository.java 9 lines GitHub ↗
package com.practice.phonepe.interview.repository;

import com.practice.phonepe.interview.entities.Submission;

import java.util.List;

public interface SubmissionRepository {

    void save(Submission submission);

    List<Submission> findByContestant(String contestantId);

    List<Submission> findByProblem(String problemId);

    List<Submission> findAll();
}
service/ContestantService.java 37 lines GitHub ↗
package com.practice.phonepe.interview.service;

import com.practice.phonepe.interview.entities.Contestant;
import com.practice.phonepe.interview.entities.Problem;
import com.practice.phonepe.interview.exception.EntityNotFoundException;
import com.practice.phonepe.interview.repository.ContestantRepository;
import com.practice.phonepe.interview.repository.ProblemRepository;
import com.practice.phonepe.interview.repository.SubmissionRepository;

import java.util.List;
import java.util.stream.Collectors;

public class ContestantService {

    private final ContestantRepository contestantRepository;
    private final SubmissionRepository submissionRepository;
    private final ProblemRepository problemRepository;

    public ContestantService(ContestantRepository contestantRepository,
                              SubmissionRepository submissionRepository,
                              ProblemRepository problemRepository) {
        this.contestantRepository = contestantRepository;
        this.submissionRepository = submissionRepository;
        this.problemRepository = problemRepository;
    }

    public Contestant addContestant(String name, String department) {
        Contestant contestant = new Contestant(name, department);
        contestantRepository.save(contestant);
        return contestant;
    }

    public List<Problem> fetchSolvedProblems(String contestantId) {
        requireContestant(contestantId);
        return submissionRepository.findByContestant(contestantId).stream()
                .map(submission -> problemRepository.findById(submission.getProblemId())
                        .orElseThrow(() -> new EntityNotFoundException("Problem not found: " + submission.getProblemId())))
                .collect(Collectors.toList());
    }

    private Contestant requireContestant(String contestantId) {
        return contestantRepository.findById(contestantId)
                .orElseThrow(() -> new EntityNotFoundException("Contestant not found: " + contestantId));
    }
}
service/ContestService.java 86 lines GitHub ↗
package com.practice.phonepe.interview.service;

import com.practice.phonepe.interview.entities.Contestant;
import com.practice.phonepe.interview.entities.Problem;
import com.practice.phonepe.interview.entities.Submission;
import com.practice.phonepe.interview.exception.AlreadySolvedException;
import com.practice.phonepe.interview.exception.EntityNotFoundException;
import com.practice.phonepe.interview.repository.ContestantRepository;
import com.practice.phonepe.interview.repository.ProblemRepository;
import com.practice.phonepe.interview.repository.SubmissionRepository;
import com.practice.phonepe.interview.strategy.RecommendationStrategy;
import com.practice.phonepe.interview.strategy.ScoringStrategy;

import java.security.KeyPair;
import java.util.*;
import java.util.stream.Collectors;

class Pair {
    int key;
    Contestant contestant;
    public Pair(int key , Contestant contestant) {
        this.key = key;
        this.contestant = contestant;
    }
}

public class ContestService {

    private static final int RECOMMENDATION_COUNT = 5;

    private final ContestantRepository contestantRepository;
    private final ProblemRepository problemRepository;
    private final SubmissionRepository submissionRepository;
    private final ScoringStrategy scoringStrategy;
    private final RecommendationStrategy recommendationStrategy;
    private final PriorityQueue<Pair> leaderBoard;

    public ContestService(ContestantRepository contestantRepository,
                           ProblemRepository problemRepository,
                           SubmissionRepository submissionRepository,
                           ScoringStrategy scoringStrategy,
                           RecommendationStrategy recommendationStrategy) {
        this.contestantRepository = contestantRepository;
        this.problemRepository = problemRepository;
        this.submissionRepository = submissionRepository;
        this.scoringStrategy = scoringStrategy;
        this.recommendationStrategy = recommendationStrategy;
        this.leaderBoard = new PriorityQueue<>();
    }

    public SolveResult solve(String contestantId, String problemId, int timeTakenSeconds) {
        Contestant contestant = contestantRepository.findById(contestantId)
                .orElseThrow(() -> new EntityNotFoundException("Contestant not found: " + contestantId));
        Problem problem = problemRepository.findById(problemId)
                .orElseThrow(() -> new EntityNotFoundException("Problem not found: " + problemId));

        Set<String> solvedProblemIds = submissionRepository.findByContestant(contestantId).stream()
                .map(Submission::getProblemId)
                .collect(Collectors.toSet());
        if (solvedProblemIds.contains(problemId)) {
            throw new AlreadySolvedException(contestant.getName() + " has already solved this problem");
        }

        int scoreAwarded = scoringStrategy.calculateScore(problem, timeTakenSeconds);
        submissionRepository.save(new Submission(contestantId, problemId, timeTakenSeconds, scoreAwarded));
        //
        Pair pair = new Pair(scoreAwarded, contestant);
        leaderBoard.add(pair);

        List<Problem> candidates = problemRepository.findAll().stream()
                .filter(candidate -> !candidate.getId().equals(problemId))
                .filter(candidate -> !solvedProblemIds.contains(candidate.getId()))
                .collect(Collectors.toList());
        List<Problem> recommendations = recommendationStrategy.recommend(problem, candidates, RECOMMENDATION_COUNT);
        // M - contestant , N - problem
        // O(log(M)) + O(N)
        return new SolveResult(scoreAwarded, recommendations);
    }

    public Optional<Contestant> getLeader() {
        if (submissionRepository.findAll().isEmpty()) {
            return Optional.empty();
        }

        Map<String, Integer> totalScoreByContestant = submissionRepository.findAll().stream()
                .collect(Collectors.groupingBy(Submission::getContestantId, Collectors.summingInt(Submission::getScoreAwarded)));

        // Assumption: ties broken by earliest-registered contestant
        Contestant leader = null;
        int bestScore = Integer.MIN_VALUE;
        for (Contestant contestant : contestantRepository.findAll()) {
            int score = totalScoreByContestant.getOrDefault(contestant.getId(), 0);
            if (score > bestScore) {
                bestScore = score;
                leader = contestant;
            }
        }
        return Optional.ofNullable(leader);
    }


}
service/ProblemService.java 48 lines GitHub ↗
package com.practice.phonepe.interview.service;

import com.practice.phonepe.interview.entities.Problem;
import com.practice.phonepe.interview.entities.Submission;
import com.practice.phonepe.interview.exception.EntityNotFoundException;
import com.practice.phonepe.interview.repository.ProblemRepository;
import com.practice.phonepe.interview.repository.SubmissionRepository;

import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class ProblemService {

    private final ProblemRepository problemRepository;
    private final SubmissionRepository submissionRepository;

    public ProblemService(ProblemRepository problemRepository, SubmissionRepository submissionRepository) {
        this.problemRepository = problemRepository;
        this.submissionRepository = submissionRepository;
    }

    public void addProblem(Problem problem) {
        problemRepository.save(problem);
    }

    public List<Problem> fetchProblems(Predicate<Problem> filter, Comparator<Problem> sortBy) {
        return problemRepository.findAll().stream()
                .filter(filter)
                .sorted(sortBy)
                .collect(Collectors.toList());
    }

    public ProblemStats getProblemStats(String problemId) {
        requireProblem(problemId);
        List<Submission> submissions = submissionRepository.findByProblem(problemId);
        double averageTime = submissions.stream()
                .mapToInt(Submission::getTimeTakenSeconds)
                .average()
                .orElse(0);
        return new ProblemStats(submissions.size(), averageTime);
    }

    // "Liked" isn't defined anywhere in the spec beyond solving, so it's treated as solve count.
    public List<Problem> getTopNProblems(String tag, int n) {
        return problemRepository.findAll().stream()
                .filter(problem -> problem.getTags().contains(tag))
                .sorted(Comparator.comparingInt((Problem problem) -> submissionRepository.findByProblem(problem.getId()).size()).reversed())
                .limit(n)
                .collect(Collectors.toList());
    }

    private Problem requireProblem(String problemId) {
        return problemRepository.findById(problemId)
                .orElseThrow(() -> new EntityNotFoundException("Problem not found: " + problemId));
    }
}
service/ProblemStats.java 15 lines GitHub ↗
package com.practice.phonepe.interview.service;

public class ProblemStats {

    private final int solveCount;
    private final double averageTimeTakenSeconds;

    public ProblemStats(int solveCount, double averageTimeTakenSeconds) {
        this.solveCount = solveCount;
        this.averageTimeTakenSeconds = averageTimeTakenSeconds;
    }

    public int getSolveCount() {
        return solveCount;
    }

    public double getAverageTimeTakenSeconds() {
        return averageTimeTakenSeconds;
    }
}
service/SolveResult.java 17 lines GitHub ↗
package com.practice.phonepe.interview.service;

import com.practice.phonepe.interview.entities.Problem;

import java.util.List;

public class SolveResult {

    private final int scoreAwarded;
    private final List<Problem> recommendedProblems;

    public SolveResult(int scoreAwarded, List<Problem> recommendedProblems) {
        this.scoreAwarded = scoreAwarded;
        this.recommendedProblems = recommendedProblems;
    }

    public int getScoreAwarded() {
        return scoreAwarded;
    }

    public List<Problem> getRecommendedProblems() {
        return recommendedProblems;
    }
}
strategy/FixedScoreStrategy.java 8 lines GitHub ↗
package com.practice.phonepe.interview.strategy;

import com.practice.phonepe.interview.entities.Problem;

public class FixedScoreStrategy implements ScoringStrategy {

    @Override
    public int calculateScore(Problem problem, int timeTakenSeconds) {
        return problem.getBaseScore();
    }
}
strategy/RecommendationStrategy.java 8 lines GitHub ↗
package com.practice.phonepe.interview.strategy;

import com.practice.phonepe.interview.entities.Problem;

import java.util.List;

public interface RecommendationStrategy {

    // candidates is expected to already exclude justSolved and anything the contestant has
    // solved, so the strategy only has to rank, not filter.
    List<Problem> recommend(Problem justSolved, List<Problem> candidates, int count);
}
strategy/ScoringStrategy.java 5 lines GitHub ↗
package com.practice.phonepe.interview.strategy;

import com.practice.phonepe.interview.entities.Problem;

public interface ScoringStrategy {

    int calculateScore(Problem problem, int timeTakenSeconds);
}
strategy/SimilarTagRecommendationStrategy.java 21 lines GitHub ↗
package com.practice.phonepe.interview.strategy;

import com.practice.phonepe.interview.entities.Problem;

import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class SimilarTagRecommendationStrategy implements RecommendationStrategy {

    @Override
    public List<Problem> recommend(Problem justSolved, List<Problem> candidates, int count) {
        return candidates.stream()
                .sorted(Comparator.comparingInt((Problem candidate) -> sharedTagCount(justSolved, candidate)).reversed())
                .limit(count)
                .collect(Collectors.toList());
    }

    private int sharedTagCount(Problem a, Problem b) {
        Set<String> shared = new HashSet<>(a.getTags());
        shared.retainAll(b.getTags());
        return shared.size();
    }
}
strategy/TimeAdjustedScoreStrategy.java 19 lines GitHub ↗
package com.practice.phonepe.interview.strategy;

import com.practice.phonepe.interview.entities.Problem;

public class TimeAdjustedScoreStrategy implements ScoringStrategy {

    private final int fastSolveThresholdSeconds;

    public TimeAdjustedScoreStrategy(int fastSolveThresholdSeconds) {
        this.fastSolveThresholdSeconds = fastSolveThresholdSeconds;
    }

    @Override
    public int calculateScore(Problem problem, int timeTakenSeconds) {
        // Assumption: no exact formula was specified. Full score within the threshold, then a
        // 10% penalty per additional threshold interval, floored at half the base score.
        if (timeTakenSeconds <= fastSolveThresholdSeconds) {
            return problem.getBaseScore();
        }
        int intervalsLate = (timeTakenSeconds - fastSolveThresholdSeconds) / fastSolveThresholdSeconds;
        double penalty = Math.min(0.5, intervalsLate * 0.1);
        return (int) Math.round(problem.getBaseScore() * (1 - penalty));
    }
}
HackathonPlatformDemo.java 155 lines GitHub ↗
package com.practice.phonepe.interview;

import com.practice.phonepe.interview.entities.Contestant;
import com.practice.phonepe.interview.entities.Difficulty;
import com.practice.phonepe.interview.entities.Problem;
import com.practice.phonepe.interview.exception.AlreadySolvedException;
import com.practice.phonepe.interview.exception.EntityNotFoundException;
import com.practice.phonepe.interview.repository.ContestantRepository;
import com.practice.phonepe.interview.repository.InMemoryContestantRepository;
import com.practice.phonepe.interview.repository.InMemoryProblemRepository;
import com.practice.phonepe.interview.repository.InMemorySubmissionRepository;
import com.practice.phonepe.interview.repository.ProblemRepository;
import com.practice.phonepe.interview.repository.SubmissionRepository;
import com.practice.phonepe.interview.service.ContestService;
import com.practice.phonepe.interview.service.ContestantService;
import com.practice.phonepe.interview.service.ProblemService;
import com.practice.phonepe.interview.service.ProblemStats;
import com.practice.phonepe.interview.service.SolveResult;
import com.practice.phonepe.interview.strategy.FixedScoreStrategy;
import com.practice.phonepe.interview.strategy.RecommendationStrategy;
import com.practice.phonepe.interview.strategy.ScoringStrategy;
import com.practice.phonepe.interview.strategy.SimilarTagRecommendationStrategy;
import com.practice.phonepe.interview.strategy.TimeAdjustedScoreStrategy;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Set;

// Each section below is labelled with the exact requirement it answers, in the same order
// the PDF lists them, so the two can be read side by side.
public class HackathonPlatformDemo {

    public static void main(String[] args) {
        ProblemRepository problemRepository = new InMemoryProblemRepository();
        ContestantRepository contestantRepository = new InMemoryContestantRepository();
        SubmissionRepository submissionRepository = new InMemorySubmissionRepository();

        ScoringStrategy scoringStrategy = new TimeAdjustedScoreStrategy(300); // 5 min fast-solve threshold
        RecommendationStrategy recommendationStrategy = new SimilarTagRecommendationStrategy();

        ProblemService problemService = new ProblemService(problemRepository, submissionRepository);
        ContestantService contestantService = new ContestantService(contestantRepository, submissionRepository, problemRepository);
        ContestService contestService = new ContestService(
                contestantRepository, problemRepository, submissionRepository, scoringStrategy, recommendationStrategy);

        requirement(1, "add problems to the Questions Library");
        Problem twoSum = new Problem("Two Sum", Set.of("array", "hashing"), Difficulty.EASY, 100);
        Problem mergeIntervals = new Problem("Merge Intervals", Set.of("array", "sorting"), Difficulty.MEDIUM, 200);
        Problem lruCache = new Problem("LRU Cache", Set.of("design", "hashing"), Difficulty.MEDIUM, 250);
        Problem wordLadder = new Problem("Word Ladder", Set.of("graph", "bfs"), Difficulty.HARD, 400);
        Problem medianOfArrays = new Problem("Median of Two Sorted Arrays", Set.of("array", "binary-search"), Difficulty.HARD, 450);
        Problem validParentheses = new Problem("Valid Parentheses", Set.of("stack"), Difficulty.EASY, 80);
        List.of(twoSum, mergeIntervals, lruCache, wordLadder, medianOfArrays, validParentheses)
                .forEach(problemService::addProblem);
        System.out.println(problemRepository.findAll().size() + " problems added");

        requirement(2, "contestants register with their name and department");
        Contestant aayushi = contestantService.addContestant("Aayushi", "Backend");
        Contestant riya = contestantService.addContestant("Riya", "Frontend");
        Contestant karan = contestantService.addContestant("Karan", "Backend");
        System.out.println(aayushi.getName() + " (" + aayushi.getDepartment() + ") registered");
        System.out.println(riya.getName() + " (" + riya.getDepartment() + ") registered");
        System.out.println(karan.getName() + " (" + karan.getDepartment() + ") registered");

        requirement(3, "a problem carries description, tag(s), difficulty, score");
        printProblems(List.of(twoSum));

        requirement(4, "filter by difficulty or tag, sort by score (extensible via Predicate/Comparator)");
        System.out.println("filter difficulty=EASY, sort by score ascending:");
        printProblems(problemService.fetchProblems(
                problem -> problem.getDifficulty() == Difficulty.EASY,
                Comparator.comparingInt(Problem::getBaseScore)));
        System.out.println("filter tag=array, sort by score descending:");
        printProblems(problemService.fetchProblems(
                problem -> problem.getTags().contains("array"),
                Comparator.comparingInt(Problem::getBaseScore).reversed()));

        requirement(5, "solve a problem, then list problems solved by that contestant");
        List<SolveResult> solves = new ArrayList<>();
        solves.add(solveAndPrint(contestService, aayushi, twoSum, 200));
        solves.add(solveAndPrint(contestService, aayushi, mergeIntervals, 900));
        solves.add(solveAndPrint(contestService, riya, twoSum, 700));
        solves.add(solveAndPrint(contestService, karan, lruCache, 300));
        solves.add(solveAndPrint(contestService, aayushi, validParentheses, 100));
        System.out.println("problems solved by Aayushi:");
        printProblems(contestantService.fetchSolvedProblems(aayushi.getId()));

        requirement(6, "number of users who solved a problem + average time taken");
        ProblemStats twoSumStats = problemService.getProblemStats(twoSum.getId());
        System.out.println("Two Sum -> solvedBy=" + twoSumStats.getSolveCount()
                + " users, avgTime=" + twoSumStats.getAverageTimeTakenSeconds() + "s");

        requirement(7, "scoring strategy is pluggable: fixed score, or score adjusted by time");
        ScoringStrategy fixed = new FixedScoreStrategy();
        int slowSolveTime = 900;
        System.out.println("Merge Intervals solved in " + slowSolveTime + "s:");
        System.out.println("  FixedScoreStrategy       -> " + fixed.calculateScore(mergeIntervals, slowSolveTime));
        System.out.println("  TimeAdjustedScoreStrategy -> " + scoringStrategy.calculateScore(mergeIntervals, slowSolveTime));

        requirement(8, "return the current leader of the contest");
        Optional<Contestant> leader = contestService.getLeader();
        leader.ifPresent(c -> System.out.println(c.getName() + " (" + c.getDepartment() + ") is leading"));

        requirement(9, "top N liked problems of a given tag (\"liked\" = solve count)");
        System.out.println("top 10 tagged 'array' (only 3 exist, so all 3 come back):");
        printProblems(problemService.getTopNProblems("array", 10));

        extension("on solving a problem, return top 5 recommended problems by similar tags");
        for (SolveResult solve : solves) {
            if (!solve.getRecommendedProblems().isEmpty()) {
                System.out.println("  recommended: " + describe(solve.getRecommendedProblems()));
            }
        }

        note("exceptions handled gracefully, errors printed to the console");
        try {
            contestService.solve("no-such-contestant", twoSum.getId(), 100);
        } catch (EntityNotFoundException e) {
            System.err.println("ERROR: " + e.getMessage());
        }
        try {
            contestService.solve(aayushi.getId(), "no-such-problem", 100);
        } catch (EntityNotFoundException e) {
            System.err.println("ERROR: " + e.getMessage());
        }
        try {
            contestService.solve(aayushi.getId(), twoSum.getId(), 150); // already solved above
        } catch (AlreadySolvedException e) {
            System.err.println("ERROR: " + e.getMessage());
        }
    }

    private static SolveResult solveAndPrint(ContestService contestService, Contestant contestant, Problem problem, int timeTakenSeconds) {
        SolveResult result = contestService.solve(contestant.getId(), problem.getId(), timeTakenSeconds);
        System.out.println(contestant.getName() + " solved '" + problem.getDescription()
                + "' in " + timeTakenSeconds + "s -> scored " + result.getScoreAwarded());
        return result;
    }

    private static void requirement(int number, String description) {
        System.out.println();
        System.out.println("=== [Mandatory " + number + "] " + description + " ===");
    }

    private static void extension(String description) {
        System.out.println();
        System.out.println("=== [Extension] " + description + " ===");
    }

    private static void note(String description) {
        System.out.println();
        System.out.println("=== [" + description + "] ===");
    }

    private static void printProblems(List<Problem> problems) {
        if (problems.isEmpty()) {
            System.out.println("  (none)");
            return;
        }
        for (Problem problem : problems) {
            System.out.printf("  - %-28s tags=%-24s difficulty=%-6s score=%d%n",
                    problem.getDescription(), problem.getTags(), problem.getDifficulty(), problem.getBaseScore());
        }
    }

    private static String describe(List<Problem> problems) {
        StringBuilder builder = new StringBuilder();
        for (Problem problem : problems) {
            if (builder.length() > 0) {
                builder.append(", ");
            }
            builder.append(problem.getDescription());
        }
        return builder.toString();
    }
}