Decomposition — Example: Messaging Application

The Starting Point

Goal: Build a messaging app where users can send direct messages, create group chats, share files, and see who's online. Think Slack or Discord — but focus on the decomposition, not the scale.

This system is different from the bookstore because it's real-time, multi-user, and has state that changes constantly (who's online, who's typing, unread counts). These properties create new kinds of seams.


Step 1: Top-Down — First Level

Ask: "What does a messaging app need?"

Messaging App
├── User Management
├── Conversations (1-on-1 and groups)
├── Messages
├── Presence (online/offline/typing)
├── Notifications
└── File Sharing

Six branches. Immediately, questions arise:

  • Is "conversations" one thing, or are 1-on-1 and groups different enough to be separate branches?
  • Where does "search messages" live? Under Messages? Its own branch?
  • Is "presence" really separate from "user management"?

Decision: Keep them separate for now. If two branches share too much, that's a signal to merge them later. It's easier to merge than to split.


Step 2: Second Level — Each Branch

User Management

User Management
├── Register new user
├── Log in (with session creation)
├── Log out (with session cleanup)
├── Update profile (display name, avatar, status message)
├── Block a user
└── Unblock a user

Conversations

Conversations
├── Direct Messages
│   ├── Start a DM conversation (with one other user)
│   └── List my DM conversations (sorted by most recent message)
├── Group Chats
│   ├── Create a group (name, initial members)
│   ├── Add member to group
│   ├── Remove member from group
│   ├── Leave group
│   ├── Update group details (name, description, avatar)
│   └── List my groups (sorted by most recent message)
└── Shared
    ├── Get conversation history (paginated, newest first)
    └── Mark conversation as read

Messages

Messages
├── Send text message (to a conversation)
├── Edit a sent message
├── Delete a sent message
├── React to a message (emoji)
├── Reply to a specific message (threaded)
├── Search messages (across all conversations or within one)
└── Pin a message in a conversation

Presence

Presence
├── Update my status (online / away / do-not-disturb / offline)
├── Get a user's current status
├── Get status for a list of users (for the sidebar)
├── Typing indicators (start typing, stop typing)
└── Last seen timestamp

Notifications

Notifications
├── In-app notification (badge count, pop-up)
├── Push notification (mobile device)
├── Email notification (for offline users, after delay)
├── Notification preferences (per conversation: all, mentions only, mute)
└── Mark notification as read

File Sharing

File Sharing
├── Upload file (attached to a message in a conversation)
├── Download file
├── Generate file preview (images, PDFs)
├── Enforce file size limits
└── Track storage usage per user

Step 3: Finding Seams — Where This Gets Interesting

Seam: Real-Time vs. Stored

Some operations are real-time (typing indicators, presence) and some are stored (message history, user profiles). This creates a fundamental seam:

  • Real-time data is ephemeral — "user is typing" doesn't go in a database; it's a transient signal
  • Stored data is permanent — messages are kept until deleted

This seam affects the entire architecture. Real-time features use different patterns (publish-subscribe) than stored features (request-response).

Seam: Who Sees What

When a message is sent to a group of 50 people, all 50 need to receive it. But:

  • 10 are currently online → they see it instantly
  • 15 have push notifications → they get a phone notification
  • 25 are offline with email notifications → they get an email after 15 minutes

Same event, three different delivery paths. This is an audience seam — the delivery mechanism changes based on the recipient's state.

Seam: Sender vs. Recipient Experience

When you send a message:

  • You see it immediately in your conversation (optimistic display)
  • The system stores it
  • The system delivers it to recipients
  • Recipients see a notification

The sender's experience and the recipient's experience are different flows triggered by the same event. This is a seam.

Seam: Conversation Create vs. Message in Conversation

What happens when you message someone for the first time? Is it:

  1. Create a conversation, then send a message into it? (Two operations)
  2. "Send message to user" — and the conversation is created as a side effect?

Both are valid decompositions. Option 1 is more explicit. Option 2 is more user-friendly. This is a decomposition judgment call — the right answer depends on how you want the user experience to work.

Decision: The user action is "send message to person." Internally, this decomposes into "find or create conversation" + "add message to conversation." The decomposition has two pieces, but they're presented to the user as one action.


Step 4: Leaf Test — Checking Our Work

"Send text message" — Leaf Test

QuestionAnswer
Contract?✅ IN: sender_id, conversation_id, message_text. OUT: message_record (id, timestamp, content). ERRORS: conversation not found, sender not a member, message too long, sender is blocked by recipient.
Estimate?⚠️ Sending is small, but delivery is complex — 50 people need to receive it via 3 different channels
Dependencies?✅ Needs: conversation existence, sender membership

Verdict: "Send" is concrete, but "deliver to all recipients" is hidden inside it. Split it:

Send text message
├── Validate and store message
└── Fan out to recipients
    ├── Deliver to online recipients (real-time)
    ├── Queue push notifications (for mobile recipients)
    └── Queue email notifications (for offline recipients, delayed)

Now each sub-piece is estimable. "Fan out to recipients" was hidden complexity — it looked simple until you asked "what about the 50 people?"

