January 9, 2026

Finding Engaged Users From Interaction Logs: What I Learned Building It in Java 7, 8, and 17

When you’re given raw interaction events like:

Timestamp,UserId,PostId

…and asked to derive a meaningful cohort (in this case, “engaged users”), the challenge is rarely the syntax. The challenge is choosing the right data structures, keeping the logic correct, and writing code that’s easy to explain in an interview.

This post walks through the core idea, common mistakes, and clean implementations in Java 7, Java 8, and Java 17.


The Problem

You have two days of interaction logs. A user is considered engaged if:

  1. They have at least one interaction on both days, and

  2. Across both days combined, they interacted with at least two unique posts.

Important detail: multiple interactions with the same post count only once toward “unique posts”.


The Core Insight

This is a “group-by user” problem with two checks:

  • Presence on day 1 and day 2

  • Unique post count across both days

The simplest and most reliable approach is:

  • Use a Map keyed by userId.

  • Store post IDs in Sets (because uniqueness is built-in).

Why sets are the right tool

If you store post IDs in a list, you’ll need extra de-dup logic. With sets:

  • Add is O(1) average

  • Uniqueness is automatic


A Simple Data Model

For two days, a lightweight per-user structure is enough:

  • Set<String> day1Posts

  • Set<String> day2Posts

A user qualifies if both sets are non-empty and the union size is ≥ 2.


Common Mistakes (The Ones That Usually Show Up in Interviews)

1) Using counts instead of presence

You only need to know whether the user appeared that day (at least one event). Counting events adds complexity with no benefit unless the requirements change.

2) Mixing mutation inside filtering predicates

Using removeIf(...) or stream filter(...) to also mutate sets is a readability and correctness trap.

Predicates should return a boolean and avoid side effects.

3) Java version mismatch

  • Java 8 streams do not have .toList(). Use collect(Collectors.toList()).

  • Java 17 streams do have .toList().

4) Splitting each log line multiple times

Calling log.split(",") repeatedly creates garbage and slows things down. Parse once per line.


Example Input Used Throughout

// Day 1
"2026-01-01T10:05:12Z,U1,P101"
"2026-01-01T10:07:45Z,U2,P102"
"2026-01-01T10:10:30Z,U1,P103"
"2026-01-01T10:15:00Z,U3,P104"

// Day 2
"2026-01-02T09:00:10Z,U1,P101"
"2026-01-02T09:05:20Z,U2,P105"
"2026-01-02T09:10:55Z,U4,P106"
"2026-01-02T09:12:00Z,U2,P102"

Expected engaged users: [U1, U2] (order doesn’t matter)


Java 7 Implementation (Imperative, Clear, Interview-Friendly)

Java 7 has no streams/lambdas, so we use plain loops.

import java.util.*;

public class EngagedUsersFinderJava7 {

    private static final List<String> DAY1_LOGS = Arrays.asList(
        "2026-01-01T10:05:12Z,U1,P101",
        "2026-01-01T10:07:45Z,U2,P102",
        "2026-01-01T10:10:30Z,U1,P103",
        "2026-01-01T10:15:00Z,U3,P104"
    );

    private static final List<String> DAY2_LOGS = Arrays.asList(
        "2026-01-02T09:00:10Z,U1,P101",
        "2026-01-02T09:05:20Z,U2,P105",
        "2026-01-02T09:10:55Z,U4,P106",
        "2026-01-02T09:12:00Z,U2,P102"
    );

    static class Engage {
        final String userId;
        final Set<String> day1Posts = new HashSet<String>();
        final Set<String> day2Posts = new HashSet<String>();

        Engage(String userId) {
            this.userId = userId;
        }
    }

    public static List<String> findEngagedUsers() {
        Map<String, Engage> userMap = new HashMap<String, Engage>();

        // Day 1
        for (String log : DAY1_LOGS) {
            String[] parts = log.split(",");
            String userId = parts[1];
            String postId = parts[2];

            Engage e = userMap.get(userId);
            if (e == null) {
                e = new Engage(userId);
                userMap.put(userId, e);
            }
            e.day1Posts.add(postId);
        }

        // Day 2
        for (String log : DAY2_LOGS) {
            String[] parts = log.split(",");
            String userId = parts[1];
            String postId = parts[2];

            Engage e = userMap.get(userId);
            if (e == null) {
                e = new Engage(userId);
                userMap.put(userId, e);
            }
            e.day2Posts.add(postId);
        }

        // Filter engaged
        List<String> result = new ArrayList<String>();
        for (Engage e : userMap.values()) {
            if (!e.day1Posts.isEmpty() && !e.day2Posts.isEmpty()) {
                Set<String> union = new HashSet<String>(e.day1Posts);
                union.addAll(e.day2Posts);
                if (union.size() >= 2) {
                    result.add(e.userId);
                }
            }
        }
        return result;
    }

    public static void main(String[] args) {
        System.out.println(findEngagedUsers());
    }
}

Java 8 Implementation (Streams for Filtering)

Java 8 adds streams and lambdas, which can make the filtering phase more declarative.

Important: use collect(Collectors.toList()) in Java 8.

import java.util.*;
import java.util.stream.Collectors;

public class EngagedUsersFinderJava8 {

    private static final List<String> DAY1_LOGS = Arrays.asList(
        "2026-01-01T10:05:12Z,U1,P101",
        "2026-01-01T10:07:45Z,U2,P102",
        "2026-01-01T10:10:30Z,U1,P103",
        "2026-01-01T10:15:00Z,U3,P104"
    );

