84. Design Dating App Gumble

Design Dating App Gumble
You are launching the Gumble app to compete in the dating apps market. Design and implement an in-memory console application to prototype the features of Gumble.

The system manages user profiles, their interests, partner preferences, a feed that suggests the best available profile to a user, and a matched list.

The platform has a fixed global set of allowed interests, provided once at initialization through the constructor. All interest strings (both global list and user-provided) must contain only lowercase letters and hyphen (-).

Functional Requirements

P0 - Basic features

  • Users can create their profile (name, age, gender).
  • Users can choose their interests from a platform-provided global list (provided in constructor).
  • Users can set partner preferences (age range, gender).
  • Users can request the best available profile and choose to accept or decline.
  • Once accepted/declined, that profile must never appear again in the user’s feed.
  • If two profiles mutually accept, they become a match and appear in each other’s matched list.
  • Users can view their matched list at any time.

P1 - Advanced features

  • To maximize the number of matched users, every time a user receives a match, their likelihood of appearing on another user’s feed goes down. This prioritizes users who have fewer/no matches.
  • Users can buy boost plans to skip the queue and increase their likelihood of appearing in more user feeds.
  • Admins can pull reports: total user count, matched users count, top-n users with highest matches, and cohort sizes (gender, age groups).

P2 - Bonus features

  • Users can mark each preference as strict vs lenient. If a profile fails a lenient preference, it may still be treated similar to a preferred profile but with lower ranking.
  • Users can super-accept a profile, which immediately ranks the sender highest on the receiver’s feed. Each user can super-accept only once in their lifetime.
  • super-accept overrides eligibility. If a candidate has an active super-accept toward requester, they are eligible regardless of preference/accept status.

Assumptions

  • gender is represented as a string such as "MALE", "FEMALE", "OTHER".
  • genderPreference is one of "MALE", "FEMALE", "OTHER", "ANY".
  • Age is an integer in years.
  • If there is no eligible profile for getBestProfile, return "" (empty string).
  • Interest format: an interest string must match the pattern [a-z-]+ (only lowercase letters and hyphen).

Constraints

  • 1 ≤ totalUsers ≤ 100000
  • 0 ≤ interestsPerUser ≤ 100
  • 18 ≤ age ≤ 100
  • 18 ≤ minAge ≤ maxAge ≤ 100
  • 1 ≤ topN ≤ totalUsers
  • All storage is in-memory only; no database usage.
  • The platform maintains a fixed set of allowed interests, provided once via the constructor. User-provided interests not present in this allowed set are ignored.

Platform Global Interest List

The platform provides the following global allowed-interest list in the constructor (all lowercase and hyphen only):

e.g. List.of( "pets","dog-lover", "standup-comedy" )

Method Signatures

0) Constructor

Gumble(List<String> allowedInterests)
  • Initializes the platform with a fixed global allowed-interest set for the entire lifetime of the application.
  • The effective allowed-interest set is formed by:
    • Removing duplicates
    • Keeping only items matching [a-z-]+
    (In this problem, tests will provide the complete valid list shown above.)

1) Upsert profile data

boolean addOrUpdateUser( String userId, String name, int age, String gender, List<String> interests, int minAge, int maxAge, String genderPreference )
Overwriting rule:
  • Calling addOrUpdateUser with the same userId overwrites existing profile data.
Exact overwrite semantics:
  • Profile fields: name, age, gender are overwritten with the provided values.
  • Interests (Key Definition moved here): The user’s interest set becomes exactly the provided interests list after:
    • Removing duplicates inside the list
    • Filtering out interests that do not match [a-z-]+
    • Filtering out interests not in the platform’s global allowed-interest set (from constructor)
    (i.e., previous interests not present in the new input are removed.)

    Mutual interests count between two users = size of intersection of their interest sets.
  • Partner preferences (Key Definition moved here): minAge, maxAge, genderPreference are overwritten with the provided values.

    A candidate is a preferred profile for a requester if the candidate strictly matches the requester’s partner preference:
    • Age range: minAge to maxAge (inclusive)
    • Gender preference: exactly the requested gender OR "ANY"
  • Preserved state on overwrite: prior accept/decline history, matches, boost status, and super-accept usage (if implemented) remain associated with the same userId unless explicitly stated otherwise.
