InterviewLegend

The REST API Round

Alex Chen18 min readUpdated 197 real interview reports
In this lesson
  • Recognize Twilio's REST-API round as an HTTP and JSON task, and the three shapes it takes
  • Work the paginated-consume task the way the HackerRank assessment scores it
  • Design a batching client that flushes on a size cap or a maximum hold time

Twilio sells APIs. So its interview asks the obvious thing back: can you work one? That is the round almost nobody prepares for. It is not an algorithm puzzle. There is no clever invariant to spot, no Big-O to shave. You are handed an HTTP endpoint that returns JSON, and you have to call it, read what comes back, and turn it into an answer. Recruiters schedule it on HackerRank, usually in Java. It surfaces in the online assessment first, then again on the phone and in the onsite loop.

Three shapes travel under the same name, and they are not the same task. The first, and the one you are most likely to meet, hands you a live paginated endpoint and asks you to page through it and compute something. The second asks you to build a small REST API of your own, endpoints and all, often live with an interviewer at your shoulder. The third is a senior favorite: design a class that batches calls to an API and knows when to send them. Take them one at a time.

The Twilio angle
This is the job itself. Twilio is a CPaaS company: programmable messaging and voice, email through SendGrid, plus the Segment customer-data platform, all shipped as developer APIs on a Java and Spring stack. An engineer there spends the day calling other services and exposing new ones. So the round drops the algorithm theater and tests the thing you would do in week one. Read an API contract, handle the response you get back rather than the happy one you hoped for, and leave code another engineer can trust. That posture, more than any trick, is what moves you forward.

Shape one: consume a paginated API

Learn the canonical version by name, because it comes up again and again. You are given a medical-records service. Fetch every record for one doctor and one diagnosis, then return the average pulse across those records. The catch is in how the data arrives. One call does not return all of it.

The endpoint answers one page at a time. Ask for a page and you get a fixed slice of the results, plus a little metadata about the whole set. The field that matters is the page count.

json
{
"page": 1,
"per_page": 10,
"total": 47,
"total_pages": 5,
"data": [
{ "doctor": { "id": 2, "name": "Dr. Bostock" },
"diagnosis": { "id": 3, "name": "Pancreatitis", "severity": 1 },
"vitals": { "pulse": 114, "breathingRate": 32 } }
]
}

Read total_pages off the first response and you know the shape of the loop before you have seen the rest of the data. Page one says there are five pages in all. So you fetch page one, then keep going until you have pulled page five, gathering matches as you pass through each one. Miss that field and you solve the whole problem on a tenth of the data, which is the single most common way this round goes wrong.

The pagination loop

Everything the round tests lives in one loop. You fetch a page, fold its matches into a running total, and move to the next page until there are none left. Here it is in Java, which is how the assessment usually presents it.

java
// Average pulse across every record for one doctor and one diagnosis.
// The data is paginated, so we walk every page before we answer.
static double averagePulse(int doctorId, String diagnosisName) {
long pulseSum = 0;
int matches = 0;
int page = 1;
int totalPages = 1; // corrected by the first response
do {
MedicalPage response = fetchPage(page);
totalPages = response.total_pages;
for (Record r : response.data) {
boolean sameDoctor = r.doctor.id == doctorId;
boolean sameDiagnosis = diagnosisName.equals(r.diagnosis.name);
if (sameDoctor && sameDiagnosis) {
pulseSum += r.vitals.pulse;
matches++;
}
}
page++;
} while (page <= totalPages);
return matches == 0 ? 0.0 : (double) pulseSum / matches;
}

The loop reads the first page no matter what, which is what lets you learn total_pages before deciding how far to go. Each record is a plain filter. Same doctor, same diagnosis, add its pulse to the sum and count it. Once the page counter runs past the last page, the loop ends and you divide. Note the empty case. No matching records means no division by zero, so the function returns zero rather than blowing up.

