Compile Ready
All low level design problems
Low Level Design/Expert/Expert Systems

Design Slack (LLD)

Workspaces, channels, threads, mentions, and notifications — team-chat modelled with clean boundaries.

Expert 65m interview 16m read Medium frequency Popularity 81
Observer Mediator Composite Strategy Atlassian Amazon Meta

Problem Statement

Design the object model for Slack from an LLD and OOD viewpoint. A workspace owns users, channels, and messages; channels may be public, private, or direct messages; users can post root messages, reply in threads, mention teammates, and receive notifications based on their preferences.

The focus is the in-memory domain model and interaction boundaries: membership rules, message routing, threaded conversations, and notification fan-out. Storage, search indexing, file uploads, reactions, and external integrations are production extensions rather than the core interview surface.

Business context

Slack is a strong expert-level LLD problem because it combines collaboration, access control, nested content, and event delivery in one familiar product. Interviewers use it to see whether a candidate can separate a Workspace aggregate from Channel membership, keep message routing inside a mediator, and model notifications without scattering conditionals through the posting flow.

A good answer emphasizes that public channels, private channels, and direct messages share most behavior but differ in discovery and membership rules. Threads should be represented as a message tree, and notification preferences should be pluggable so product rules evolve without changing channel posting code.

Functional Requirements

  • Support multiple workspaces, each with its own users and channels.

  • Support public channels that any workspace member may join.

  • Support private channels that require invitation before membership.

  • Support direct messages between exactly two users.

  • Allow a channel member to post a root message with zero or more mentioned users.

  • Allow a channel member to reply to any message in the same channel thread.

  • Route messages only to users who are members of the target channel.

  • Notify members according to preferences such as all messages, mentions plus DMs, or mute.

Non-Functional Requirements

Membership correctness

A user who is not in a channel must not post, read through the channel object, or receive channel notifications. The invariant belongs to Channel, not to the caller.

Extensibility

New channel types, notification preferences, and delivery targets should be additive. The posting flow should not grow a switch for every product feature.

Low latency fan-out

Posting should do bounded in-memory work proportional to channel members and mentions. Expensive delivery, persistence, and indexing can later move behind queues.

Thread integrity

Replies must attach only to messages that already belong to the same channel. A thread should be traversable without a separate thread service in the base design.

Clear ownership

Workspace owns users and channels; Channel owns membership and root messages; Message owns its replies. This keeps lifecycle decisions local.

Requirement Clarification

QDo we need to model organizations with multiple workspaces?

No. Treat Workspace as the top-level aggregate. Enterprise grid, shared channels, and cross-workspace identity are production extensions.

QCan anyone join a public channel?

Any user already in the workspace can join a public channel. Private channels and direct messages are restricted by their channel type.

QAre threads separate entities or part of messages?

For the base design, a Message can contain replies, making a thread a message tree. This cleanly demonstrates the Composite pattern.

QShould notifications be sent to the author?

No. The author sees their own message immediately, so the notification service skips the author and evaluates preferences for the other members.

QDo mentions override mute?

Not in the base policy. A muted strategy returns false for every message. A product-specific urgent mention strategy can be added later.

UML Class Diagram

Rendering diagram…
Workspace is the aggregate root, Channel is the mediator for membership and posting, Message is a composite tree for threads, and NotificationPreference is the strategy seam used by the observer service.

Sequence Diagram

Rendering diagram…
Posting is mediated twice: Workspace resolves ids and owns the catalog, while Channel enforces membership, creates messages, attaches replies, and triggers observers.

Entity Identification

Workspace

Top-level aggregate for one Slack workspace. Owns registered users, channels, id generation for messages, and convenience methods that resolve ids before delegating to channels.

idnameuserschannelsnotificationServicenextMessageNumber

Channel

Abstract mediator for channel conversations. Owns members and root messages, validates posting access, attaches replies only inside the same channel, and notifies observers after a message is accepted.

idnamemembersmessagesnotificationService

PublicChannel

Concrete channel that is discoverable and joinable by any workspace user. It changes discovery and membership policy without duplicating posting behavior.

allowsDiscoveryisDirectMessage

PrivateChannel

Concrete channel that requires invitation before membership. It keeps the invited set local to the private channel policy.

invitedinvite(user)

DirectMessage