Validation rules:
  • Returns false if:
    • userId is empty
    • age is outside [18, 100]
    • minAge or maxAge is outside [18, 100] or minAge > maxAge
    • gender is not one of "MALE", "FEMALE", "OTHER"
    • genderPreference is not one of "MALE", "FEMALE", "OTHER", "ANY"
  • Returns true otherwise (both “create new” and “overwrite existing”).
  • Allowed-interest filtering:
    • Interests not matching [a-z-]+ are ignored.
    • Interests not present in the platform’s allowed set are ignored.
    • If all interests are invalid or list is empty, the user ends up with an empty interest set (still returns true if other validations pass).

2) Get best profile

String getBestProfile(String userId)
  • Returns the userId of the best available candidate based on the exact eligibility + ranking rules below.
  • Returns "" if userId does not exist or if no candidate is eligible.
  • This call must not mark the candidate as accepted/declined.
Eligibility rules (candidate must satisfy all):
  • Candidate must not be the requesting user.
  • Candidate must not have been previously accepted or declined by the requesting user.
  • Let preferred(requester, candidate) be whether candidate strictly matches requester’s partner preference: age in [minAge, maxAge] inclusive AND (gender matches OR requester genderPreference is "ANY").
  • If candidate is unpreferred for requester, candidate is eligible only if candidate has already accepted requester.
Ranking (highest to lowest) (Key Definition moved here):
  1. Super-accept priority (P2): If one or more users have an active super-accept toward userId, then getBestProfile(userId) must return one of those super-accepting users first. If multiple exist, return the lexicographically smallest super-accepting userId.
    After a super-accept from user X to user Y, X must remain above everything else in Y’s feed until Y reacts (accept/decline).
  2. Preferred profiles that have already accepted the user (preferred(requester,candidate)=true AND candidateAcceptedRequester=true), ordered by highest mutual interests.
  3. Preferred profiles (preferred(requester,candidate)=true AND candidateAcceptedRequester=false), ordered by highest mutual interests.
  4. Unpreferred profiles that have already accepted the user (preferred(requester,candidate)=false AND candidateAcceptedRequester=true), ordered by highest mutual interests.
Within the same bucket (2, 3, 4), resolve multiple candidate clashes in this order:
  • Boost first (P1): boosted candidates rank above non-boosted candidates within the same bucket.
  • Higher mutual interests first.
  • Lower match count first (P1: prioritize users with fewer matches).
  • If still tied, lexicographically smaller userId first.

3) React to profile (accept/decline)

boolean acceptDeclineProfile(String userId, String targetUserId, boolean isAccepted)
  • If isAccepted == true, this records that userId accepted targetUserId.
  • If isAccepted == false, this records that userId declined targetUserId.
  • Accept / Decline / Match (Key Definition moved here):
    • If user A accepts user B: record that A accepted B; B must not appear again in A’s feed.
    • If user A declines user B: record that A declined B; B must not appear again in A’s feed.
    • If A accepts B AND B has already accepted A, then A and B become matched and appear in each other’s matched list.
  • Returns false if either user does not exist, or if userId == targetUserId.
  • If already reacted for same userId and targetUserId, ignore any subsequent reaction (even if different) and return false.

4) List matched profiles

List<String> listMatchedProfiles(String userId)
  • Returns a list of matched userIds for userId, sorted lexicographically.
  • Returns empty list if no matches or user not found.

5) P1 - buy boost

boolean buyBoost(String userId)
  • Marks the user as boosted.
  • Boost behavior (Key Definition moved here): boosted users should be prioritized above non-boosted users within the same ranking bucket in getBestProfile. If multiple boosted users tie, use the same tie-breakers (mutual interests, lower match count, lexicographic userId).
  • Returns false if user not found.

6) P1 - show stats

List<String> showStats(int topN)
  • Returns admin stats as a list of human-readable strings (each entry one line).
  • Must include:
    • Total user count
    • Matched users count (users with at least 1 match)
    • Comma separated Top-N users with highest match count (descending), including match count in format: userId-matchCount
      Example: user1-12,user11-8,user2-8
      Tie-breaker for same match count: lexicographically smaller userId first.
    • Cohort sizes: gender counts and age buckets (18-25, 26-35, 36-50, 51-100)

7) P2 - super accept

