MongoDB Peer-Programming: Reviewing a Pull Request
🗣️ About this round, and why it matters more than you might expect. Unlike the first peer-programming question (where you build a feature on a repository you were given to set up in advance), this one tends to show up far more often, and across many more MongoDB teams. We believe the "review and refactor an open pull request" format is one of the most frequently used exercises in MongoDB's onsite loop, for a straightforward reason: it is the most conversational interview they run. A code review is a two-way discussion by nature, so it gives two interviewers the richest possible window into how you think, what you prioritise, and how you treat a teammate's work.
Two things make it different from Question 1:
- You do not get the repository in advance. It is shared with you live, at the start of the call. You have to open an unfamiliar codebase cold and orient yourself fast.
- The job is not to add code, it is to judge code. You are reviewing an open PR as if a colleague said, "can you take a look before I merge?" They want to see how quickly you understand the change, which problems you catch, how good your suggested fixes are, and how clearly and kindly you communicate them.
How this round actually works
Here is the shape candidates tend to report:
- Timing: plan for roughly 60 minutes of review, after a 10 to 15 minute introduction (introductions, a little setup, and getting oriented). It is shorter than a build-a-feature round, so the pace and prioritisation of your feedback matter.
- The interviewer shares a small but real service, often in Java. MongoDB runs a lot of backend services and owns the official MongoDB Java driver, so a Java service over a MongoDB collection is a natural choice. You clone it, or they share their screen.
- There is one open pull request. It compiles. It probably even works for the happy path. Your job is to review it before it merges.
- For the first few minutes you read: the project, then the PR. Narrate as you go. They want to hear your first impressions of unfamiliar code.
- Then you walk the PR the way you would on GitHub: "here is something I would flag, here is why, here is what I would suggest instead." They push back, ask "what happens if...", and discuss. That back-and-forth is the interview.
- If you finish with time to spare, they often ask you to apply one of your fixes, or to add a small feature on top. More on that at the end.
📋 A note on the language: you will not be surprised by it. For the peer-programming rounds, the preferred language is conveyed to you well in advance, usually stated in the job description and confirmed by your recruiter. Sometimes the recruiter asks which language you would prefer; more often the team has a fixed stack (here, Java with the MongoDB Java driver) and tells you ahead of time so you can prepare. We use Java for this walkthrough for that reason, but the review skills below are the same whatever the language turns out to be. Either way, you walk in knowing what you will be working in.
The bar is not "find every nit." It is: do you catch the issues that actually matter (correctness, data safety, performance) before the cosmetic ones, do your fixes show real understanding of Java and of MongoDB, and are you a reviewer people would want on their team? A reviewer who opens with "the indentation looks off" while missing a query that loads an entire collection into memory has lost the round, even if the nit is correct.
The codebase you're handed
Take a breath and read before you react. This is a tiny Spring Boot service over a MongoDB movies collection (picture the sample_mflix dataset). Click through it:
Explorer
1package com.example.catalog;2 3import com.mongodb.client.MongoCollection;4import org.bson.Document;5import org.bson.types.ObjectId;6 7import java.util.Optional;8 9import static com.mongodb.client.model.Filters.eq;10 11public class MovieService {12 13 private final MongoCollection<Document> movies;14 15 public MovieService(MongoCollection<Document> movies) {16 this.movies = movies;17 }18 19 public Optional<Movie> findById(String id) {20 Document doc = movies.find(eq("_id", new ObjectId(id))).first();21 if (doc == null) {22 return Optional.empty();23 }24 return Optional.of(toMovie(doc));25 }26 27 static Movie toMovie(Document doc) {28 Movie movie = new Movie();29 movie.setId(doc.getObjectId("_id").toHexString());30 movie.setTitle(doc.getString("title"));31 movie.setYear(doc.getInteger("year", 0));32 movie.setRating(doc.getDouble("rating"));33 return movie;34 }35}How to orient yourself in about three minutes, out loud:
- Find the entry point and the shape.
CatalogApplicationis a stock Spring Boot app,MovieControllerexposes/movies,MovieServicetalks to aMongoCollection<Document>through the MongoDB Java driver, andMovieis the model. That is the whole app. - Read the existing
MovieServiceclosely, because it sets the house style. Notice the patterns the team already uses:Filters.eqfrom the driver's builders (not hand-builtDocumentqueries),Optionalfor "might not be there," and a sharedtoMovie(Document)mapper. Strong reviewers anchor on the existing conventions, because the most useful feedback is often "there is already a pattern for this in the file." - Glance at the test.
MovieServiceTestactually asserts on the mapper. That tells you the team expects tests to assert, not just run.
Then say the one-liner: "So it is a thin Spring Boot service: a controller, a service that wraps a MongoDB collection with the Java driver, a model, and a unit test. The house style is driver builders, Optional, and a shared mapper." Now you are ready to review.
The pull request
Here is the change you have been asked to review. The author's description:
PR #142: Add movie title search
Adds a
GET /movies/searchendpoint so the frontend can search movies by title, paginated ten at a time. Tested locally against my Atlas cluster, works great. 🎬
The diff is below: new lines are highlighted, and the three changed files carry a dot in the tree.
Explorer
1package com.example.catalog;2 3import com.mongodb.client.FindIterable;4import com.mongodb.client.MongoCollection;5import com.mongodb.client.MongoCursor;6import org.bson.Document;7import org.bson.types.ObjectId;8 9import java.util.ArrayList;10import java.util.List;11import java.util.Optional;12 13import static com.mongodb.client.model.Filters.eq;14 15public class MovieService {16 17 private final MongoCollection<Document> movies;18 19 public MovieService(MongoCollection<Document> movies) {20 this.movies = movies;21 }22 23 public Optional<Movie> findById(String id) {24 Document doc = movies.find(eq("_id", new ObjectId(id))).first();25 if (doc == null) {26 return Optional.empty();27 }28 return Optional.of(toMovie(doc));29 }30 31 // Added in this PR: search movies by title, 10 per page.32 public List<Movie> searchByTitle(String title, int page) {33 try {34 Document query = new Document("title",35 new Document("$regex", ".*" + title + ".*"));36 37 FindIterable<Document> found = movies.find(query);38 39 List<Movie> results = new ArrayList<Movie>();40 MongoCursor<Document> cursor = found.iterator();41 while (cursor.hasNext()) {42 Document doc = cursor.next();43 Movie m = new Movie();44 m.setId(doc.getString("_id"));45 m.setTitle(doc.getString("title"));46 m.setYear(doc.getInteger("year"));47 m.setRating(doc.getDouble("rating"));48 results.add(m);49 }50 51 int from = page * 10;52 int to = from + 10;53 if (to > results.size()) {54 to = results.size();55 }56 return results.subList(from, to);57 } catch (Exception e) {58 e.printStackTrace();59 return null;60 }61 }62 63 static Movie toMovie(Document doc) {64 Movie movie = new Movie();65 movie.setId(doc.getObjectId("_id").toHexString());66 movie.setTitle(doc.getString("title"));67 movie.setYear(doc.getInteger("year", 0));68 movie.setRating(doc.getDouble("rating"));69 return movie;70 }71}Before you comment on any single line, do this:
- Read the description for intent. The author wants title search with pagination. Reasonable feature. Now you know what "correct" should look like.
- Build the mental model. The new method builds a regex query on
title, pulls every match through a cursor, maps each document to aMovie, then slices out a page. Four steps. Hold them in your head. - Run it in your mind on a realistic input. Picture searching
"the"against a collection of 100,000 movies. What actually happens? That single question surfaces the most important issue in the whole PR.
Say your headline first. Before the line-by-line, give them the summary a good reviewer leads with: "Nice, the feature works and the endpoint is wired up correctly. I have one performance issue I think is blocking, a couple of correctness and robustness things I would want fixed, and then some small style notes. Want me to walk through them worst-first?" That one sentence shows prioritisation, sets a collaborative tone, and is exactly what they are listening for.
How to run the review: lenses, in priority order
Strong reviewers do not read top to bottom and comment on whatever catches their eye. They pass over the diff several times, each pass through a different lens, most important first:
- Correctness. Does it do the right thing, including on empty results, missing fields, and odd inputs?
- Data safety and security. Untrusted input reaching the query, injection, resource leaks.
- Performance and scale. How does this behave on a large collection? This is the MongoDB-flavoured lens, and the one they care about most.
- API and design. Does it fit the existing patterns and the REST contract?
- Tests. Do they actually prove the change works?
- Style and polish. Naming, constants, idioms. Last, and lightest.
Now let's walk PR #142 through those lenses.
🔴 Blocking: raise these first
1. It loads the whole result set into memory, then paginates
This is the one to lead with. Look at the order of operations:
FindIterable<Document> found = movies.find(query);
// ...map every matching document into `results`...
int from = page * 10;
int to = from + 10;
return results.subList(from, to);
The pagination happens in application memory after every matching document has already been pulled out of MongoDB. On sample_mflix that is harmless. On a real collection, a search for "the" could drag hundreds of thousands of documents across the network and into the JVM heap just to hand back ten of them.
What you would say: "My one blocking comment is the pagination. We are fetching every match into a list and then slicing a page out of it in memory, so the cost scales with how many movies match, not with the page size. I would push skip and limit into the query and let MongoDB do the paging."
The fix:
movies.find(filter)
.skip(page * PAGE_SIZE)
.limit(PAGE_SIZE)
.iterator();
A senior bonus catch, for free: the in-memory version also has a latent crash. results.subList(from, to) throws IndexOutOfBoundsException when from is past the end of the list, so asking for page 5 of a 12-result search blows up. Moving paging into the query makes that bug disappear too, an out-of-range page simply returns an empty list.
2. It builds a regex out of raw user input
Document query = new Document("title",
new Document("$regex", ".*" + title + ".*"));
There are three problems stacked in that one line, and a strong candidate peels all three:
- The input is not escaped. Whatever the user types is spliced straight into a regular expression. Regex metacharacters change the meaning of the query, and a pathological input can trigger catastrophic backtracking (a ReDoS) that pins a CPU on the server. Wrap the input with
Pattern.quote(...). - It cannot use an index. A leading
.*makes this an unanchored substring scan, so MongoDB cannot use a normal index ontitleand falls back to a collection scan. For real search you want a text index ($text) or Atlas Search, which is MongoDB's purpose-built full-text engine. Worth saying out loud even if you do not implement it. - It is case-sensitive. Searching
"Matrix"will not find"the matrix". Add the case-insensitive option.
What you would say: "This regex is built by string concatenation from user input. I would at least escape it with Pattern.quote and make it case-insensitive. But I would also flag that a leading-wildcard regex can't use an index, so at scale the right tool here is a text index or Atlas Search. Can we talk about whether this needs to be true substring search or whether prefix search would do?"
The fix (pragmatic version):
Bson filter = Filters.regex("title", Pattern.quote(title), "i");
3. It swallows the exception and returns null, and the test hides it
These two are listed together on purpose, because they combine into something worse than either alone.
} catch (Exception e) {
e.printStackTrace();
return null;
}
Catching Exception swallows everything, including bugs that should be loud, logs to stderr with printStackTrace instead of a real logger, and then returns null. The controller hands that null straight back, so the endpoint answers 200 OK with an empty body, or a caller downstream hits a NullPointerException far from the real cause.
Now look at the test that shipped with the PR:
@Test
void testSearch() {
MovieService service = new MovieService(null);
List<Movie> results = service.searchByTitle("matrix", 0);
System.out.println(results);
}
It constructs the service with a null collection, so the very first line inside searchByTitle throws a NullPointerException. That exception is swallowed by the catch, the method returns null, and the test prints null and passes. The method is completely broken, and the test is green. That is the single most important thing to point out in the whole review: the swallowed exception and the assertion-free test conspire to make a broken method look healthy.
What you would say: "Two things that I think are actually the same problem. The catch block swallows every exception and returns null, which a search method should never do, it should return an empty list or let the error propagate. And the new test has no assertions and passes a null collection, so it would stay green even if the method always failed. I would have the method return an empty list, drop the catch-all, and make the test assert on real results."
The fix: delete the catch-all (let genuine failures surface, or wrap them with context), and return an empty list, never null:
if (title == null || title.isBlank()) {
return List.of();
}
🟠 Should-fix: clearly worth changing
4. The cursor is never closed
MongoCursor<Document> cursor = found.iterator();
while (cursor.hasNext()) { /* ... */ }
A MongoCursor holds a server-side cursor and a connection. If anything throws mid-loop, or even on the happy path here, it is never closed, which leaks resources under load. MongoCursor is Closeable, so wrap it in try-with-resources:
try (MongoCursor<Document> cursor = movies.find(filter)./*...*/.iterator()) {
while (cursor.hasNext()) {
results.add(toMovie(cursor.next()));
}
}
5. It reinvents the existing mapper, and reintroduces a bug
The PR maps documents to Movie by hand:
m.setId(doc.getString("_id"));
m.setYear(doc.getInteger("year"));
But MovieService already has a toMovie(Document) helper three methods up, and the hand-rolled copy is subtly wrong: _id is an ObjectId, not a String, so doc.getString("_id") returns null, and getInteger("year") with no default unboxes to a NullPointerException if year is ever missing. The existing toMovie already handles both correctly.
What you would say: "There is already a toMovie helper in this file that does this mapping, and it handles the ObjectId id and a missing year properly. Reusing it removes this duplication and also fixes the _id mapping bug here."
6. Paginating without a sort gives unstable pages
find(...).skip(...).limit(...) with no sort relies on MongoDB's natural order, which is not guaranteed to be stable between calls. The same movie can appear on page 1 and page 2, or be skipped entirely, as data changes. Pagination needs a deterministic sort key:
.sort(Sorts.ascending("title"))
7. No input validation
There is no guard for a null or blank title, or a negative page. A blank title should return an empty list (or everything, but decide deliberately), and a negative page should be clamped. The isBlank() guard above covers the title; Math.max(page, 0) covers the page.
🟡 Polish: mention briefly, do not dwell
These are the kind of notes you raise quickly and explicitly mark as minor, so the author knows they are not blocking:
- Magic number.
10is hard-coded and appears twice. Pull it into aprivate static final int PAGE_SIZE. - Diamond operator.
new ArrayList<Movie>()can benew ArrayList<>(). - Endpoint ergonomics. In the controller,
@RequestParam int pageis required, so a caller who omitspagegets a 400. AdefaultValue = "0"is friendlier and matches how the rest of the API behaves. The existinggetByIdreturns aResponseEntity; returning a bareListfor search is acceptable (an empty list plus 200 is a fine "no results"), but it is worth a sentence acknowledging the inconsistency.
The refactor, all together
Pull the blocking and should-fix items together and the change becomes small, safe, and fast. Here is the version a strong candidate lands on (changed lines highlighted):
Explorer
1package com.example.catalog;2 3import com.mongodb.client.MongoCollection;4import com.mongodb.client.MongoCursor;5import com.mongodb.client.model.Filters;6import com.mongodb.client.model.Sorts;7import org.bson.Document;8import org.bson.conversions.Bson;9import org.bson.types.ObjectId;10 11import java.util.ArrayList;12import java.util.List;13import java.util.Optional;14import java.util.regex.Pattern;15 16import static com.mongodb.client.model.Filters.eq;17 18public class MovieService {19 20 private static final int PAGE_SIZE = 20;21 22 private final MongoCollection<Document> movies;23 24 public MovieService(MongoCollection<Document> movies) {25 this.movies = movies;26 }27 28 public Optional<Movie> findById(String id) {29 Document doc = movies.find(eq("_id", new ObjectId(id))).first();30 if (doc == null) {31 return Optional.empty();32 }33 return Optional.of(toMovie(doc));34 }35 36 public List<Movie> searchByTitle(String title, int page) {37 if (title == null || title.isBlank()) {38 return List.of();39 }40 int skip = Math.max(page, 0) * PAGE_SIZE;41 42 Bson filter = Filters.regex("title", Pattern.quote(title), "i");43 44 List<Movie> results = new ArrayList<>();45 try (MongoCursor<Document> cursor = movies.find(filter)46 .sort(Sorts.ascending("title"))47 .skip(skip)48 .limit(PAGE_SIZE)49 .iterator()) {50 while (cursor.hasNext()) {51 results.add(toMovie(cursor.next()));52 }53 }54 return results;55 }56 57 static Movie toMovie(Document doc) {58 Movie movie = new Movie();59 movie.setId(doc.getObjectId("_id").toHexString());60 movie.setTitle(doc.getString("title"));61 movie.setYear(doc.getInteger("year", 0));62 movie.setRating(doc.getDouble("rating"));63 return movie;64 }65}Notice what the refactor does and does not do. It changes only what the PR touched: findById and the existing toMovie are left exactly as they were. That restraint is itself a senior signal. Reviewers who rewrite the whole file lose trust, the discipline is to fix the change under review, not to redecorate.
What each change bought you:
- Paging in the query (
skip+limit): cost now scales with page size, not collection size, and the out-of-range crash is gone. Filters.regex(..., Pattern.quote(title), "i"): input is escaped, search is case-insensitive, and you have flagged the index conversation for later.- try-with-resources: the cursor is always closed.
- Reusing
toMovie: less code, and the_idandyearbugs vanish. - A real test that asserts on the validation path, instead of one that prints and always passes.
Delivering the review like a teammate, not a gatekeeper
The fixes are half the score. How you deliver them is the other half, and it is where this round really separates people. A few habits that read as senior:
- Lead with a summary and something genuine you liked. "The feature works and the wiring is clean" costs you nothing and buys a lot of goodwill.
- Order by severity, and label it. Say which comments are blocking, which are should-fix, and which are nits, so the author knows where to spend their attention.
- Frame as questions and suggestions, not commands. "What do you think about pushing the paging into the query?" lands far better than "this is wrong." You are reviewing the code, not the person.
- Explain the why, with the blast radius. "This matters because on a big collection it pulls everything into memory" teaches; "don't do this" does not.
- Offer to pair. "Happy to jump in and apply the paging fix together if that is useful" signals you are a collaborator.
Watch the clock and stay in conversation. This is a discussion, not a monologue. Pause after each blocking item and let them react, they will often steer you toward what they most want to talk about. If they go quiet, ask: "Want me to keep going through the smaller stuff, or focus on getting the paging fix in?"
If there's time: the small feature add-on
Candidates who handle the review well are often asked, with the time that remains, to apply one of the fixes for real or to add a small feature on top. This is a bonus on top of the review and refactor, not the main event, but it is common enough to rehearse. A typical ask here would be: "Now let users filter the search by a minimum rating."
It is a small, natural extension of the method you just cleaned up. Add a parameter, add one more clause to the filter with the driver's builders, and (a nice touch) sort by rating so the best matches come first:
public List<Movie> searchByTitle(String title, int page, double minRating) {
if (title == null || title.isBlank()) {
return List.of();
}
int skip = Math.max(page, 0) * PAGE_SIZE;
Bson filter = Filters.and(
Filters.regex("title", Pattern.quote(title), "i"),
Filters.gte("rating", minRating));
List<Movie> results = new ArrayList<>();
try (MongoCursor<Document> cursor = movies.find(filter)
.sort(Sorts.descending("rating"))
.skip(skip)
.limit(PAGE_SIZE)
.iterator()) {
while (cursor.hasNext()) {
results.add(toMovie(cursor.next()));
}
}
return results;
}
And expose it on the endpoint with a sensible default so existing callers keep working:
@GetMapping("/search")
public List<Movie> search(
@RequestParam("q") String query,
@RequestParam(name = "page", defaultValue = "0") int page,
@RequestParam(name = "minRating", defaultValue = "0") double minRating) {
return movieService.searchByTitle(query, page, minRating);
}
Talk through the trade-off as you go: "Filters.and keeps this readable, and now that we filter and sort on rating, this is the point where I would add a compound index on title and rating so the query and the sort can both be served by the index." That single sentence ties the feature back to the performance lens and shows you think about MongoDB, not just Java.
What earns a Strong Hire
✅ Strong-Hire signals
- You orient fast and out loud. A clear one-sentence summary of an unfamiliar codebase inside a few minutes.
- You prioritise. Blocking issues first, nits last, and you say which is which.
- You catch what matters. The in-memory paging, the unescaped regex, the swallowed exception and the test that hides it. Performance and data-safety instincts on a MongoDB query are exactly what they are probing.
- Your fixes are idiomatic. Driver builders, try-with-resources, reusing the existing helper, returning empty over null.
- You speak MongoDB.
skip/limitin the query, text indexes versus regex, a compound index for the filter and sort. - You review the code, not the coder. Kind, specific, question-shaped feedback, and a word of genuine praise.
🚩 No-Hire signals to avoid
- Opening with style nits while the collection-scan query goes unmentioned.
- Reading silently, then delivering a verdict with no discussion.
- Rewriting the entire file, or "improving" code the PR never touched.
- Spotting the swallowed exception but not connecting it to the always-green test.
- Harsh or vague feedback: "this is bad" with no why and no suggestion.
Your game plan, start to finish
- Orient (about 3 minutes). Entry point, the one service, the model, the test. Say the one-liner.
- Read the PR for intent, build the four-step mental model, and run it in your head on a big collection.
- Give your headline, then review worst-first: correctness and safety, then performance, then design and tests, then nits.
- Discuss, do not lecture. Pause after each blocking item. Frame as questions. Praise what is good.
- If time allows, apply a fix or add the small feature, and tie it back to indexing.
This round is a direct read on the teammate MongoDB wants: someone who reads unfamiliar code quickly, knows what actually breaks at scale, fixes it cleanly in the language and the database, and makes their teammates better in the process. Review a few PRs like this one out loud until the worst-first instinct is automatic, and you will walk in ready. 🚀