Buildout // Geektrust // Design + Implement

Xlido

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.

Entities: User · Event · Question · Reply Architecture: SEBER + Command Runtime: Command-line, comma-delimited Verification: file diff vs expectedOutput
01

What Xlido Does

Slido lets audiences ask and vote on questions during live meetings and events. Xlido is the CLI version, supporting seven core features.

  • Create users — register a person by unique email + password.
  • Create / delete events — an organizer opens a Q&A event; only the organizer can delete it.
  • Create / delete questions — a user adds a question to an event; only its author can delete it.
  • Upvote questions — a user can upvote a question once (no double-voting).
  • Reply to questions — a user comments on a question.
  • List questions — display an event's questions sorted by POPULAR (votes) or RECENT.
02

Class Model

Nouns in the requirements become entities. Ownership rules (organizer, author) drive the relationships and most error cases.

User

- id: Long
- email: String // unique
- password: String

Event

- id: Long
- name: String
- organizer: User
- questions: List<Question>

Question

- id: Long
- content: String
- author: User
- event: Event
- upvoters: Set<User>
- replies: List<Reply>

Reply

- id: Long
- content: String
- author: User
- question: Question

Relationships & rules

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.
03

SEBER — The Layers

The same layered architecture as any well-designed CLI app: each request flows down through named layers, each with one job.

C
Client

The CLI line, parsed and dispatched to a command.

S
Service

UserService, EventService, QuestionService orchestrate each use case.

B
Business Logic

Ownership checks, uniqueness, double-vote guards, sorting.

E
Entities

User, Event, Question, Reply.

R
Repositories

In-memory stores behind interfaces, one per entity.

Every error message in the spec comes from a business-logic check in the service layer: "does the user exist?", "is this user the organizer?", "has this user already upvoted?". Get those guards right and the output contracts fall into place.
04

The Eight Operations

Each operation's input format, parameters, output/error contract, and sample I/O. Output strings must match exactly — verification is a text diff.

CREATE_USERCreate

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
CREATE_EVENTCreate

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
DELETE_EVENTDelete

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
Check order matters: validate the user exists and the event exists before the organizer check — row 4 expects the user-not-found error even though the event also wouldn't match.
ADD_QUESTIONCreate

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
DELETE_QUESTIONDelete

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
UPVOTE_QUESTIONAction

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
Why a Set: storing upvoters as a Set<User> makes the double-vote check a simple contains — and vote count is just upvoters.size().
REPLY_QUESTIONAction

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
Watch the argument order: unlike the others, the ID args are question_id then user_id, and the content comes first at index 1. Parse tokens.get(2) as question, tokens.get(3) as user.
LIST_QUESTIONSRead

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
Format precisely: each question prints four labeled lines (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.
05

Dispatch — Switch Now, Commands Later

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.

App.java — your starter switch, cases filled in
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());
        }
    }
}
The pattern for every case: parse tokens → call the service → print only the success line. The service throws with the exact spec message on any failure, and the surrounding try/catch in run() prints ERROR: <message>. So you never write the error branch inside a case — you throw it from the service.
Two parsing traps: 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).
ICommand.java — the refactor target (optional, after tests pass)
public interface ICommand {
    void invoke(List<String> tokens);
}
The learning goal is a modular, scalable design. Once the switch works, each case body becomes a command class implementing this interface — identical parsing, just relocated. The shared input stays List<String> tokens.
CreateEventCommand.java
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());
        }
    }
}
The command parses tokens, calls its service, and prints the success line. The service throws with the exact spec message on failure; the command turns that into ERROR: <message>.
CommandInvoker.java
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);
    }
}
App registers one command per operation name (CREATE_USER, CREATE_EVENT, …), then feeds each input line to invoke. Adding UPVOTE_QUESTION is a new class + one register line.
EventService.java — where the error contracts live
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;
}
Copy the messages verbatim. Verification is a text diff — "is not a organizer of" (note: "a organizer", not "an") must match the spec character-for-character, or the line fails.
06

How You're Graded

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.

  • Your implementation runs against test/resources/<OP>/input.txt.
  • It writes results to test/resources/<OP>/output.txt.
  • That is compared to test/resources/<OP>/expectedOutput.txt for text-match similarity.
  • Identical files = correct. Any differing line (like the dummy "line 5" mismatch) = fail.
  • Run each operation's test via the beaker icon; a green tick means the diff matched.
Because grading is a raw text diff, output formatting is part of the spec. Exact labels, spacing, capitalization, and the literal 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.