boolean superAcceptProfile(String userId, String targetUserId)
  • Each user may super-accept only once in their lifetime.
  • Super-accept behavior (Key Definition moved here): After a super-accept from userId to targetUserId, userId must appear at the very top of targetUserId feed (above everything else) until targetUserId reacts (accept/decline).
  • If multiple users have an active super-accept toward the same target user, break ties by lexicographically smaller userId first.
  • Returns false if invalid users, self-action, or super-accept already used by userId.

Examples

Example 1: Upsert + overwrite semantics + interest filtering + basic ranking by mutual interests

  • Gumble( allowedInterests=List.of( "movies","books","travel","music","pets", "football","standup-comedy","dog-lover" ) ) (app initialized)
  • addOrUpdateUser(userId="u1", name="Asha", age=24, gender="FEMALE", interests=List.of("movies","books","travel"), minAge=22, maxAge=26, genderPreference="MALE")true
  • addOrUpdateUser(userId="u2", name="Ravi", age=25, gender="MALE", interests=List.of("movies","football","music"), minAge=18, maxAge=100, genderPreference="ANY")true
  • addOrUpdateUser(userId="u3", name="Neel", age=23, gender="MALE", interests=List.of("movies","books","pets"), minAge=18, maxAge=100, genderPreference="ANY")true
  • addOrUpdateUser(userId="u4", name="Maya", age=24, gender="FEMALE", interests=List.of("travel","music"), minAge=18, maxAge=100, genderPreference="ANY")true
  • getBestProfile(userId="u1")"u3"
    Explanation: Preferred candidates for u1 are MALE with age 22-26: u2(25), u3(23). Mutual interests: u1 vs u2 = {movies} = 1; u1 vs u3 = {movies,books} = 2. So u3 ranks higher.
  • addOrUpdateUser(userId="u1", name="Asha Sharma", age=25, gender="FEMALE", interests=List.of("music","travel","music","INVALID","dog-lover","unknown"), minAge=23, maxAge=30, genderPreference="ANY")true
    Explanation: Overwrite u1. Interests become exactly {music,travel,dog-lover}. "INVALID" is ignored (does not match [a-z-]+); "unknown" is ignored (not in allowedInterests). Prior accept/decline/matches/boost/super-accept state (if any) remains preserved for userId="u1".
  • getBestProfile(userId="u1")"u4"
    Explanation: u1 now prefers ANY gender age 23-30, so u2(25), u3(23), u4(24) are all preferred. Mutual interests: u1 vs u2 = {music}=1; u1 vs u3 = {}=0; u1 vs u4 = {music,travel}=2. So u4 ranks highest.

Example 2: Eligibility buckets + accept/decline rules (one-shot reaction) + unpreferred-only-if-accepted + matching + listMatchedProfiles

Continuing from Example 1:
  • addOrUpdateUser(userId="u5", name="Dev", age=29, gender="MALE", interests=List.of("travel","books"), minAge=18, maxAge=100, genderPreference="ANY")true
    Explanation: u5 exists but is unpreferred for u1 when u1 later sets a tight age range.
  • addOrUpdateUser(userId="u1", name="Asha Sharma", age=25, gender="FEMALE", interests=List.of("music","travel"), minAge=22, maxAge=26, genderPreference="MALE")true
    Explanation: Overwrite u1's preferences back to MALE, age 22-26. Prior reaction history is preserved.
  • acceptDeclineProfile(userId="u1", targetUserId="u2", isAccepted=true)true
    Explanation: u2 must never appear again in u1's feed.
  • acceptDeclineProfile(userId="u1", targetUserId="u2", isAccepted=false)false
    Explanation: u1 already reacted to u2 earlier; subsequent reactions are ignored and return false.
  • getBestProfile(userId="u1")"u3"
    Explanation: u2 is excluded (already reacted). u3 is preferred (MALE, 23 within 22-26) and not yet reacted. u5 is unpreferred (age 29 outside 22-26) and u5 has not accepted u1 yet, so u1 cannot see u5.
  • acceptDeclineProfile(userId="u1", targetUserId="u3", isAccepted=false)true
    Explanation: u3 must never appear again in u1's feed.
  • getBestProfile(userId="u1")""
    Explanation: u2 accepted and u3 declined already; u4 is unpreferred (FEMALE) and has not accepted u1; u5 is unpreferred (age 29) and has not accepted u1. Therefore, no candidate is eligible.
  • acceptDeclineProfile(userId="u5", targetUserId="u1", isAccepted=true)true
    Explanation: u5 has now accepted u1; this can make u5 eligible to appear for u1 even if u5 is unpreferred.
  • getBestProfile(userId="u1")"u5"
    Explanation: u5 is unpreferred for u1 (age 29), but u5 already accepted u1, so u5 is eligible in the "unpreferred but accepted" bucket. Others remain ineligible due to prior reactions or lack of acceptance.
  • acceptDeclineProfile(userId="u1", targetUserId="u5", isAccepted=true)true
    Explanation: Mutual acceptance (u1 accepted u5 and u5 had already accepted u1) creates a match.
  • listMatchedProfiles(userId="u1")List.of("u5")
  • listMatchedProfiles(userId="u5")List.of("u1")

