X (Twitter) Interview Prep
Interview Profile
- Format: 4–5 rounds — typically 1 online assessment, 2–3 coding rounds, 1 system design, 1 behavioral
- Coding focus: Graph/BFS heavy (social graph traversal), sliding window for analytics streams, heap-based ranking and feed construction
- System design focus: High-throughput write-heavy systems; fan-out on write vs. fan-out on read for timeline construction
- Senior bar: Expected to discuss write amplification tradeoffs for celebrity accounts (100M+ followers), timeline cache invalidation strategies, and horizontal partitioning of the social graph
- What X cares about: Real-time systems at scale, distributed data structures, social graph traversal, and ranking/trending computation pipelines
Pattern Frequency Breakdown
| Pattern | Estimated Frequency | Why X Asks |
|---|---|---|
| Graphs — BFS/DFS | 30% | Social graph traversal, follower clusters, connected components |
| Sliding Window / Arrays | 25% | Engagement analytics, spam detection, rate limiting |
| Heap / Priority Queue | 20% | Feed ranking, trending computation, k-way merge for timelines |
| HashMap / Design | 15% | Tweet indexing, random sampling, O(1) data structures |
| Dynamic Programming | 10% | Trending score sequences, recommendation relevance |
Problems From This Repo
The following problems from completed sessions directly map to X interview patterns. Study them with the “Why X/Twitter” lens in mind.
| Problem | Session Link | Why X/Twitter |
|---|---|---|
| Number of Islands (LC 200) | [[Session-08-Graphs|Number of Islands]] | Follower cluster detection — each island maps to an isolated engagement cluster |
| Clone Graph (LC 133) | [[Session-08-Graphs|Clone Graph]] | Social graph replication for data center mirroring / shadow reads |
| Find Median from Data Stream (LC 295) | [[Session-07-Heap|Find Median from Data Stream]] | Real-time engagement percentile tracking (P50 impressions, P99 latency) |
| Insert Delete GetRandom O(1) (LC 380) | [[Session-04-HashMap|Insert Delete GetRandom O(1)]] | Random tweet sampling for abuse detection / A-B test randomization |
| Longest Increasing Subsequence (LC 300) | [[Session-09-DP1D|Longest Increasing Subsequence]] | Trending score sequences — detecting monotonically rising engagement runs |
| Number of Connected Components (LC 323) | [[Session-14-AdvancedGraphs|Number of Connected Components]] | Cluster analysis on the follower graph; isolating disconnected communities |
| Non-overlapping Intervals (LC 435) | [[Session-12-Greedy|Non-overlapping Intervals]] | Content de-duplication windows — removing redundant notification batches |
| Binary Tree Level Order Traversal (LC 102) | [[Session-06-Trees|Binary Tree Level Order Traversal]] | BFS social graph levels — “friends of friends” at hop distance k |
| Permutation in String (LC 567) | [[Session-02-SlidingWindow|Permutation in String]] | Spam pattern detection in sliding tweet windows |
| Top K Frequent Elements (LC 347) | [[Session-04-HashMap|Top K Frequent Elements]] | Trending hashtag computation — top-k by frequency in real-time window |
New Problems (X-Specific)
The five problems below are not yet in the repo and are high-signal for X interviews. Each includes a brute-force approach and the optimal approach with full Java code.
Problem 1: Design Twitter (LC 355) — Medium
Link: LeetCode 355
Quick Summary: Implement a simplified Twitter with postTweet, getNewsFeed (most recent 10 tweets from followed users), follow, and unfollow.
Industry Context: This is literally the Twitter product as a coding problem. Interviewers at X will ask it and then immediately escalate: “What if a user has 100 million followers — how does postTweet change?” This forces the fan-out on write vs. fan-out on read discussion. In production, X uses a hybrid: fan-out on write for users with < ~10K followers (pre-populate timelines), and fan-out on read for celebrity accounts (inject at read time to avoid write amplification).
Approach 1: Brute Force — Linear scan per feed request
// Time: postTweet O(1), getNewsFeed O(U*T) where U=followed users, T=tweets per user
// Space: O(total tweets)
import java.util.*;
class TwitterBrute {
private final Map<Integer, List<int[]>> tweets = new HashMap<>(); // userId -> [[tweetId, timestamp]]
private final Map<Integer, Set<Integer>> following = new HashMap<>();
private int time = 0;
public void postTweet(int userId, int tweetId) {
tweets.computeIfAbsent(userId, k -> new ArrayList<>()).add(new int[]{tweetId, time++});
}
public List<Integer> getNewsFeed(int userId) {
// Collect all tweets from self + followed, sort by timestamp desc
List<int[]> all = new ArrayList<>();
Set<Integer> sources = new HashSet<>(following.getOrDefault(userId, Collections.emptySet()));
sources.add(userId);
for (int uid : sources) {
all.addAll(tweets.getOrDefault(uid, Collections.emptyList()));
}
all.sort((a, b) -> b[1] - a[1]); // sort by timestamp descending
List<Integer> feed = new ArrayList<>();
for (int i = 0; i < Math.min(10, all.size()); i++) feed.add(all.get(i)[0]);
return feed;
}
public void follow(int followerId, int followeeId) {
following.computeIfAbsent(followerId, k -> new HashSet<>()).add(followeeId);
}
public void unfollow(int followerId, int followeeId) {
following.getOrDefault(followerId, Collections.emptySet()).remove(followeeId);
}
}- Time:
getNewsFeedO(U * T + UT * log(UT)) — collecting and sorting all tweets from all followed users - Space: O(total tweets stored)
- Tradeoff: Correct but blows up when a user follows thousands of accounts each with thousands of tweets. The sort over all collected tweets is the bottleneck.
Approach 2: Optimal — Min-heap k-way merge (fan-out on read)
// Time: postTweet O(1), getNewsFeed O(U log U + 10 log U) ≈ O(U log U)
// Space: O(U) for the heap at any given getNewsFeed call
import java.util.*;
class TwitterOptimal {
// Each tweet: [tweetId, timestamp]
private final Map<Integer, Deque<int[]>> tweets = new HashMap<>();
private final Map<Integer, Set<Integer>> following = new HashMap<>();
private int time = 0;
public void postTweet(int userId, int tweetId) {
tweets.computeIfAbsent(userId, k -> new ArrayDeque<>()).addFirst(new int[]{tweetId, time++});
}
public List<Integer> getNewsFeed(int userId) {
// Max-heap by timestamp — always grab the freshest tweet across all followed users
// [timestamp, tweetId, iterator over that user's tweet deque]
PriorityQueue<int[]> maxHeap = new PriorityQueue<>((a, b) -> b[0] - a[0]);
Set<Integer> sources = new HashSet<>(following.getOrDefault(userId, Collections.emptySet()));
sources.add(userId);
// Seed: push the head (most recent) tweet of each source into the heap
Map<Integer, Iterator<int[]>> iters = new HashMap<>();
for (int uid : sources) {
Deque<int[]> userTweets = tweets.getOrDefault(uid, new ArrayDeque<>());
if (!userTweets.isEmpty()) {
Iterator<int[]> it = userTweets.iterator();
int[] head = it.next();
maxHeap.offer(new int[]{head[1], head[0], uid}); // [ts, tweetId, uid]
iters.put(uid, it);
}
}
List<Integer> feed = new ArrayList<>();
while (!maxHeap.isEmpty() && feed.size() < 10) {
int[] top = maxHeap.poll();
feed.add(top[1]); // tweetId
int uid = top[2];
Iterator<int[]> it = iters.get(uid);
if (it != null && it.hasNext()) {
int[] next = it.next();
maxHeap.offer(new int[]{next[1], next[0], uid});
}
}
return feed;
}
public void follow(int followerId, int followeeId) {
following.computeIfAbsent(followerId, k -> new HashSet<>()).add(followeeId);
}
public void unfollow(int followerId, int followeeId) {
following.getOrDefault(followerId, Collections.emptySet()).remove(followeeId);
}
}- Time:
getNewsFeedO(U log U) to build heap + O(10 log U) to extract 10 tweets = O(U log U) - Space: O(U) heap + O(total tweets) storage
- Key Insight: This is the k-way merge pattern from LC 23 (Merge K Sorted Lists), applied to per-user tweet streams. Each user’s tweets are stored newest-first so a simple iterator walk gives descending order. The heap only ever holds one entry per followed user (the “frontier”), not all tweets — so heap size is bounded by U (number of followed users), not total tweet count. This is fan-out on read: the merge happens at read time, keeping writes O(1).
Problem 2: Top K Frequent Words (LC 692) — Medium
Link: LeetCode 692
Quick Summary: Given a list of words and integer k, return the k most frequent words sorted by frequency descending; ties broken alphabetically ascending.
Industry Context: Trending topics computation at X — the Trending tab must rank hashtags and keywords by frequency within a time window, and when two topics have identical tweet counts the alphabetical tie-breaking ensures deterministic ordering. This tests whether a candidate can construct a compound Comparator in Java, which trips up many engineers who only handle single-key comparisons.
Approach 1: Brute Force — Sort after counting
// Time: O(n + m log m) Space: O(m) where n = words.length, m = unique words
import java.util.*;
public static List<String> topKFrequentWordsBrute(String[] words, int k) {
Map<String, Integer> freq = new HashMap<>();
for (String w : words) freq.merge(w, 1, Integer::sum);
List<String> candidates = new ArrayList<>(freq.keySet());
// Sort: higher frequency first; ties → alphabetical
candidates.sort((a, b) -> {
int cmp = freq.get(b) - freq.get(a); // descending frequency
return cmp != 0 ? cmp : a.compareTo(b); // ascending lexicographic on tie
});
return candidates.subList(0, k);
}- Time: O(n) to count + O(m log m) to sort all unique words — even words outside the top-k are sorted
- Space: O(m) for the frequency map and candidate list
- Tradeoff: Simple and readable. Acceptable when m is small. For large vocabularies (trending across all of X globally, m could be tens of millions), sorting the entire vocabulary to find k results is wasteful.
Approach 2: Optimal — Min-heap of size k with compound Comparator
// Time: O(n + m log k) Space: O(m + k)
import java.util.*;
public static List<String> topKFrequentWordsOptimal(String[] words, int k) {
Map<String, Integer> freq = new HashMap<>();
for (String w : words) freq.merge(w, 1, Integer::sum);
// Min-heap: evict the "least desirable" element when size exceeds k.
// "Least desirable" = lowest frequency; on tie = lexicographically largest
// (because we want to keep lexicographically smallest on ties in the result)
PriorityQueue<String> minHeap = new PriorityQueue<>((a, b) -> {
int cmp = freq.get(a) - freq.get(b); // ascending frequency → min on top
return cmp != 0 ? cmp : b.compareTo(a); // descending lex → larger word on top (evict it)
});
for (String word : freq.keySet()) {
minHeap.offer(word);
if (minHeap.size() > k) minHeap.poll(); // evict least desirable
}
// Drain heap into result; heap gives ascending order, so build list in reverse
List<String> result = new ArrayList<>();
while (!minHeap.isEmpty()) result.add(minHeap.poll());
Collections.reverse(result);
return result;
}- Time: O(n) counting + O(m log k) heap maintenance — only k entries in heap at once
- Space: O(m) map + O(k) heap
- Key Insight: The comparator direction inversion is the senior-level trap here. The heap’s natural order determines what gets evicted (polled) when size exceeds k — so we want the “least wanted” element at the top. For frequency that means lowest frequency on top (ascending), so
freq.get(a) - freq.get(b). For lexicographic tie-breaking we want to keep the alphabetically smaller word, so we evict the alphabetically larger one — put it on top withb.compareTo(a)(descending). When draining for the result, reverse to get descending frequency order.
Problem 3: Longest Consecutive Sequence (LC 128) — Medium
Link: LeetCode 128
Quick Summary: Given an unsorted array of integers, return the length of the longest consecutive elements sequence in O(n) time.
Industry Context: Streak detection in engagement analytics — X tracks consecutive active days (daily tweet streaks), consecutive trending hours for a hashtag, or consecutive active sessions for a creator. The O(n) requirement maps to the constraint that this check runs on every metrics ingestion event, so sorting is too slow for the ingestion hot path.
Approach 1: Brute Force — Sort
// Time: O(n log n) Space: O(1) if sorting in-place (or O(n) copy)
import java.util.*;
public static int longestConsecutiveBrute(int[] nums) {
if (nums.length == 0) return 0;
Arrays.sort(nums);
int best = 1, current = 1;
for (int i = 1; i < nums.length; i++) {
if (nums[i] == nums[i - 1]) continue; // skip duplicate
if (nums[i] == nums[i - 1] + 1) {
current++;
best = Math.max(best, current);
} else {
current = 1;
}
}
return best;
}- Time: O(n log n) — dominated by sort
- Space: O(1) extra if in-place sort is allowed; O(n) if a copy is needed
- Tradeoff: Straightforward. Violates the O(n) requirement in the problem statement. In a live ingestion pipeline at X scale, the sort cost per event is prohibitive.
Approach 2: Optimal — HashSet O(n) boundary trick
// Time: O(n) Space: O(n)
import java.util.*;
public static int longestConsecutiveOptimal(int[] nums) {
Set<Integer> numSet = new HashSet<>();
for (int n : nums) numSet.add(n);
int best = 0;
for (int n : numSet) {
// Only start counting from the beginning of a sequence.
// n is a sequence start if (n-1) is not in the set.
if (!numSet.contains(n - 1)) {
int current = n;
int length = 1;
while (numSet.contains(current + 1)) {
current++;
length++;
}
best = Math.max(best, length);
}
}
return best;
}- Time: O(n) — each element is visited at most twice: once in the outer loop and once in the inner
while - Space: O(n) for the HashSet
- Key Insight: The
!numSet.contains(n - 1)guard is the entire trick — it ensures we only start counting from the left boundary of a sequence. Without it, we’d re-scan every element in every sequence, giving O(n²) worst case. With it, the innerwhileonly runs in total O(n) steps across all outer loop iterations (each number is the “current” in the while loop at most once). This pattern — “only start work at a boundary, never in the middle” — is a general O(n) streak detection template. Tests that candidates understand HashSet is O(1) lookup, not a sorted structure.
Problem 4: The Maze (LC 490) — Medium
Link: LeetCode 490
Quick Summary: A ball starts at a position in a maze. It rolls in a direction until hitting a wall, then stops. Determine if it can reach the destination.
Industry Context: Content propagation through constrained graphs — routing rules where a message propagates along a channel until it hits a “wall” (rate limiter, policy boundary, or dead-end shard). The non-standard neighbor-generation rule (roll until wall, not step-by-step) is the senior-level signal: if a candidate applies standard BFS with single-step neighbors, they get a wrong answer without realizing it.
Approach 1: Brute Force — DFS without visited tracking
// Time: O((m*n)^2) worst case without visited set Space: O(m*n) recursion
// WARNING: This TLEs or produces wrong answers without visited set
import java.util.*;
public static boolean hasPathDFS(int[][] maze, int[] start, int[] destination) {
boolean[][] visited = new boolean[maze.length][maze[0].length];
return dfs(maze, start[0], start[1], destination, visited);
}
private static boolean dfs(int[][] maze, int r, int c, int[] dest, boolean[][] visited) {
if (r == dest[0] && c == dest[1]) return true;
if (visited[r][c]) return false;
visited[r][c] = true;
int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};
for (int[] d : dirs) {
// Roll until hitting a wall
int nr = r, nc = c;
while (nr + d[0] >= 0 && nr + d[0] < maze.length
&& nc + d[1] >= 0 && nc + d[1] < maze[0].length
&& maze[nr + d[0]][nc + d[1]] == 0) {
nr += d[0];
nc += d[1];
}
if (dfs(maze, nr, nc, dest, visited)) return true;
}
return false;
}- Time: O(m * n * (m + n)) — each cell is a node; rolling in each direction costs O(m) or O(n)
- Space: O(m * n) visited array + O(m * n) recursion depth worst case
- Tradeoff: DFS with visited is actually correct and not “brute force” in the traditional sense here — the key mistake candidates make is forgetting the visited array entirely. This is the standard approach; BFS below is preferred for shortest-path variants.
Approach 2: Optimal — BFS with roll-to-wall neighbor generation
// Time: O(m * n * (m + n)) Space: O(m * n)
import java.util.*;
public static boolean hasPathBFS(int[][] maze, int[] start, int[] destination) {
int m = maze.length, n = maze[0].length;
boolean[][] visited = new boolean[m][n];
Queue<int[]> queue = new LinkedList<>();
queue.offer(start);
visited[start[0]][start[1]] = true;
int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};
while (!queue.isEmpty()) {
int[] cur = queue.poll();
if (cur[0] == destination[0] && cur[1] == destination[1]) return true;
for (int[] d : dirs) {
int nr = cur[0], nc = cur[1];
// Roll until hitting a wall — this is the non-standard neighbor rule
while (nr + d[0] >= 0 && nr + d[0] < m
&& nc + d[1] >= 0 && nc + d[1] < n
&& maze[nr + d[0]][nc + d[1]] == 0) {
nr += d[0];
nc += d[1];
}
if (!visited[nr][nc]) {
visited[nr][nc] = true;
queue.offer(new int[]{nr, nc});
}
}
}
return false;
}- Time: O(m * n * (m + n)) — each cell enqueued at most once; rolling costs O(m+n) per direction per cell
- Space: O(m * n) for visited array and queue
- Key Insight: The graph here has stop positions (where the ball rests after rolling) as nodes, not every cell. The edge between two nodes requires simulating the roll — that inner
whileloop IS the edge-generation logic. Standard BFS/DFS structure is unchanged; only the neighbor-generation is non-standard. BFS is preferred over DFS when LC 505 (Maze II — shortest path) is the follow-up, as BFS naturally finds minimum-distance paths. Mark visited before enqueuing, not after polling, to avoid the same stop-position being enqueued multiple times from different directions.
Problem 5: Minimum Number of Vertices to Reach All Nodes (LC 1557) — Medium
Link: LeetCode 1557
Quick Summary: In a DAG with n nodes, find the minimum set of vertices from which all nodes are reachable.
Industry Context: Follower graph root identification — finding the minimal set of accounts from which the entire follow graph is reachable (e.g., identifying original content sources vs. amplifiers in a retweet cascade). This is a senior-level “trap” problem: it appears complex but reduces trivially once you recognize the invariant.
Approach 1: Brute Force — BFS/DFS reachability check
// Time: O(V * (V + E)) Space: O(V + E)
// Tries every possible source set — naive and far too slow for any real graph
import java.util.*;
// NOTE: This naive approach is impractical. Included to show the contrast with the trivial insight.
public static List<Integer> findSmallestSetOfVerticesBrute(int n, List<List<Integer>> edges) {
// Build adjacency list
Map<Integer, List<Integer>> adj = new HashMap<>();
Set<Integer> hasIncoming = new HashSet<>();
for (List<Integer> edge : edges) {
adj.computeIfAbsent(edge.get(0), k -> new ArrayList<>()).add(edge.get(1));
hasIncoming.add(edge.get(1));
}
// Any node with in-degree 0 MUST be in the source set (nothing points to it)
// Any node with in-degree > 0 CAN be reached from its predecessors
// Therefore: source set = all nodes with in-degree 0
List<Integer> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (!hasIncoming.contains(i)) result.add(i);
}
return result;
}- Time: O(V + E) — the “brute force” framing collapses immediately to the trivial solution
- Space: O(V + E)
- Tradeoff: There is no meaningful brute-force for this problem once you see the invariant. The trap is spending time on BFS reachability. The senior signal is recognizing the O(V+E) solution in under two minutes.
Approach 2: Optimal — In-degree 0 collection (O(V+E))
// Time: O(V + E) Space: O(V)
import java.util.*;
public static List<Integer> findSmallestSetOfVerticesOptimal(int n, List<List<Integer>> edges) {
// A node with in-degree > 0 is always reachable from some other node.
// A node with in-degree 0 has no predecessors — it MUST be included as a source.
// These two facts together mean the answer is exactly the set of nodes with in-degree 0.
boolean[] hasIncoming = new boolean[n];
for (List<Integer> edge : edges) {
hasIncoming[edge.get(1)] = true; // edge.get(1) has at least one incoming edge
}
List<Integer> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (!hasIncoming[i]) result.add(i);
}
return result;
}- Time: O(V + E) — single pass over edges + single pass over vertices
- Space: O(V) for the boolean array
- Key Insight: Every node with in-degree > 0 is reachable from at least one other node — so it does not need to be in the source set. Every node with in-degree 0 is unreachable from any other node — so it must be in the source set. These two facts together prove that the answer is exactly and only the nodes with in-degree 0, with no further reasoning needed. When X interviewers use this as a “trap”, they are testing whether a senior candidate can resist over-engineering (BFS reachability simulation) and instead reason from graph-theoretic invariants to a one-liner solution. State the insight out loud before coding — that is the senior signal.
System Design Connections
| Algorithm / Pattern | X System Design Topic |
|---|---|
| Graph BFS (LC 200, LC 323) | Social graph BFS for follower/following traversal; connected component = engagement cluster |
| k-way merge heap (LC 355 feed) | Fan-out on read timeline construction — merge k per-user tweet streams at read time |
| Top-K heap (LC 347, LC 692) | Trending computation pipeline — count-min sketch + heap over sliding windows |
| HashSet streak (LC 128) | Consecutive engagement analytics — streak detection on the hot metrics ingestion path |
| In-degree 0 (LC 1557) | Content source identification in retweet cascades; root discovery in propagation DAG |
Fan-Out Write vs. Fan-Out Read
This is the canonical X system design topic. Always be ready to discuss:
- Fan-out on write: On
postTweet, push the tweet ID to every follower’s timeline cache (Redis sorted set, score = timestamp).getNewsFeedis O(1) — just read the pre-populated cache. Write amplification: a single tweet from a 100M-follower account generates 100M cache writes. X uses this for normal users (< ~10K followers). - Fan-out on read: On
postTweet, only store the tweet in the author’s own store.getNewsFeedpulls and merges tweets from all followed users at read time (k-way merge). Read amplification: every timeline load must merge k streams. X uses this for celebrity accounts to avoid write amplification. - Hybrid: X’s actual production system uses fan-out on write for most users and injects celebrity tweets into timelines at read time. The k-way merge in LC 355 Approach 2 above is the read-path merge logic.
Trending Topic Computation Pipeline
- Ingest tweet stream into Kafka
- Flink/Storm windowed aggregation: count hashtag frequency in 1-hour tumbling windows
- Top-K extraction: maintain a min-heap of size K per partition, merge partition heaps at reduce step (LC 347 / LC 692 pattern)
- Serve trending list from Redis with TTL matching the window size
2-Week Sprint Plan
| Day | Focus | Problems |
|---|---|---|
| 1–2 | Graph BFS/DFS foundation | LC 200 (repo), LC 133 (repo), LC 323 (repo) |
| 3–4 | Feed design + k-way merge | LC 355 (this sheet — Design Twitter) |
| 5–6 | Trending / Top-K | LC 347 (repo), LC 692 (this sheet — Top K Frequent Words) |
| 7 | Streak detection | LC 128 (this sheet — Longest Consecutive Sequence) |
| 8–9 | Non-standard graph traversal | LC 490 (this sheet — The Maze) |
| 10 | Graph invariant reasoning | LC 1557 (this sheet — Minimum Vertices) |
| 11–12 | Mock interview — 2 problems in 45 min | Pick 1 graph + 1 heap/design |
| 13–14 | System design deep dive | Fan-out write vs. read; trending pipeline end-to-end |
Senior-Level Talking Points
These are the answers that separate a senior candidate from a mid-level candidate at X:
- Write amplification: Always bring up fan-out write vs. fan-out read unprompted when any feed/social graph design comes up. State the threshold (~10K followers for hybrid at X) and the tradeoff. This signals production system awareness.
- Timeline cache invalidation: When a user unfollows someone, their pre-populated timeline cache may contain stale tweets from the unfollowed account. Discuss lazy invalidation (TTL expiry) vs. eager invalidation (write-through delete on unfollow).
- Hot vs. cold path: High-follower accounts (Elon Musk, Barack Obama) are the “hot path” — design decisions for them differ fundamentally from the median user. Always call out this asymmetry.
- Graph partitioning: The social graph at X scale cannot fit on one machine. Discuss hash-based vs. locality-based partitioning of the follower graph; edges that cross partitions require cross-shard joins.
- Sliding window for analytics: When asked about rate limiting or spam detection, default to sliding window counter (LC 567 pattern) over fixed-window counter — explain the boundary-burst problem with fixed windows.
- Heap with compound Comparator: In Java, always verify the
Comparatordirection matches what gets evicted (min-heap evicts minimum). Articulate the comparator out loud before writing it.