"Typing indicators" — Leaf Test

QuestionAnswer
Contract?✅ IN: user_id, conversation_id, is_typing (yes/no). OUT: (broadcast event to other members). ERRORS: none meaningful — this is fire-and-forget.
Estimate?✅ Small — ephemeral event, no storage
Dependencies?✅ Needs: real-time connection to conversation members

Verdict: Concrete enough. But note the seam: this is a real-time feature. It follows a completely different pattern from message storage.

"Search messages" — Leaf Test

QuestionAnswer
Contract?⚠️ IN: query, scope (all conversations or specific one). OUT: list of matching messages with context. But what about: fuzzy matching? Matching within files? Searching by date? Searching by sender?
Estimate?❌ "Medium" is a guess — full-text search is a deep problem
Dependencies?✅ Needs: all stored messages

Verdict: Needs further decomposition:

Search messages
├── Basic keyword search (exact match within text)
├── Filter by conversation
├── Filter by sender
├── Filter by date range
├── Combine filters (keyword + sender + date range)
└── Return results with surrounding context (messages before/after)

Step 5: Dependency Map

User Management ────────────────── (foundation)
       │
       ▼
Conversations ──── needs users to exist
       │
       ├──────────────────────────┐
       ▼                          ▼
Messages ──── needs conversation   Presence ──── needs user sessions
       │                                │
       ▼                                │
Fan-out to recipients ──────────────────┘
       │                     needs presence to know 
       │                     who's online vs. offline
       ▼
Notifications ──── needs message events + user preferences
       │
       ▼
File Sharing ──── needs messages (files are attached to messages)

What This Map Reveals

The fan-out node depends on BOTH messages AND presence. To deliver a message, you need to know who's online (real-time delivery) vs. offline (push/email). This dependency is invisible if you decompose messages and presence separately — the dependency map reveals the connection.

File sharing is a leaf-level feature. It depends on messages but nothing depends on it. This means it can be built last (or omitted from v1).

Notifications depend on nearly everything. They need messages (what happened), users (who to notify), presence (how to notify), and preferences (should we notify). This makes notifications a high-dependency, build-last feature.


Step 6: The Complete Tree

Messaging App
├── User Management (foundation)
│   ├── Register
│   ├── Login / Logout
│   ├── Update profile
│   └── Block / Unblock user
│
├── Conversations
│   ├── DM: Start conversation (find or create)
│   ├── DM: List my conversations
│   ├── Group: Create group
│   ├── Group: Add / Remove member
│   ├── Group: Leave group
│   ├── Group: Update group details
│   ├── Group: List my groups
│   ├── Shared: Get message history (paginated)
│   └── Shared: Mark as read
│
├── Messages
│   ├── Send message (validate + store)
│   ├── Fan out to recipients
│   │   ├── Real-time delivery (online users)
│   │   ├── Push notification queue (mobile)
│   │   └── Email notification queue (offline, delayed)
│   ├── Edit message
│   ├── Delete message
│   ├── React to message
│   ├── Reply (thread)
│   ├── Pin message
│   └── Search
│       ├── Keyword search
│       ├── Filter by conversation / sender / date
│       └── Return results with context
│
├── Presence
│   ├── Update my status
│   ├── Get user status
│   ├── Get bulk status (sidebar)
│   ├── Typing indicator (broadcast)
│   └── Last seen timestamp
│
├── Notifications
│   ├── In-app badge / pop-up
│   ├── Push notification dispatch
│   ├── Email notification dispatch
│   ├── Notification preferences
│   └── Mark notification as read
│
└── File Sharing
    ├── Upload file (to message)
    ├── Download file
    ├── Generate preview
    └── Enforce limits (size, storage)

Total leaf count: 35+ operations.


Comparing Bookstore vs. Messaging App

AspectBookstoreMessaging App
Primary data flowRequest-response (user asks, system answers)Bidirectional (users send/receive continuously)
Real-time requirementsNone (page refreshes are fine)Critical (messages must appear instantly)
Hardest decomposition challengeCheckout flow (sequential, many steps)Message fan-out (one event → many recipients × multiple channels)
Hidden complexityPayment error handlingPresence + notification routing
Deepest branchCheckout (8 leaves)Messages → Fan-out → 3 delivery channels
Seams discoveredData format changes, time boundariesReal-time vs. stored, sender vs. recipient, audience routing
Build-last featuresReturns, low-stock alertsNotifications, file sharing, search
Approximate leaf count2335+

The messaging app has more leaves not because it's "harder" but because it has more dimensions: real-time + stored, sender + receiver, online + offline. Each dimension multiplies the decomposition.


What This Example Teaches

  1. Real-time creates new seam types — ephemeral vs. persistent data is a fundamental split
  2. Fan-out is hidden complexity — "send a message" sounds atomic but it triggers per-recipient work
  3. Some features bridge multiple branches — notifications depend on messages, presence, AND user preferences
  4. "Start conversation" is a design decision — explicit vs. implicit conversation creation changes the decomposition
  5. More dimensions = more leaves — systems with multiple audiences, delivery channels, and timing requirements have larger trees