    private static final List<String> DAY2_LOGS = Arrays.asList(
        "2026-01-02T09:00:10Z,U1,P101",
        "2026-01-02T09:05:20Z,U2,P105",
        "2026-01-02T09:10:55Z,U4,P106",
        "2026-01-02T09:12:00Z,U2,P102"
    );

    static class Engage {
        final String userId;
        final Set<String> day1Posts = new HashSet<>();
        final Set<String> day2Posts = new HashSet<>();

        Engage(String userId) {
            this.userId = userId;
        }
    }

    public static List<String> findEngagedUsers() {
        Map<String, Engage> userMap = new HashMap<>();

        for (String log : DAY1_LOGS) {
            String[] parts = log.split(",");
            String userId = parts[1];
            String postId = parts[2];

            Engage e = userMap.get(userId);
            if (e == null) {
                e = new Engage(userId);
                userMap.put(userId, e);
            }
            e.day1Posts.add(postId);
        }

        for (String log : DAY2_LOGS) {
            String[] parts = log.split(",");
            String userId = parts[1];
            String postId = parts[2];

            Engage e = userMap.get(userId);
            if (e == null) {
                e = new Engage(userId);
                userMap.put(userId, e);
            }
            e.day2Posts.add(postId);
        }

        return userMap.values().stream()
            .filter(e -> !e.day1Posts.isEmpty() && !e.day2Posts.isEmpty())
            .filter(e -> {
                Set<String> union = new HashSet<>(e.day1Posts);
                union.addAll(e.day2Posts);
                return union.size() >= 2;
            })
            .map(e -> e.userId)
            .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        System.out.println(findEngagedUsers());
    }
}

Java 17 Implementation (Records + Modern Conveniences)

Java 17 lets you write concise, expressive code:

  • record for modeling

  • List.of(...) for input lists

  • Stream.toList() for collection

import java.util.*;
import java.util.stream.Stream;

public class EngagedUsersFinderJava17 {

    private static final List<String> DAY1_LOGS = List.of(
        "2026-01-01T10:05:12Z,U1,P101",
        "2026-01-01T10:07:45Z,U2,P102",
        "2026-01-01T10:10:30Z,U1,P103",
        "2026-01-01T10:15:00Z,U3,P104"
    );

    private static final List<String> DAY2_LOGS = List.of(
        "2026-01-02T09:00:10Z,U1,P101",
        "2026-01-02T09:05:20Z,U2,P105",
        "2026-01-02T09:10:55Z,U4,P106",
        "2026-01-02T09:12:00Z,U2,P102"
    );

    record Engage(String userId, Set<String> day1Posts, Set<String> day2Posts) {
        Engage(String userId) {
            this(userId, new HashSet<>(), new HashSet<>());
        }
    }

    public static List<String> findEngagedUsers() {
        Map<String, Engage> userMap = new HashMap<>();

        DAY1_LOGS.forEach(log -> {
            var parts = log.split(",");
            userMap.computeIfAbsent(parts[1], Engage::new).day1Posts().add(parts[2]);
        });

        DAY2_LOGS.forEach(log -> {
            var parts = log.split(",");
            userMap.computeIfAbsent(parts[1], Engage::new).day2Posts().add(parts[2]);
        });

        return userMap.values().stream()
            .filter(e -> !e.day1Posts().isEmpty() && !e.day2Posts().isEmpty())
            .filter(e ->
                Stream.concat(e.day1Posts().stream(), e.day2Posts().stream())
                      .distinct()
                      .count() >= 2
            )
            .map(Engage::userId)
            .toList();
    }

    public static void main(String[] args) {
        System.out.println(findEngagedUsers());
    }
}

A quick note: the record holds mutable sets here. That’s fine for a coding exercise, but in production you’d typically prefer immutability (or keep it as a normal class with encapsulation).


Complexity (What You Should Say Out Loud)

Let:

  • N = number of events on day 1

  • M = number of events on day 2

Time Complexity: O(N + M)
Space Complexity: O(U + P)
(where U is number of users and P is total unique user-post relationships)


How to Explain the Approach in an Interview (A Strong Script)

Here’s a concise explanation that typically lands well:

“I group events by user. For each user I track unique post IDs for each day using sets.
Then I filter users who appear on both days and whose union of post IDs across the two days contains at least two unique posts.
Sets give me uniqueness for free, and the solution is linear in the size of the logs.”


Extensions That Make Great Follow-Ups

1) More than two days

Replace day-specific fields with:

  • Set<LocalDate> activeDays, or

  • BitSet / bitmask if the day count is bounded, or

  • Map<Day, Set<Post>> if you want to keep per-day post sets

2) Huge log files

Process logs line-by-line with BufferedReader, avoid loading everything in memory. The same data structures still work.

3) Malformed lines

Add validation around split length and skip/record bad lines.


Final Takeaways

  • Sets are the simplest way to enforce uniqueness.

  • Separate aggregation from filtering.

  • Be mindful of Java version APIs (toList() vs Collectors.toList()).

  • Clean naming (day1Posts, not day1Users) makes your reasoning easier to follow.

  • The best interview solutions are not the “cleverest”—they’re the clearest.

If you want, I can turn this into a “publish-ready” Medium-style post (with headings, diagrams, and a “what the interviewer is looking for” rubric) or add a JUnit test section for each Java version.