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):
- 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).
- Preferred profiles that have already accepted the user (preferred(requester,candidate)=true AND candidateAcceptedRequester=true), ordered by highest mutual interests.
- Preferred profiles (preferred(requester,candidate)=true AND candidateAcceptedRequester=false), ordered by highest mutual interests.
- 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.