Machine Coding Problem

Ephemeral Stories

macoAllsocialsoft-deleteexpiry-logic
Commonly Asked By:MetaSnapWhatsApp

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.

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 ===");
    }
}