java
// One page over HTTP, parsed straight into typed objects with Gson.
static MedicalPage fetchPage(int page) {
String url = "https://jsonmock.hackerrank.com/api/medical_records?page=" + page;
try {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("GET");
int status = conn.getResponseCode();
if (status != 200) {
throw new IOException("unexpected status " + status);
}
try (Reader reader = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)) {
return new Gson().fromJson(reader, MedicalPage.class);
}
} catch (IOException e) {
throw new UncheckedIOException(e); // surface it; do not swallow a failed call
}
}
// Gson fills these straight from the JSON field names.
class MedicalPage { int page, per_page, total, total_pages; List<Record> data; }
class Record { Doctor doctor; Diagnosis diagnosis; Vitals vitals; }
class Doctor { int id; String name; }
class Diagnosis { int id; String name; int severity; }
class Vitals { int pulse; int breathingRate; }

Two details separate a clean submission from a shaky one. Gson maps JSON onto typed objects when the field names line up, so the model classes are named to match the payload exactly, total_pages and all. And the network can fail. A non-200 status or a dropped connection should not be swallowed and quietly turned into a wrong answer, so this code raises instead of returning empty-handed. Interviewers notice which way you jump there.

The pagination loop is the entire skill on display. Read the page count from the first response, walk from page one to the last, and accumulate as you go. Everything else, the HTTP call and the JSON mapping, is plumbing around that loop.

  • HTTP client: call it yourself. The data does not arrive as a function argument. You open the connection, set the request method, then read the stream back, usually with HttpURLConnection or a light client.
  • The loop: read the count, page through. The page count is the whole game. Fetch page one, learn how many pages there are, and keep fetching until you have them all.
  • Deserialize: JSON to objects. Map the payload onto typed classes with Gson or Jackson so field access stays readable. Naming the classes to match the JSON is half the work.
  • Filter and total: the actual question. Only now do you do the thing the prompt asked: keep the records that match, then fold them into a running sum or count or max.
  • Failure paths: do not swallow them. A bad status or a broken read should surface, not become a silent wrong answer. Guard the empty result too, so an average never divides by zero.
There is no tidy LeetCode page for this, and that is why it filters. To rehearse, point your code at a real paginated API and page through it for a total or an average. The public jsonmock endpoints that HackerRank itself uses are the closest thing to the real prompt, and any open REST API with a page count will build the same muscle.

Shape two: build a CRUD REST API

The second shape flips the round around. Instead of consuming an API you design and build one, usually live with an interviewer watching. A common prompt asks you to expose CRUD operations over a domain, say a stock-price service: create a record, read it back, update it, and delete it. A related version has you model a booking platform down to its endpoints, the classes behind them, and the enums that pin down each state. Segment runs a pairing round in this spirit, where you sketch a chatbot service and then implement it through REST calls on the spot.

They are watching for the instincts a REST API lives or dies by. The strongest candidates reach for these without being told.

  • Resources, not verbs: model the nouns. Shape the surface around resources like messages and accounts, and let the HTTP verbs do the acting. A GET reads, a POST creates, a PATCH edits. Endpoints that read like function calls invite a follow-up.
  • Status codes: say what happened. A created row is 201, a bad request is 400, a missing one is 404, a conflict is 409. Returning 200 with the real error hidden in the body is the classic tell of a rushed design.
  • Idempotency: safe to retry. Networks drop responses and clients retry, so a repeated create must not send a second message. Accept an idempotency key and make the retry return the original result.
  • Pagination: bound every list. You just felt why on the consume side. Any endpoint that lists things needs a page or a cursor, so no single call promises to hand back everything at once.
  • Versioning: plan for change. Twilio pins versions in the path so a contract can move without breaking callers. Say how you would evolve a field without a flag day for everyone who depends on it.
