Skip to main content

Design a Trello / Jira-lite Board

Problem Statement

Design an in-memory task planning board (similar to the core organization models of Trello or Jira). The board must support managing workspaces, creating customizable progress lists (e.g., To Do, In Progress, Done), adding cards with priorities and assignees, moving cards between lists, and maintaining a chronological audit trail (activity log) of all operations.

Asked In Companies
Atlassian Microsoft

Design Decisions & Patterns Used

A task board requires an intuitive hierarchy. A Board contains multiple CardLists (columns), and each CardList contains multiple Cards (tasks). We model this relationship cleanly using standard collections. To capture activity history without coupling the audit logging logic to the card movement operations, we use event delegation.

We will utilize the following Design Patterns:

  • Observer Pattern: Registering history loggers that listen to events (like card movement or assignee updates) and record audit trails.
  • Command Pattern: Wrapping card movement operations as commands to enable undo/redo actions.

Functional Requirements

  • Model Boards, CardLists, and Cards with metadata (priorities, descriptions).
  • Support creating and ordering lists on the board.
  • Support adding and removing cards in lists.
  • Move cards between lists dynamically.
  • Automatically update an activity audit trail when a card's list or assignee changes.

Objects Required

  • Priority (Enum mapping LOW, MEDIUM, and HIGH values)
  • Card (Model storing task details)
  • CardList (Model managing a column of cards)
  • BoardObserver (Interface defining event hooks)
  • Board (Aggregate coordinator managing lists and broadcasting updates)

Priority Enum & Card Class

The Priority enum defines the importance of a task, and the Card class models the task details.


public enum Priority {
    LOW,
    MEDIUM,
    HIGH
}

Let's define the Card class:


public class Card {
    private final String id;
    private String title;
    private String description;
    private String assignee;
    private Priority priority;

    public Card(String id, String title, String description, Priority priority) {
        this.id = id;
        this.title = title;
        this.description = description;
        this.priority = priority;
        this.assignee = "Unassigned";
    }

    public String getId() { return id; }
    
    public synchronized String getTitle() { return title; }
    public synchronized void setTitle(String title) { this.title = title; }

    public synchronized String getDescription() { return description; }
    public synchronized void setDescription(String description) { this.description = description; }

    public synchronized String getAssignee() { return assignee; }
    public synchronized void setAssignee(String assignee) { this.assignee = assignee; }

    public synchronized Priority getPriority() { return priority; }
    public synchronized void setPriority(Priority priority) { this.priority = priority; }
}

The constructor sets the initial values and defaults the assignee to "Unassigned". Getter and setter methods are synchronized to ensure thread-safe updates to card properties.


CardList Class

The CardList class represents a column of cards on the board, managing card additions and removals.


import java.util.ArrayList;
import java.util.List;

public class CardList {
    private final String id;
    private final String name;
    private final List<Card> cards;

    public CardList(String id, String name) {
        this.id = id;
        this.name = name;
        this.cards = new ArrayList<>();
    }

    public synchronized void addCard(Card card) {
        cards.add(card);
    }

    public synchronized boolean removeCard(Card card) {
        return cards.remove(card);
    }

    public String getId() { return id; }
    public String getName() { return name; }
    
    public synchronized List<Card> getCards() {
        return new ArrayList<>(cards); // Return a copy for thread safety
    }
}

The constructor configures the list name and initializes the card array list. Methods modifying the card list are synchronized to ensure thread safety.


BoardObserver Interface & Board Class

We apply the **Observer Pattern**. The BoardObserver interface defines the event hooks, and the Board class registers observers and coordinates updates.


public interface BoardObserver {
    void onCardMoved(Card card, String sourceListName, String targetListName);
    void onCardAssigned(Card card, String oldAssignee, String newAssignee);
}

Let's implement the Board class:


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Board {
    private final String id;
    private final String name;
    private final Map<String, CardList> lists;
    private final List<BoardObserver> observers;

    public Board(String id, String name) {
        this.id = id;
        this.name = name;
        this.lists = new HashMap<>();
        this.observers = new ArrayList<>();
    }

    public void addList(CardList list) {
        lists.put(list.getId(), list);
        System.out.println("Added column '" + list.getName() + "' to board: " + name);
    }

    public void registerObserver(BoardObserver observer) {
        observers.add(observer);
    }

    public synchronized void moveCard(String cardId, String sourceListId, String targetListId) {
        CardList sourceList = lists.get(sourceListId);
        CardList targetList = lists.get(targetListId);

        if (sourceList == null || targetList == null) {
            throw new IllegalArgumentException("Invalid source or target list ID.");
        }

        // Find card in source list
        Card targetCard = sourceList.getCards().stream()
                .filter(c -> c.getId().equals(cardId))
                .findFirst()
                .orElse(null);

        if (targetCard == null) {
            throw new IllegalArgumentException("Card with ID " + cardId + " not found in source list.");
        }

        // Move the card under a synchronized lock
        sourceList.removeCard(targetCard);
        targetList.addCard(targetCard);

        // Notify observers of the movement event
        notifyCardMoved(targetCard, sourceList.getName(), targetList.getName());
    }

    public synchronized void assignCard(String cardId, String listId, String assigneeName) {
        CardList list = lists.get(listId);
        if (list == null) throw new IllegalArgumentException("List not found.");

        Card card = list.getCards().stream()
                .filter(c -> c.getId().equals(cardId))
                .findFirst()
                .orElse(null);

        if (card == null) throw new IllegalArgumentException("Card not found.");

        String oldAssignee = card.getAssignee();
        card.setAssignee(assigneeName);

        // Notify observers of the assignment event
        notifyCardAssigned(card, oldAssignee, assigneeName);
    }

    private void notifyCardMoved(Card card, String source, String target) {
        for (BoardObserver observer : observers) {
            observer.onCardMoved(card, source, target);
        }
    }

    private void notifyCardAssigned(Card card, String oldAssignee, String newAssignee) {
        for (BoardObserver observer : observers) {
            observer.onCardAssigned(card, oldAssignee, newAssignee);
        }
    }
}