Specialized private channel between exactly two users. It reuses private-channel posting and notification behavior while enforcing the two-member invariant.

firstsecondisDirectMessage

Message

Immutable message payload plus mutable child replies. A root message and all nested replies form one thread tree using the Composite pattern.

idauthortextcreatedAtparentrepliesmentions

User

Workspace participant with channel memberships and per-channel notification preferences. Receives notifications from the observer service.

iddisplayNamechannelspreferences

NotificationService

Observer hub. Maintains subscribed users, filters them by channel membership and author exclusion, then delegates the final decision to each user's notification strategy.

observersregisternotify

NotificationPreference

Strategy interface for notification rules. All messages, mentions plus DMs, and muted preferences implement the same method.

shouldNotify(recipient, channel, message)

Design Patterns Used

Mediator

Workspace and Channel coordinate interactions so users do not send messages directly to one another. Workspace resolves ids and channel type, while Channel enforces membership and routes accepted messages to observers.

Observer

NotificationService keeps observers and is notified after channel state changes. Users receive notifications without Channel knowing delivery devices, push gateways, or email adapters.

Strategy

NotificationPreference lets each user choose all messages, mentions plus DMs, or mute. Notification rules change by swapping strategies, not editing the posting flow.

Composite

Message owns child replies, so a thread is a tree of messages. Root messages and replies can be traversed with the same recursive behavior.

Step-by-Step Design

  1. 1Start with the workspace boundary

    Make Workspace the aggregate root for users and channels. It should resolve ids, create channel types, and then delegate posting to the selected Channel rather than owning channel-specific rules.

  2. 2Use channels as the message mediator

    All posting flows enter Channel.post or Channel.reply. The channel checks membership before creating a message, which prevents callers from bypassing access control.

    public Message post(String messageId, User author, String text, Set<User> mentions) {
        ensureCanPost(author);
        Message message = new Message(messageId, author, text, mentions, null);
        messages.add(message);
        notificationService.notify(this, message);
        return message;
    }
  3. 3Separate public, private, and DM membership policy

    Put shared behavior in abstract Channel and keep only the policy differences in PublicChannel, PrivateChannel, and DirectMessage. This avoids duplicate posting and notification code.

    class PrivateChannel extends Channel {
        private final Set<User> invited = new HashSet<>();
    
        public void invite(User user) {
            invited.add(user);
            super.addMember(user);
        }
    
        protected boolean canJoin(User user) {
            return invited.contains(user);
        }
    }
  4. 4Model threads as a message tree

    A reply is another Message attached under a parent message. This keeps the thread close to the data it organizes and lets rendering traverse recursively.

    public Message reply(String replyId, User replier, String text, Set<User> mentions) {
        Message reply = new Message(replyId, replier, text, mentions, this);
        replies.add(reply);
        return reply;
    }
  5. 5Notify through observers and strategies

    After a channel accepts a message, it calls NotificationService. The service filters candidates to channel members, skips the author, and then asks each recipient's NotificationPreference whether to deliver.

    if (channel.hasMember(recipient)
            && !recipient.equals(message.author())
            && recipient.preferenceFor(channel).shouldNotify(recipient, channel, message)) {
        recipient.receiveNotification(notification);
    }
  6. 6Keep production concerns behind seams

    Persistence, push providers, unread counters, search indexing, and audit logs should subscribe to the same accepted-message event later. The base model stays focused on correctness and extensibility.

Complete Java Implementation

Loading…

Explanation of Every Class

User

Represents a workspace member. It stores membership references and per-channel NotificationPreference values, and exposes receiveNotification as the observer callback.

Message

Represents both root posts and replies. Each message owns a list of child replies, so a thread can be rendered recursively using the Composite pattern.

NotificationService

Observer coordinator. It registers workspace users, skips authors, filters by channel membership, and delegates the final send decision to the recipient's strategy.

NotificationPreference

Strategy interface implemented by AllMessagesPreference, MentionAndDirectMessagePreference, and MutedPreference. It keeps product notification rules out of Channel.

Channel

Abstract mediator for conversations. It centralizes membership checks, root-message creation, reply attachment, and notification triggering for every channel type.

PublicChannel

Concrete channel with open membership and discoverability. It inherits all routing behavior from Channel and only supplies policy differences.