The Twilio angle
Treat Twilio's own API as the reference sitting right in front of you. It is resource-oriented to the core, versioned in the path, and built so a retried request never double-sends a text. That last point matters here. A duplicate POST that fires a second SMS is a real charge and a real message to a real phone. That's why idempotency earns more credit in this round than a clever data structure. Design the unhappy path first and you are designing the way Twilio's platform has to behave.

Shape three: design an API client that batches

The senior version of the round is the most interesting, and it reads almost word for word the same every time. Design a class that batches requests to an endpoint. Send the batch once it fills to a maximum size. But no single request may wait forever, so if the oldest one has been held for its maximum duration, send what you have right away. The conversation usually turns to threads. Hold that thought, because there is a cleaner way to get the logic right first.

Two rules fire the send, and they race each other. Size is the easy one: the batch hits its cap and goes. Time is the subtle one: even a half-full batch has to leave the moment its oldest request reaches the age limit, or that caller waits too long. A real service would drive the timer from a background thread. To get the behavior provably correct, though, do not reach for a clock at all. Pass the current time into every call and the whole thing turns deterministic, easy to reason about and easy to test.

One buffer, two triggers. A request enters and either fills the batch to its size cap and flushes at once, or it waits. A separate check asks whether the oldest waiting request has aged past the limit, and if it has, everything held goes out together. Model those two triggers cleanly and the concurrency story becomes a wrapper around correct logic, instead of a substitute for it.

Practice: the request batcher

The class below is stripped to the part the interview grades. Time is an argument, not a wall clock, so the batcher is fully deterministic. add records a request at a given moment and returns the batch if it just filled, otherwise nothing leaves. tick is the timer check: hand it the current time and it flushes only when the oldest held request has waited its limit.

1

The Request Batcher

MediumFrequently asked
Problem
Design a RequestBatcher that collects outbound API requests and decides when to send them, the way Twilio's senior coding round poses it. Every method is handed the current time, so the class never reads a real clock and behaves the same way on every run.

RequestBatcher(max_size, max_wait) builds an empty batcher. It flushes once a batch reaches max_size requests, and it never holds a request longer than max_wait units of time.

add(request, now) records request as arriving at time now and appends it to the current batch. If the batch has now reached max_size, it flushes: return the list of held requests in arrival order and start a new empty batch. Otherwise return an empty list.

tick(now) is the time check. If at least one request is waiting and the oldest has been held for max_wait or more, flush and return the whole held batch in arrival order. Held for max_wait means now minus its arrival time is at least max_wait. Otherwise return an empty list.

A flush from either method empties the batcher, so the age clock effectively restarts from the next request that arrives.

Input encoding. Each test case is a JSON array of operations. The first is ["RequestBatcher", [max_size, max_wait]]; the rest name a method and its arguments. Every operation records its return value: the constructor records null, while add and tick record the list they return, which is empty when nothing flushes.

Input: RequestBatcher(3, 100); add("r1", 0); add("r2", 0); add("r3", 0)

Output: [null, [], [], ["r1", "r2", "r3"]]

The size cap is three and the wait limit is a distant hundred. The first two adds return an empty list, because the batch is still filling. The third add makes three, hits the cap, and flushes the whole batch in arrival order. The wait limit never comes into play.

Input: RequestBatcher(10, 10); add("a", 0); tick(5); tick(10)

Output: [null, [], [], ["a"]]

Here the size cap is a roomy ten, so one add fills nothing. At tick(5) the oldest request has waited only five, under the limit, so nothing goes out. At tick(10) it has waited ten, which meets the limit, so it flushes.

How this uses the pattern

Hold the waiting requests in a list tagged with arrival time, oldest at the front. add appends and checks the size cap; tick checks whether the front request has aged past max_wait. Route both through one small flush that empties the buffer and returns what left.

The Request Batcher

Loading...
Click "Run" to execute your code

Test Cases

