Machine Coding Problem

Ephemeral Stories

macoAllsocialsoft-deleteexpiry-logic
Commonly Asked By:MetaSnapWhatsApp

Requirements & System Scope

Functional Scope (In-Scope)

  • Soft-Delete Expiry Lifecycles: Automatically updates stories to archived state after 24 hours rather than performing physical hard-deletion.
  • Follower Story Feed Builder: Aggregates, filters, and sorts non-expired stories from followees dynamically.
  • Atomic Story View Tracking: Thread-safe view recording and list logs block duplicate counts.
  • Creator Archive Access: Retains expired records in private creator listings while evicting them from general feeds.

Explicit Boundaries (Out-of-Scope)

  • Concrete Multi-CDN Media Resizing: Simplifies image compression, video chunking, and file-hosting details.
  • Full-Featured Direct Messaging: Omits real-time chat widgets, push alerts, and direct messaging features.

Class Diagram & Entity Relationships

Soft delete indicator triggers, view counts, and follower matrix mapping:

Loading...
  • Story Model: Holds creator IDs, absolute expirations, a soft-delete boolean, and unique viewer sets.
  • Follower Registry: Maps followers to targets to drive feed generation.

Design Patterns & SOLID Principles

  • Observer Pattern / Publish-Subscribe: Decouples views by using event queues to update view counts asynchronously, reducing write pressure on the main thread.
  • Filter Pattern: Separates criteria evaluation (e.g. ActiveFeedFilter, ArchiveFilter) from story queries.
  • Single Responsibility Principle (SRP): Isolates story feed queries, soft-deletion jobs, and view logging into separate modules.

Core Execution Workflows

Nondestructive Soft-deletion & Feed Querying

  1. Publishing & Feed Generation Flow:
    1. Create a story with expiresAt = createdAt + 24 hours.
    2. When followers query their feed, check the followee list and pull their active stories.
    3. Exclude stories where isExpired is true or the current time exceeds the expiration timestamp.
    4. Sort the matching stories chronologically.
  2. Soft-delete Expiry Sweeps:
    1. A background cron job runs at regular intervals.
    2. Query active stories where the current timestamp exceeds the expiration time.
    3. Set isExpired = true for expired stories, removing them from feeds while preserving them in creator archives.

Concurrency & Thread Safety Strategy

Securing access under parallel view counts and updates:

  • Volatile Expiry Status: Expiry flags use memory barriers to ensure background sweeps are immediately visible to feeds.
  • Concurrent Viewer Collections: Thread-safe collections manage viewer sets to prevent duplicate counts during concurrent requests.

Complete Clean Code Blueprint

Production reference implementations demonstrating soft-deletes, follower feeds, view tracking logs, and creator archives in Java and Python:

// โ”€โ”€โ”€ JAVA BLUEPRINT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;

class Story {
    private final String id;
    private final String creatorId;
    private final String mediaUrl;
    private final long createdAtMs;
    private final long expiresAtMs;
    private volatile boolean isExpired; // Soft Delete indicator
    private final Set<String> viewerIds;
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public Story(String id, String creatorId, String mediaUrl, long durationMs) {
        this.id = id;
        this.creatorId = creatorId;
        this.mediaUrl = mediaUrl;
        this.createdAtMs = System.currentTimeMillis();
        this.expiresAtMs = this.createdAtMs + durationMs;
        this.isExpired = false;
        this.viewerIds = ConcurrentHashMap.newKeySet();
    }

    public String getId() { return id; }
    public String getCreatorId() { return creatorId; }
    public String getMediaUrl() { return mediaUrl; }
    public long getCreatedAtMs() { return createdAtMs; }
    public long getExpiresAtMs() { return expiresAtMs; }
    
    public boolean isExpired(long now) {
        lock.readLock().lock();
        try {
            return isExpired || now > expiresAtMs;
        } finally {
            lock.readLock().unlock();
        }
    }

    public void markExpired() {
        lock.writeLock().lock();
        try {
            this.isExpired = true;
        } finally {
            lock.writeLock().unlock();
        }
    }

    public boolean getIsExpiredIndicator() {
        lock.readLock().lock();
        try {
            return isExpired;
        } finally {
            lock.readLock().unlock();
        }
    }

    public void addViewer(String userId) {
        viewerIds.add(userId);
    }

    public Set<String> getViewerIds() {
        return Collections.unmodifiableSet(viewerIds);
    }

    public int getViewCount() {
        return viewerIds.size();
    }
}

class ViewRecord {
    private final String userId;
    private final String storyId;
    private final long viewedAtMs;

    public ViewRecord(String userId, String storyId, long viewedAtMs) {
        this.userId = userId;
        this.storyId = storyId;
        this.viewedAtMs = viewedAtMs;
    }

    public String getUserId() { return userId; }
    public String getStoryId() { return storyId; }
    public long getViewedAtMs() { return viewedAtMs; }
}

class EphemeralStoryService {
    private final ConcurrentHashMap<String, Story> stories = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Set<String>> followMatrix = new ConcurrentHashMap<>(); // userId -> following set
    private final ConcurrentLinkedQueue<ViewRecord> viewLog = new ConcurrentLinkedQueue<>();
    private final ScheduledExecutorService sweeperExecutor = Executors.newSingleThreadScheduledExecutor(runnable -> {
        Thread thread = new Thread(runnable, "StorySweeper");
        thread.setDaemon(true);
        return thread;
    });