PrivateChannel

Concrete channel with invite-only membership. Its invited set is the single source of truth for who may join.

DirectMessage

Two-person private channel. It reuses private-channel behavior but prevents extra users from being invited and marks itself as a direct message for notification strategy decisions.

Workspace

Aggregate root and outer mediator. It owns users and channels, creates concrete channel objects, resolves mentions by user id, and delegates posting to Channel.

Main

Small executable scenario that creates users, joins a public channel, posts a mentioned message, replies in a thread, opens a direct message, and prints the thread tree.

Dry Run

Sample input

Workspace with Alice, Bob, and Chen. Channel general has all three members. Bob uses all-message notifications, Chen uses mentions plus DMs. Alice posts in general mentioning Bob, Bob replies mentioning Alice, and Chen sends Alice a DM.

StepActionMediator decisionObservers consideredOutcome
1Alice posts in general mentioning BobWorkspace resolves ids, Channel accepts Alice as memberBob and Chen, author Alice skippedBob notified; Chen not notified because no mention
2Bob replies in the thread mentioning AliceChannel verifies parent belongs to generalAlice and Chen, author Bob skippedAlice notified by mention; Chen not notified
3Chen opens DM with AliceWorkspace creates DirectMessage with two membersNo message yetPrivate two-user channel exists
4Chen sends DM to AliceDirectMessage accepts Chen as memberAlice only, author Chen skippedAlice notified because DMs pass default strategy
5Render launch threadMessage tree is traversed recursivelyNo notification fan-outRoot message prints with Bob's nested reply

The dry run separates routing from delivery. Workspace and Channel decide whether a message is valid; NotificationService and each user's strategy decide whether a valid message becomes a notification.

Complexity Analysis

OperationTimeSpaceNote
create channelO(1)O(1)Insert one channel object into the workspace map; initial owner membership for private channels is constant work.
join public channelO(1)O(1)Hash-set insertion into the channel members set and user's channel set.
post root messageO(M + U)O(K)M observers are scanned, U mentioned user ids are resolved, and K mentioned users are stored on the message.
reply to messageO(T + M + U)O(K)T messages in the channel thread tree may be searched to validate the parent before the same notification fan-out runs.
render a threadO(T)O(H)Traverse T messages in the thread; recursion depth is the thread height H.

The hot path is notification fan-out. The base design scans registered observers and filters by channel membership; production systems usually maintain per-channel subscriber lists and enqueue delivery jobs so posting latency is not tied to push-provider latency.

Extensibility

New notification rule

Implement NotificationPreference for keywords, urgent mentions, quiet hours, or mobile-only preferences. Channel.post and NotificationService.notify stay unchanged.

New channel type

Subclass Channel or PrivateChannel for shared channels, announcement-only channels, or archived channels. Override membership/discovery rules while reusing posting and threading.

Async delivery

Replace direct receiveNotification calls with a queue-backed delivery adapter. The observer decision remains the same, but delivery becomes retriable and non-blocking.

Unread counters and read receipts

Add a per-user channel state object that listens to accepted messages and read events. This avoids polluting Message with viewer-specific state.

Search and retention

Publish accepted-message events to an indexer and retention policy service. The domain model does not need to know how messages are searched or expired.

Alternative Designs

Separate Thread class

Create an explicit Thread aggregate with a root message and replies instead of storing replies directly on Message.

Tradeoffs

This can help with thread-level metadata such as subscribers and resolved status, but it adds another lifecycle object and makes simple recursive rendering less direct.

Notification rules on Channel

Let each channel decide whether each member should be notified instead of using NotificationPreference strategies on users.

Tradeoffs

Channel-centric policy is simple for global channel settings, but it handles personal preferences poorly and tends to accumulate conditional logic.

Event bus instead of direct observer service

Publish a MessagePosted event to an application event bus, with notification, search, audit, and unread-counter consumers subscribing independently.

Tradeoffs

Better production decoupling and reliability, but too much infrastructure for the core interview model unless the interviewer asks about scale.