Example 3: P1 tie-breakers (boost, mutual interests, match count, lexicographic) + P2 super-accept priority + showStats

Continuing from Example 2:
  • addOrUpdateUser(userId="u6", name="Kiran", age=24, gender="MALE", interests=List.of("travel"), minAge=18, maxAge=100, genderPreference="ANY")true
  • addOrUpdateUser(userId="u7", name="Raj", age=24, gender="MALE", interests=List.of("travel"), minAge=18, maxAge=100, genderPreference="ANY")true
  • addOrUpdateUser(userId="u8", name="Ira", age=24, gender="FEMALE", interests=List.of("travel"), minAge=18, maxAge=100, genderPreference="ANY")true
  • addOrUpdateUser(userId="u9", name="Sara", age=24, gender="FEMALE", interests=List.of("travel"), minAge=18, maxAge=100, genderPreference="ANY")true
  • acceptDeclineProfile(userId="u6", targetUserId="u8", isAccepted=true)true
  • acceptDeclineProfile(userId="u8", targetUserId="u6", isAccepted=true)true
    Explanation: u6 and u8 match, so u6 now has 1 match.
  • acceptDeclineProfile(userId="u6", targetUserId="u9", isAccepted=true)true
  • acceptDeclineProfile(userId="u9", targetUserId="u6", isAccepted=true)true
    Explanation: u6 and u9 match, so u6 now has 2 matches.
  • addOrUpdateUser(userId="u4", name="Maya", age=24, gender="FEMALE", interests=List.of("travel","music"), minAge=24, maxAge=24, genderPreference="MALE")true
    Explanation: u4 now prefers MALE age exactly 24, making u6 and u7 preferred candidates. (u2 is age 25, so not preferred; also u2 was previously reacted to by u1, but that does not affect u4.)
  • getBestProfile(userId="u4")"u7"
    Explanation: Both u6 and u7 are in the same bucket (preferred, and neither accepted u4). Mutual interests tie (both share only "travel" = 1). Neither is boosted, so tie-breaker "lower match count first" applies: u7 has 0 matches, u6 has 2 matches.
  • buyBoost(userId="u6")true
  • getBestProfile(userId="u4")"u6"
    Explanation: Same bucket and mutual-interest tie, but boosted users rank above non-boosted users within the same bucket.
  • superAcceptProfile(userId="u2", targetUserId="u4")true
  • getBestProfile(userId="u4")"u2"
    Explanation: Super-accept has highest priority. u2 must appear at the very top of u4's feed until u4 reacts.
  • superAcceptProfile(userId="u2", targetUserId="u1")false
  • Explanation: Each user can super-accept only once in their lifetime.
  • showStats(topN=3)List.of( "totalUsers=9", "matchedUsers=5", "topNByMatches=u6-2,u1-1,u5-1", "genderCohort=MALE:5,FEMALE:4,OTHER:0", "ageCohort=18-25:8,26-35:1,36-50:0,51-100:0" )
    Explanation: totalUsers = {u1..u9} = 9. Matched pairs are (u1,u5), (u6,u8), (u6,u9) so matched users are {u1,u5,u6,u8,u9} = 5. Top-3 by match count: u6 has 2, then u1 and u5 have 1 (lexicographic tie-break). Gender cohort counts: MALE users are u2,u3,u5,u6,u7 (5); FEMALE users are u1,u4,u8,u9 (4); OTHER is 0. Age cohort: 18-25 includes u1(25),u2(25),u3(23),u4(24),u6(24),u7(24),u8(24),u9(24) = 8; 26-35 includes u5(29) = 1.




Please use Laptop/Desktop or any other large screen to add/edit code.