Buildout // Geektrust // Design + Implement
A simplified command-line version of Slido — a live Q&A platform where users create events, post questions, upvote, and reply. The features matter, but the real goal is a well-designed, scalable, modular application. This reference threads the domain model, the SEBER layers, the Command Pattern, and all eight operations with their exact I/O contracts.
Slido lets audiences ask and vote on questions during live meetings and events. Xlido is the CLI version, supporting seven core features.
POPULAR
(votes) or RECENT.Nouns in the requirements become entities. Ownership rules (organizer, author) drive the relationships and most error cases.
User ↔ Event — a user (organizer) creates many events; only the
organizer may delete an event.Event ↔ Question — an event holds many questions; a question
belongs to one event.User ↔ Question — a user authors many questions; only the author
may delete a question.Question ↔ User (upvoters) — a Set of upvoters enforces
"one vote per user" — re-voting is rejected.Question ↔ Reply — a question holds many replies, each written by
a user.The same layered architecture as any well-designed CLI app: each request flows down through named layers, each with one job.
The CLI line, parsed and dispatched to a command.
UserService, EventService, QuestionService orchestrate each use case.
Ownership checks, uniqueness, double-vote guards, sorting.
User, Event, Question, Reply.
In-memory stores behind interfaces, one per entity.
Each operation's input format, parameters, output/error contract, and sample I/O. Output strings must match exactly — verification is a text diff.
Registers a new user with a unique email and a password.
email (string) — must be unique across the system.password (string).Input
CREATE_USER, <email>, <password>
Output
User ID: <user_id>
| # | Input | Output |
|---|---|---|
| 1 | CREATE_USER,test1@user.com,abcd | User ID: 1 |
Creates a new event with the given name, owned by an organizer.
title (string) — event name.organizer_id (number) — ID of the user who creates the event.Input
CREATE_EVENT, <title>, <organizer_id>
Output
Event ID: <event_id> or
ERROR: <message>
| # | Input | Output |
|---|---|---|
| 1 | CREATE_EVENT,Event1,1 | Event ID: 1 |
| 2 | CREATE_EVENT,Event2,2 | ERROR: User with an id 2 does not exist |
Removes an event. Only the organizer who created it may delete it.
event_id (number) — event being removed.user_id (number) — must be the event's organizer.Input
DELETE_EVENT, <event_id>, <user_id>
Output
EVENT_DELETED <event_id> or
ERROR: <message>
| # | Input | Output |
|---|---|---|
| 1 | DELETE_EVENT,1,1 | EVENT_DELETED 1 |
| 2 | DELETE_EVENT,2,2 | ERROR: User with an id 2 is not a organizer of Event with an id 2 |
| 3 | DELETE_EVENT,1,1 | ERROR: Event with an id 1 does not exist |
| 4 | DELETE_EVENT,2,3 | ERROR: User with an id 3 does not exist |
Adds a new question into an existing event.
content (string) — the question text.user_id (number) — the author.event_id (number) — target event.Input
ADD_QUESTION, <content>, <user_id>, <event_id>
Output
Question ID: <question_id> or
ERROR: <message>
| # | Input | Output |
|---|---|---|
| 1 | ADD_QUESTION,Is the mid-hinge resistant to outliers?,1,1 | Question ID: 1 |
| 2 | ADD_QUESTION,...,3,1 | ERROR: User with an id 3 does not exist |
| 3 | ADD_QUESTION,...,2,2 | ERROR: Event with an id 2 does not exist |
Removes a question. Only its author may delete it.
question_id (number) — question being removed.user_id (number) — must be the question's author.Input
DELETE_QUESTION, <question_id>, <user_id>
Output
QUESTION_DELETED <question_id> or
ERROR: <message>
| # | Input | Output |
|---|---|---|
| 1 | DELETE_QUESTION,1,1 | QUESTION_DELETED 1 |
| 2 | DELETE_QUESTION,2,1 | ERROR: User with an id 1 is not an author of question with an id 2 |
| 3 | DELETE_QUESTION,1,1 | ERROR: Question with an id 1 does not exist |
| 4 | DELETE_QUESTION,2,3 | ERROR: User with an id 3 does not exist |
Lets a user upvote a question — but only once.
question_id (number) — question being upvoted.user_id (number) — the upvoter.Input
UPVOTE_QUESTION, <question_id>, <user_id>
Output
QUESTION_UPVOTED <question_id> or
ERROR: <message>
| # | Input | Output |
|---|---|---|
| 1 | UPVOTE_QUESTION,1,1 | QUESTION_UPVOTED 1 |
| 2 | UPVOTE_QUESTION,1,1 | ERROR: User with an id 1 has already upvoted a question with an id 1 |
| 3 | UPVOTE_QUESTION,2,1 | ERROR: Question with an id 2 does not exist |
| 4 | UPVOTE_QUESTION,1,2 | ERROR: User with an id 2 does not exist |
Set<User> makes
the double-vote check a simple contains — and vote count is just
upvoters.size().Lets a user reply to a specific question.
reply_content (string) — the reply text.question_id (number) — question being replied to.user_id (number) — the replier.Input
REPLY_QUESTION, <reply_content>, <question_id>, <user_id>
Output
REPLY_ADDED or ERROR: <message>
| # | Input | Output |
|---|---|---|
| 1 | REPLY_QUESTION,Yes. It is resistant to outliers,1,1 | REPLY_ADDED |
| 2 | REPLY_QUESTION,...,2,1 | ERROR: Question with an id 2 does not exist |
| 3 | REPLY_QUESTION,...,1,3 | ERROR: User with an id 3 does not exist |
question_id then user_id, and the content comes first at
index 1. Parse tokens.get(2) as question, tokens.get(3) as user.
Displays all questions in an event in a specified order.
event_id (number) — the event.sort_by (string) — POPULAR (by vote count) or
RECENT (recently added first).Input
LIST_QUESTIONS, <event_id>, <sort_by>
Output (per question)
Question ID: <id> / Content: <content> / Votes: <n> / Replies: / - User <uid>: <reply>
| # | Input | Output |
|---|---|---|
| 1 | LIST_QUESTIONS,1,POPULAR | Question ID: 1 Content: Is the mid-hinge resistant to outliers? Votes: 3 Replies: - User 1: Yes. It is resistant to outliers … (next question) |
| 2 | LIST_QUESTIONS,2,POPULAR | ERROR: Event with an id 2 does not exist |
Question ID, Content, Votes, Replies:),
then one - User <id>: <content> line per reply.
POPULAR sorts by upvoters.size() descending; RECENT
by insertion order, newest first.Your starter App.run() parses each line and routes on a
switch. Fill each case first to pass the tests; the Command Pattern below is the clean
refactor once they're green.
public void run(List<String> commands) { for (String line : commands) { if (line == null) break; List<String> tokens = Arrays.asList(line.split(",")); try { switch (tokens.get(0)) { case "CREATE_USER": { User u = userService.createUser(tokens.get(1), tokens.get(2)); System.out.println("User ID: " + u.getId()); break; } case "CREATE_EVENT": { Event e = eventService.createEvent(tokens.get(1), Long.parseLong(tokens.get(2))); System.out.println("Event ID: " + e.getId()); break; } case "DELETE_EVENT": { Long id = eventService.deleteEvent(Long.parseLong(tokens.get(1)), Long.parseLong(tokens.get(2))); System.out.println("EVENT_DELETED " + id); break; } case "ADD_QUESTION": { Question q = questionService.addQuestion(tokens.get(1), Long.parseLong(tokens.get(2)), Long.parseLong(tokens.get(3))); System.out.println("Question ID: " + q.getId()); break; } case "DELETE_QUESTION": { Long id = questionService.deleteQuestion(Long.parseLong(tokens.get(1)), Long.parseLong(tokens.get(2))); System.out.println("QUESTION_DELETED " + id); break; } case "UPVOTE_QUESTION": { Long id = questionService.upvoteQuestion(Long.parseLong(tokens.get(1)), Long.parseLong(tokens.get(2))); System.out.println("QUESTION_UPVOTED " + id); break; } case "REPLY_QUESTION": { // NOTE: content, THEN questionId (2), THEN userId (3) questionService.replyQuestion(tokens.get(1), Long.parseLong(tokens.get(2)), Long.parseLong(tokens.get(3))); System.out.println("REPLY_ADDED"); break; } case "LIST_QUESTIONS": { questionService.listQuestions(Long.parseLong(tokens.get(1)), tokens.get(2)); break; // service prints the formatted block itself } default: throw new RuntimeException("INVALID_COMMAND"); } } catch (Exception e) { System.out.println("ERROR: " + e.getMessage()); } } }
try/catch in run() prints
ERROR: <message>. So you never write the error branch inside a case — you
throw it from the service.REPLY_QUESTION is content(1),
questionId(2), userId(3) — not the usual order. And LIST_QUESTIONS returns
nothing to print here; the service formats and prints the multi-line block itself (or throws for a
missing event).public interface ICommand { void invoke(List<String> tokens); }
case body becomes a command class implementing this interface — identical
parsing, just relocated. The shared input stays List<String> tokens.public class CreateEventCommand implements ICommand { private final EventService eventService; public CreateEventCommand(EventService eventService) { this.eventService = eventService; } @Override public void invoke(List<String> tokens) { String title = tokens.get(1); Long organizerId = Long.parseLong(tokens.get(2)); try { Event event = eventService.createEvent(title, organizerId); System.out.println("Event ID: " + event.getId()); } catch (Exception e) { System.out.println("ERROR: " + e.getMessage()); } } }
ERROR: <message>.public class CommandInvoker { private final Map<String, ICommand> commands = new HashMap<>(); public void register(String name, ICommand command) { commands.put(name, command); } private List<String> parse(String input) { return Arrays.asList(input.split(",")); } public void invoke(String input) { List<String> tokens = parse(input); ICommand command = commands.get(tokens.get(0)); if (command == null) { System.out.println("ERROR: INVALID_COMMAND"); return; } command.invoke(tokens); } }
CREATE_USER,
CREATE_EVENT, …), then feeds each input line to invoke. Adding
UPVOTE_QUESTION is a new class + one register line.public Event createEvent(String title, Long organizerId) { User organizer = userRepository.findById(organizerId) .orElseThrow(() -> new RuntimeException( "User with an id " + organizerId + " does not exist")); Event event = new Event(title, organizer); return eventRepository.save(event); } public Long deleteEvent(Long eventId, Long userId) { User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException( "User with an id " + userId + " does not exist")); Event event = eventRepository.findById(eventId) .orElseThrow(() -> new RuntimeException( "Event with an id " + eventId + " does not exist")); if (!event.getOrganizer().getId().equals(userId)) { throw new RuntimeException("User with an id " + userId + " is not a organizer of Event with an id " + eventId); } eventRepository.deleteById(eventId); return eventId; }
"is not a organizer of" (note: "a organizer", not "an") must match the spec
character-for-character, or the line fails.Each operation has its own test folder. Your code reads an input file and writes an output file, which is diffed against the expected output.
test/resources/<OP>/input.txt.test/resources/<OP>/output.txt.test/resources/<OP>/expectedOutput.txt for text-match
similarity.ERROR: prefix all matter as much as the
logic. When a test fails, open the generated output.txt next to
expectedOutput.txt and find the first differing line.