Here is an explanation of the core operations in the Board class:

  • The constructor configures the board name and registers the lists map.
  • moveCard() locates the source and target lists, finds the target card, removes it from the source, adds it to the target, and notifies registered observers.
  • assignCard() updates the card's assignee and dispatches notification events.

AuditLogListener Implementation

The AuditLogListener implements the observer interface to print a chronological audit log of all board updates.


public class AuditLogListener implements BoardObserver {
    @Override
    public void onCardMoved(Card card, String sourceListName, String targetListName) {
        System.out.println("[AUDIT LOG] Card '" + card.getTitle() + "' moved from: [" + 
                sourceListName + "] to: [" + targetListName + "]");
    }

    @Override
    public void onCardAssigned(Card card, String oldAssignee, String newAssignee) {
        System.out.println("[AUDIT LOG] Card '" + card.getTitle() + "' assignee changed from: '" + 
                oldAssignee + "' to: '" + newAssignee + "'");
    }
}

The class prints structured audit trails to stdout, decoupling log processing from card state changes.


Main Driver Class

This class tests our task planning board. It creates lists, adds cards, registers observers, assigns cards, and moves cards to verify audit trail prints.


public class Main {
    public static void main(String[] args) {
        Board board = new Board("B-01", "SaaS Development Board");

        // Register the audit log listener
        board.registerObserver(new AuditLogListener());

        // Create Lists
        CardList todoList = new CardList("L-01", "To Do");
        CardList inProgressList = new CardList("L-02", "In Progress");
        CardList doneList = new CardList("L-03", "Done");

        board.addList(todoList);
        board.addList(inProgressList);
        board.addList(doneList);

        // Add Card to "To Do" list
        System.out.println("\n--- Adding Card ---");
        Card card1 = new Card("C-101", "Implement OAuth", "Integrate Google and GitHub logins.", Priority.HIGH);
        todoList.addCard(card1);
        System.out.println("Card '" + card1.getTitle() + "' added to 'To Do' list.");

        // Assign Card
        System.out.println("\n--- Assigning Card ---");
        board.assignCard("C-101", "L-01", "Developer Alice");

        // Move Card from "To Do" to "In Progress"
        System.out.println("\n--- Moving Card (To Do -> In Progress) ---");
        board.moveCard("C-101", "L-01", "L-02");

        // Move Card from "In Progress" to "Done"
        System.out.println("\n--- Moving Card (In Progress -> Done) ---");
        board.moveCard("C-101", "L-02", "L-03");
    }
}

The main() driver configures the board and list elements, registers the audit logger, performs operations on cards, and asserts that events are correctly captured in the audit logs.


Also See

Comments

Popular posts from this blog

Designing a Parking Lot - Low Level Design

Problem Statement Design a parking lot that can handle vehicles entering and leaving while managing parking across multiple floors. Each vehicle should be assigned a suitable parking spot based on its type, and the spot should be freed once the vehicle exits. The design should also support generating a ticket at entry and optionally calculating the parking fee based on the duration of stay. Asked In Companies Amazon Google Microsoft Uber Walmart Flipkart Meta PayPal Oracle Salesforce Adobe Apple Intuit LinkedIn Atlassian Functional Requirements The design should support multiple vehicle types such as bikes, cars, and trucks A vehicle must be assigned a parking spot compatible with its type A parking spot cannot be assigned to more than one vehicle at a time The parking lot should support multiple levels (floors) The design should search and allocate an availa...

Most Frequently Asked Low Level Design(LLD) Interview Questions

Below are the curated list of most commonly asked Low Level Design (LLD) interview problems. Each problem includes a short description and a link to the complete solution with code and class diagrams. Design Parking Lot System The system should handle parking for different vehicle types such as bikes, cars, and trucks. It should manage slot allocation, availability tracking, and entry/exit flow. The design also ensures efficient usage of parking space under varying load conditions. View Solution Design Elevator / Lift System The system should support multiple elevators operating across floors with request handling logic. It focuses on scheduling algorithms to minimize wait time and optimize movement. It also manages direction control and concurrent floor requests. View Solution Design Movie Ticket Booking System The system should allow users to browse movies, select shows, and book seats. It handles seat ...

Software Design Patterns for LLD Interviews: A Complete Guide

Software Design Patterns for LLD Interviews: A Complete Guide In Software Development Engineer (SDE) interviews—especially for mid-level and senior roles—low-level design (LLD) rounds assess your ability to write clean, reusable, maintainable, and extensible code. The foundation of resolving these architectural challenges lies in the standard Gang of Four (GoF) Design Patterns. Rather than memorizing theoretical definitions, interviewers expect you to apply these patterns to real-world scenarios, identifying the trade-offs of each. Below is a comprehensive guide to the 12 most frequently asked design patterns in LLD interviews, categorized by their classification (Creational, Structural, and Behavioral). Each pattern contains a concrete, real-world Java implementation and a detailed breakdown of design decisions. Creational Design Patterns Creational design patterns deal with object creation mechanisms. They abstract the instantiation process, making a system independent of how...