Common Mistakes

  • ×

    Letting User send messages directly to other users, which bypasses channel membership and loses the Mediator boundary.

  • ×

    Treating public, private, and DM channels as unrelated classes with duplicated post and reply logic.

  • ×

    Storing thread replies in a global list without validating that the parent belongs to the channel.

  • ×

    Hard-coding notification behavior in Channel.post instead of delegating to strategies.

  • ×

    Not skipping the author during notification fan-out.

  • ×

    Allowing direct messages to grow beyond two members, which turns them into private channels with a misleading type.

  • ×

    Mixing persistence, push gateway calls, and domain validation in one class during the interview design.

Follow-up Interview Questions

QHow would you scale notification fan-out for a channel with thousands of members?

Keep the domain decision synchronous but enqueue delivery jobs after the message is accepted. Maintain a per-channel subscriber index so the notification service scans channel members instead of all workspace users.

QHow would you add unread counts?

Introduce UserChannelState with last-read message id or timestamp per user and channel. It listens to accepted messages and read events rather than changing Message itself.

QHow do you support channel admins and posting restrictions?

Add a role map on Channel and move ensureCanPost to a policy method. Announcement-only channels can override the policy or inject a posting strategy.

QHow would mentions work for user groups such as engineering?

Resolve mention tokens through a MentionResolver before calling Channel.post. It can expand a group to users while keeping Message as a set of mentioned recipients.

QWhere do external integrations and bots fit?

Model bots as users or apps with scoped permissions. They post through the same channel mediator so membership, threading, and notifications stay consistent.

Production Considerations

Durable storage

Persist workspaces, channels, memberships, messages, and user preferences in separate repositories. Channel posting becomes a transaction that writes the message and emits an event.

Authorization and audit

Enforce workspace membership, channel role, retention, and compliance policies at the service boundary. Audit membership changes, private-channel invites, and deletion events.

Delivery reliability

Use a queue for push, email, and desktop notifications with retries, idempotency keys, and dead-letter handling. Do not block message posting on a mobile push provider.

Eventual consistency

Search indexing, unread counters, and analytics can lag accepted messages by a few seconds. The primary invariant is that channel storage and membership checks remain strongly consistent.

Privacy and retention

Private channels and DMs need strict access checks, retention policies, export controls, and deletion workflows. Those policies should live at service/repository boundaries, not inside UI code.

What Interviewers Look For

  • Did you identify Workspace and Channel as mediators instead of making users talk directly?

  • Did you keep membership checks inside the channel boundary for public, private, and DM types?

  • Did you model threads explicitly rather than flattening all replies into one message list?

  • Did you separate notification preferences with a Strategy interface?

  • Did you use Observer for notifications without coupling channel code to device delivery?

  • Can you describe how the simple model evolves to queues, repositories, and search without rewriting the domain?

Quiz

0/5 answered

  1. 1.Why should **Channel** mediate posting instead of allowing **User** to call another user directly?

  2. 2.Which design choice demonstrates the Composite pattern?

  3. 3.What is the main benefit of **NotificationPreference** as a Strategy?

  4. 4.Why does **DirectMessage** extend private-channel behavior but restrict invites?

  5. 5.When Bob replies to Alice's root message, what should the channel validate first?

Practice Variants

Add unread counters

Intermediate

Create UserChannelState and update unread counts when messages are accepted. Keep the message tree independent of per-user read state.

Add announcement-only channels

Advanced

Introduce channel roles and a posting policy so only admins can post while every member can read. Decide whether this belongs in a Strategy or a subclass.

Add async notification delivery

Advanced

Replace direct observer callbacks with a queue-backed delivery adapter that supports retries and per-device channels.

Flashcards

Cheat Sheet

Entities: Workspace owns Users and Channels; Channel owns members and root Messages; Message owns replies; NotificationService owns observers; NotificationPreference owns delivery rules.

Channel types: PublicChannel is discoverable and open to workspace users. PrivateChannel requires invite. DirectMessage reuses private behavior but allows exactly two users.

Patterns: Mediator for Workspace/Channel routing, Observer for notifications, Strategy for preferences, Composite for message threads.

Post flow: resolve ids in Workspace → validate membership in Channel → create Message → store as root or reply → notify observers according to preferences.

Invariants: non-members cannot post; replies must target a message in the same channel; the author is not notified; DMs have two participants; muted users receive no notifications.

Scale path: replace direct callbacks with queues, add repositories for persistence, add subscriber indexes for fan-out, and publish accepted-message events to search and unread-counter consumers.

References