    public EphemeralStoryService() {
        // Run background sweep every 500 milliseconds for demo responsiveness
        sweeperExecutor.scheduleAtFixedRate(this::purgeExpiredStories, 500, 500, TimeUnit.MILLISECONDS);
    }

    public void follow(String followerId, String followeeId) {
        followMatrix.computeIfAbsent(followerId, k -> ConcurrentHashMap.newKeySet()).add(followeeId);
    }

    public Story publishStory(String creatorId, String mediaUrl, long customDurationMs) {
        String storyId = UUID.randomUUID().toString();
        long duration = customDurationMs > 0 ? customDurationMs : 24 * 60 * 60 * 1000L; // Default 24h

        Story story = new Story(storyId, creatorId, mediaUrl, duration);
        stories.put(storyId, story);
        return story;
    }

    public List<Story> getActiveFeed(String userId) {
        Set<String> following = followMatrix.get(userId);
        if (following == null || following.isEmpty()) {
            return Collections.emptyList();
        }

        long now = System.currentTimeMillis();
        return stories.values().stream()
            .filter(story -> following.contains(story.getCreatorId()))
            .filter(story -> !story.isExpired(now))
            .sorted(Comparator.comparingLong(Story::getCreatedAtMs).reversed())
            .collect(Collectors.toList());
    }

    public void recordView(String userId, String storyId) {
        Story story = stories.get(storyId);
        if (story == null) {
            throw new IllegalArgumentException("Story does not exist.");
        }
        
        long now = System.currentTimeMillis();
        if (story.isExpired(now)) {
            throw new IllegalStateException("Cannot view an expired story.");
        }

        story.addViewer(userId);
        viewLog.offer(new ViewRecord(userId, storyId, now));
    }

    public List<Story> getCreatorArchive(String creatorId) {
        return stories.values().stream()
            .filter(story -> story.getCreatorId().equals(creatorId))
            .sorted(Comparator.comparingLong(Story::getCreatedAtMs).reversed())
            .collect(Collectors.toList());
    }

    public void purgeExpiredStories() {
        long now = System.currentTimeMillis();
        stories.values().forEach(story -> {
            if (!story.getIsExpiredIndicator() && now > story.getExpiresAtMs()) {
                story.markExpired(); // Soft Delete
                System.out.println("[SWEEPER] Auto-archived expired Story ID: " + story.getId() + " by Creator: " + story.getCreatorId());
            }
        });
    }

    public void shutdown() {
        sweeperExecutor.shutdown();
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        System.out.println("=== JAVA EPHEMERAL STORIES DEMO ===");
        EphemeralStoryService service = new EphemeralStoryService();

        // Setup social graph
        service.follow("Alice", "Bob");
        service.follow("Alice", "Charlie");

        // Bob publishes a story with 24 hours expiry
        Story bobsStory = service.publishStory("Bob", "https://media.com/bob_sunset.png", 24 * 60 * 60 * 1000L);
        System.out.println("Bob published story ID: " + bobsStory.getId());

        // Charlie publishes an ephemeral story with tiny duration (800ms) for testing expiration
        Story charliesStory = service.publishStory("Charlie", "https://media.com/charlie_coffee.png", 800L);
        System.out.println("Charlie published quick-expiry story ID: " + charliesStory.getId());

        // Alice fetches feed immediately
        List<Story> feedBefore = service.getActiveFeed("Alice");
        System.out.println("Alice's Active Feed count immediately: " + feedBefore.size());
        for (Story s : feedBefore) {
            System.out.println(" - Story by " + s.getCreatorId() + " (" + s.getMediaUrl() + ")");
        }

        // Alice views Bob's story
        service.recordView("Alice", bobsStory.getId());
        System.out.println("Alice viewed Bob's story. View count: " + bobsStory.getViewCount());

        // Sleep to let Charlie's story expire
        System.out.println("Waiting 1200ms for Charlie's story to expire...");
        Thread.sleep(1200);

        // Alice fetches feed again
        List<Story> feedAfter = service.getActiveFeed("Alice");
        System.out.println("Alice's Active Feed count after 1.2s: " + feedAfter.size());
        for (Story s : feedAfter) {
            System.out.println(" - Story by " + s.getCreatorId() + " (" + s.getMediaUrl() + ")");
        }

        // Alice tries to view Charlie's expired story (should fail)
        try {
            service.recordView("Alice", charliesStory.getId());
        } catch (Exception e) {
            System.out.println("Expected exception when viewing expired story: " + e.getMessage());
        }

        // Charlie can still see his own expired story in his private archive
        List<Story> charlieArchive = service.getCreatorArchive("Charlie");
        System.out.println("Charlie's archive count: " + charlieArchive.size());
        System.out.println("Charlie's archive story 1 expired state: " + charlieArchive.get(0).getIsExpiredIndicator());

        service.shutdown();
        System.out.println("=== END OF JAVA DEMO ===");
    }
}

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...