Input: [["RequestBatcher",[3,100]],["add",["r1",0]],["add",["r2",0]],["add",["r3",0]]]Expected: [null,[],[],["r1","r2","r3"]]
Input: [["RequestBatcher",[10,10]],["add",["a",0]],["tick",[5]],["tick",[10]]]Expected: [null,[],[],["a"]]
Input: [["RequestBatcher",[3,10]],["tick",[5]],["tick",[100]]]Expected: [null,[],[]]
Input: [["RequestBatcher",[5,10]],["add",["a",0]],["add",["b",3]],["tick",[8]],["tick",[12]]]Expected: [null,[],[],[],["a","b"]]
Input: [["RequestBatcher",[2,10]],["add",["a",0]],["add",["b",1]],["tick",[11]],["add",["c",11]],["tick",[20]]]Expected: [null,[],["a","b"],[],[],[]]
Input: [["RequestBatcher",[4,5]],["add",["x",0]],["tick",[5]],["add",["y",6]],["add",["z",6]],["tick",[10]],["tick",[11]]]Expected: [null,[],["x"],[],[],[],["y","z"]]
Input: [["RequestBatcher",[3,100]],["add",["a",0]],["tick",[1]],["add",["b",2]],["add",["c",3]]]Expected: [null,[],[],[],["a","b","c"]]
The Twilio angle
Twilio lives behind rate limits, so a client that batches outbound calls under both a size cap and a latency ceiling is close to real infrastructure rather than a toy. The senior round states it almost verbatim: fill to a maximum, but never hold a request past its deadline. In the room it usually arrives wrapped in talk of threads. Ask whether you can model time as an argument first, prove the flush logic correct, then describe what a lock and a background flusher would add on top. Clean code with every edge case handled is what scores here, well above a concurrency flourish.
Take it further
Now make it thread-safe. In a live service, many threads call add at once while a scheduled timer fires tick on an interval, so the buffer needs a lock and the time-based flush needs something to drive it. Describe where the lock goes, how the background flusher wakes up, and how you keep a request from slipping past its deadline while another thread holds the lock. The logic you just wrote does not change. It gains a guard around it.

Preparing for a round with no LeetCode page

None of these three shapes has a problem set to grind, which is the whole reason they screen so well. The good news is that each one rehearses with tools you already have.

  • For the consume shape, point real code at a public paginated API and compute a total or an average across every page. Force yourself to read the page count from the first response rather than hard-coding it.
  • For the build shape, take a domain you know cold and design its endpoints on paper: the resources, the status codes, the retry story. Then stand up a tiny version and hit it with a duplicate request to watch what idempotency has to catch.
  • For the batching shape, write the deterministic version above until the edge cases are reflex, then say out loud how a lock and a background flusher would wrap it. Narrating that layer is most of the senior signal.

Key Takeaways

  • ✓Twilio's REST-API round tests whether you can work an HTTP API, not whether you can crack an algorithm. It travels in three shapes: consume a paginated endpoint, build a small CRUD API, and design a client that batches calls.
  • ✓The consume shape is the common one, and the whole skill is the pagination loop. Read the page count off the first response, walk from the first page to the last, and accumulate matches as you go, then guard the empty case so an average never divides by zero.
  • ✓The build shape rewards REST instincts over cleverness. Model resources rather than verbs, return honest status codes, and make writes idempotent, because a retried POST that double-sends an SMS is a real charge on a real phone at Twilio.
  • ✓The batching class is the senior variant. Flush on a size cap or on the oldest request aging past its limit. Model time as an argument to keep it deterministic. Threads are a lock-and-timer shell around logic you've already proven correct.
Had your Twilio interview?Tell us what they asked. Reviewed before it goes in, never with your name.
Twilio interview guide

Know what Twilio asks before you walk in.

9 coding · 3 system design · 18 behavioral questions

Get Premium$49 a year · 30-day money-back guarantee
Or start freeNo card.

Get their questions first.

InterviewLegend is independent and not affiliated with or endorsed by any company it covers.