Q2 - MongoDB Legacy Data Cleanup (~2 Parts)
We believe this style of question has been reported repeatedly by recent MongoDB candidates.
It will feel different from anything on LeetCode, there is no single "right" answer. If you've solved 500 algorithm problems, your honest reaction will be: "I've never seen this exact thing… but I can clearly do it." That reaction is the whole point. MongoDB uses problems like this to see how you operate when the spec is fuzzy and real, the way an actual ticket would be.
You've just joined a team migrating data into MongoDB. A colleague drops a text file on you, a dump exported from some ancient CRM that's being decommissioned, and says:
"Can you clean this up so we can import it? It's kind of a mess."
That's the entire spec. No schema. No format document. No examples of the desired output. Just a messy file and a vague ask. Welcome to the interview.
When you peek inside, the file looks something like this, clearly meant to be a list of records, but exported by something that didn't care much about consistency:
A peek at the messy file
# CRM export - legacy system v3
# generated 2026-01-02
Name: John Doe
Email: JOHN@EXAMPLE.COM
Phone: 555-1234
NAME: Jane Smith
email : jane@sample.org
Name: Bob Jones
Phone: 555 222 3333
this line is junk with no field
Email: bob@test.io
Inconsistent key casing, random extra spaces, comment lines, blank lines between records, a junk line with no field at all. Your job, loosely stated, is to turn this into something structured and clean that could be imported as documents.
💡 First, the meta-insight, why this question even exists. Multiple candidates have reported that MongoDB and Stripe lean heavily on file, string, and text-parsing problems, lots of splitting, trimming, normalizing, and reformatting, rather than classic graph/DP problems. It makes sense: both companies live and die by ingesting messy real-world data (documents, payment events, API payloads).
Because of this, and because the work is mostly string manipulation, we strongly recommend solving these in Python, its standard library (str.split, str.strip, str.partition, the csv and json modules) makes you dramatically faster than in a more verbose language. That said, sometimes the interviewer will pin you to a specific language, usually whatever you discussed with your recruiter or the language the team works in. Confirm that up front. (We still provide all 8 languages below for completeness.)
🗣️ This is a "drive the spec with questions" problem, do not start coding yet. The input and output formats are intentionally unspecified, and a strong candidate interrogates the problem before writing a line. Ask things like:
- What is the input, exactly? A file path I open myself? An already-open file handle? A list of lines? One big string? (This changes your very first line of code.)
- Is it actually a CSV under the hood? If so, I'll reach for the
csvmodule / a CSV library instead of hand-splitting. It correctly handles quoting, embedded commas, and newlines. If it's free-form blocks like the sample, I'll parse it manually. - What separates a key from a value, only
:? Could it be=? And what separates records, blank lines? - What do I do with junk lines and duplicate keys? Skip them silently, collect them for a report, or raise an error?
- What's the desired output? A list of dictionaries/maps? JSON? Write back out to a file? Normalized how, should I lowercase emails, strip phone formatting?
The good news for MongoDB specifically: candidates consistently describe MongoDB interviewers as relaxed about which approach you pick. They generally don't mind your choices as long as you state your assumptions out loud and justify them. "I'll assume the input is the raw file text as a string, split records on blank lines, and skip malformed lines, sound okay?" is exactly the kind of sentence that scores points. (Some companies are pickier and want one exact format; MongoDB tends to be chill, make your case and move.)
For the rest of this walkthrough, we'll commit to a reasonable set of assumptions, state these to your interviewer and adjust if they push back:
- The input is the raw file content as a single string.
- Records are separated by blank lines; each non-empty, non-comment line is a
key: valuepair split on the first:. - Keys are normalized: trimmed and lowercased. Values are trimmed.
- Comment lines start with
#and are ignored. Malformed lines (no:) are skipped. - Output is a list of records, each a key→value map.
Part 1: Normalizing One Messy File (~20 minutes)
Write a function parse_records(content) that takes the raw text and returns the cleaned list of records.
Example 1
Input (raw file text):
"""
# CRM export - legacy system v3
Name: John Doe
Email: JOHN@EXAMPLE.COM
Phone: 555-1234
NAME: Jane Smith
email : jane@sample.org
Name: Bob Jones
Phone: 555 222 3333
this line is junk with no field
Email: bob@test.io
"""
Output (list of records):
[
{"name": "John Doe", "email": "JOHN@EXAMPLE.COM", "phone": "555-1234"},
{"name": "Jane Smith", "email": "jane@sample.org"},
{"name": "Bob Jones", "phone": "555 222 3333", "email": "bob@test.io"},
]
Notice the normalization in action: NAME: and Name: both become the key name; the stray spaces around email : are gone; the comment line and the "this line is junk..." line vanish; and blank lines correctly slice the stream into three records.
Solution - Part 1
The approach is a single linear pass, a tiny state machine with exactly one piece of state: the record we're currently building. We walk the lines and react to each one:
- Blank line → the current record is done; if it has any fields, push it and start a fresh one.
- Comment (
#…) → ignore. key: value→ split on the first:(values may legitimately contain colons, e.g. a URL), normalize the key, trim the value, store it.- Anything else (no
:) → malformed; skip it (we could just as easily collect these into a "rejects" list, a nice thing to mention).
Here's the idiomatic Python version first, since this is exactly the kind of problem where Python's str.partition and str.strip shine:
def parse_records(content: str) -> list[dict]:
records = []
current = {}
for raw_line in content.splitlines():
line = raw_line.strip()
if line == "": # blank line -> record boundary
if current:
records.append(current)
current = {}
continue
if line.startswith("#"): # comment -> ignore
continue
if ":" not in line: # malformed -> skip (or collect)
continue
key, _, value = line.partition(":") # split on the FIRST colon only
key = key.strip().lower()
value = value.strip()
if key:
current[key] = value # duplicate keys: last one wins
if current: # don't forget the final record
records.append(current)
return records
The "did you handle the last record?" trap. A very common bug here is forgetting to flush current after the loop ends, if the file doesn't end with a trailing blank line, the final record silently disappears. Interviewers love to test exactly this input. The if current: records.append(current) after the loop is doing real work.
Complexity Analysis
- Time: O(n), where n is the total length of the input, each line is processed once, with O(line length) splitting/trimming.
- Space: O(n) for the parsed records we return.
Part 2: Merging Many Files (~25 minutes)
Just as you finish, the interviewer smiles: "Turns out that CRM exported the data in several files, one per sync run, and the same customer can appear in more than one. Newer files have corrections. Can you merge them into one clean view, one record per customer?"
This is the realistic complication, and it turns a parsing problem into a data-reconciliation problem, which is squarely MongoDB's world (this is essentially an upsert / mongoimport --mode merge). Before coding, more questions surface, and again, state your assumptions:
- What makes two records "the same" customer? We'll assume a stable
idfield. Records without anidare skipped (or you could keep them as standalone, your call). - When two files disagree on a field, who wins? Files can arrive out of order, so "last file wins" is fragile. We'll assume each record carries an integer
version, and higherversionwins on a conflict (ties broken by file order). A field that only one file has is always kept (we union fields). - Are
idandversionpart of the output document? We'll treat them as control metadata: keepid, dropversionfrom the merged fields.
So merge_files(file_contents) takes a list of raw file strings (in order) and returns one merged record per id.
Example 2
Input (two files, in order):
file_1 = """
id: 1
name: John
email: john@old.com
version: 1
id: 2
name: Jane
version: 1
"""
file_2 = """
id: 1
email: john@new.com
phone: 555-1234
version: 2
id: 3
name: Bob
version: 1
"""
merge_files([file_1, file_2])
Output (one record per id):
[
{"id": "1", "name": "John", "email": "john@new.com", "phone": "555-1234"},
{"id": "2", "name": "Jane"},
{"id": "3", "name": "Bob"},
]
Trace customer id: 1. File 1 (version 1) sets name=John and email=john@old.com. File 2 (version 2) is newer, so its email=john@new.com overrides the old one, and its brand-new phone field is added. But name, which file 2 never mentions, is preserved from file 1. The result is the union of all fields, with the newest version winning every conflict. That's exactly upsert-merge semantics.
Solution - Part 2
We reuse parse_records from Part 1 to turn each file into records, then fold them together. For every id we keep two things: the merged document so far, and the highest version we've applied to it. For each incoming field:
- If the merged doc doesn't have that field yet → add it (fill the gap, regardless of version).
- If it already has it → overwrite only if the incoming record's version is ≥ the best version we've applied (newer wins; equal wins by file order, since we process in order).
That single rule, fill gaps always, overwrite only when newer-or-equal, gives us "union of fields, newest value wins."
def merge_files(file_contents: list[str]) -> list[dict]:
merged = {} # id -> merged record
best_version = {} # id -> highest version applied so far
for content in file_contents:
for record in parse_records(content): # reuse Part 1
if "id" not in record:
continue # no identity -> skip
rid = record["id"]
version = int(record.get("version", "0"))
if rid not in merged:
merged[rid] = {"id": rid}
best_version[rid] = -1 # so the first record always applies
target = merged[rid]
best = best_version[rid]
for key, value in record.items():
if key in ("id", "version"): # control metadata, not content
continue
if key not in target or version >= best:
target[key] = value # fill gap, or overwrite if newer/equal
best_version[rid] = max(best, version)
return list(merged.values())
Why "fill gaps regardless of version" is correct. A field that the winning (newest) version never mentions shouldn't vanish, older data is better than no data. So we always backfill missing fields, but we only overwrite an existing field when the incoming record is at least as new. This is precisely how MongoDB's merge/upsert keeps a document complete while letting the freshest write win each individual field.
Complexity Analysis
- Time: O(N), where N is the total size of all files combined. We parse every file once and touch each field once.
- Space: O(U), where U is the size of the merged output (one document per unique
id, holding the union of its fields).
Final Notes
We believe this type of open-ended, file/string-processing question is increasingly common at MongoDB (and Stripe). It rarely looks identical twice, the surface story changes (CRM dumps, log lines, config files, CSV exports), but the underlying skill is always the same: parse messy real-world text into clean structure, then reconcile it.
Here's how to actually win this kind of problem:
1. The first five minutes are clarifying questions, not code. We cannot stress this enough. The vagueness is the test. A candidate who immediately starts typing has failed the most important signal. A candidate who says "before I write anything, is the input a path or a string? Is this secretly a CSV? How should I treat junk lines?" has already separated themselves. MongoDB interviewers have repeatedly been described as happy to let you choose your own approach, as long as you justify it. Use that freedom out loud.
2. Reach for the standard library, and say why. If it turns out to be a real CSV, don't hand-roll comma splitting, use Python's csv module (or your language's equivalent). It handles quoted fields, embedded commas, and escaped newlines that will absolutely appear in real data and absolutely break a naive split(","). Knowing when not to write the parser yourself is senior-level signal. This is the core reason we recommend Python for these: str.strip, str.partition, csv, and json let you move fast and correctly.
3. Handle the ugly inputs before they're asked. Make your code run (remember, MongoDB uses a live compiler) and walk these out loud: an empty file, a file that's only comments, a file with no trailing newline (the classic "lost last record" bug), duplicate keys, a value that itself contains a :, and, for Part 2, the same id in three files with versions arriving out of order. Naming these unprompted is exactly the robustness MongoDB looks for.
4. Keep parsing and reconciling separate. Notice Part 2 simply reused parse_records. Resisting the urge to cram everything into one mega-function, keeping the "turn text into records" stage cleanly separate from the "merge records" stage, is the kind of decomposition that reads like production code. Interviewers notice clean seams.
Where this can go next (great things to offer if you finish early): stream the files line-by-line instead of loading them fully (so a 10 GB export doesn't blow up memory, very MongoDB), emit the result as newline-delimited JSON ready for mongoimport, collect rejected/malformed lines into an error report, or support a version-per-field model instead of per-record. Volunteering one of these is a strong closing signal.
The bottom line: this problem calls for the opposite instincts of a LeetCode sprint. Slow down, ask questions, pick a sane approach and defend it, lean on your standard library, and write clean, well-separated, runnable code. Do that, and you'll be writing the calm, pragmatic code MongoDB hires for.