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