Razorpay Lead SDE Machine Coding interview

Anonymous User
552

I went through the machine coding round at Razorpay for a Lead Software Engineer position and wanted to share my experience. Hope this gives you a clear picture of what to expect.

Round: Machine Coding Round (Virtual)
Duration: 2 Hours
Platform: I was allowed to use my own local IDE and share my screen. The interviewer was present throughout, observing and occasionally asking questions.


The Problem Statement

The problem was to design and implement a persistent (in-memory) Pub/Sub queue system. The requirements were very clear and provided upfront:

Implement a persistent (in memory) pub-sub queue mechanism with guaranteed delivery of every published message for all the subscribed consumers in that subscribed topic in the same order.

Features:

  1. Expose topics.
  2. Publisher to push messages against a topic.
  3. Subscribe and unsubscribe from a topic.
  4. Subscriber to consume from a topic. A topic can have multiple subscribers.
  5. Maintain the state of consumption of each message in each topic for each consumer.
  6. Maintain order of message consumption for each consumer.
  7. Filtered message consumption in a topic by the consumer.
  8. Handle cases of consumers not being available. What to do with those messages?

My Approach and Thought Process (First 20-25 mins)

This is the most critical part. Before writing a single line of code, I dedicated time to clarifying requirements and laying out a solid design.

1. Requirement Clarification (Asking the "Right" Questions):

I started by asking clarifying questions to define the scope:

  • Message Format: "What is the expected format of a message? Is it a simple string, or a structured object with metadata?" (Interviewer said a string payload is fine, but metadata for filtering is a good idea).
  • Filtering Logic: "For filtered consumption, what's the complexity? Should it be simple key-value matching on metadata, or something more complex like regex or custom functions?" (Interviewer suggested starting with simple key-value matching).
  • Subscriber Unavailability: "For requirement #8, if a consumer is unavailable, should its messages be held indefinitely, or is there a TTL or memory constraint I should consider?" (Interviewer confirmed that for this round, holding them indefinitely in memory is the expected behavior, ensuring guaranteed delivery).
  • Concurrency: "Should I assume a single-threaded environment, or do I need to make the system thread-safe?" (Interviewer said to start with a single-threaded implementation but be prepared to discuss concurrency).

2. Low-Level Design (LLD) on a Scratchpad:

I sketched out the main components and data structures. Communicating this design to the interviewer is key.

  • Message: A simple struct to hold a payload (std::string) and metadata (std::map<std::string, std::string>).
  • Topic: The core component. I decided it should manage its own state.
    • std::vector<Message> messages: To store all messages in the order they were published. A vector provides O(1) access by index, which is perfect for our consumption logic.
    • std::unordered_map<SubscriberId, size_t> subscriber_offsets: This was the crux of the solution for features #5, #6, and #8. It maps each subscriber's ID to the index (offset) of the next message they are due to receive. This elegantly handles individual consumption state and ensures that an unavailable consumer can pick up right where they left off.
    • std::unordered_map<SubscriberId, Filter> subscriber_filters: To store the filter criteria for each subscriber on this topic.
  • PubSubService: The main service class acting as the entry point.
    • std::unordered_map<TopicId, Topic> topics_: A map to hold all created topics for fast lookup.
    • This class would expose the public API (createTopic, publish, subscribe, consume, etc.).

I explicitly chose unordered_map for lookups due to its average O(1) time complexity, which is ideal for performance.


The Coding Phase (Approx. 80-90 mins)

My strategy was to build a working, simple solution first, and then layer on complexity if time permitted. I chose to implement the simpler version we discussed, initially skipping the filtering to ensure the core pub/sub logic was flawless.

  1. File Structure: I created three files to demonstrate modularity: pubsub.h, pubsub.cpp, and main.cpp. This keeps the design clean and is good practice for a senior role.

  2. Core Logic Implementation:

    • publish(topicId, payload): This was straightforward. Find the topic in the map and push_back the new message into the topic's messages vector.
    • subscribe(subscriberId, topicId): Find the topic. Add the subscriberId to the subscriber_offsets map. The initial offset was set to topic.messages.size(), meaning the subscriber will only get new messages published after they subscribe.
    • consume(subscriberId, topicId): This is the most important method.
      1. Find the topic and locate the subscriber's current offset in the subscriber_offsets map.
      2. Check if offset < topic.messages.size().
      3. If yes, retrieve the message at that index.
      4. Crucially, increment the subscriber's offset (offset++) in the map.
      5. Return the message.
      6. If no, it means the subscriber is up-to-date, so return an empty/null value (std::optional is great for this).
  3. Driver Code (main.cpp): I spent about 10-15 minutes writing a comprehensive main function. Do not skip this step! It proves your code works. I created multiple topics and subscribers, published messages, and had each subscriber consume messages to demonstrate:

    • Correct ordering.
    • That each subscriber maintains their own independent progress.
    • That an unsubscribed user no longer receives messages.
    • Basic error handling (e.g., publishing to a non-existent topic).

Follow-up Questions & Discussion (Final 15 mins)

After I demonstrated the working solution, the interviewer moved to conceptual questions to test the depth of my knowledge.

  • Concurrency: "How would you make this thread-safe?"

    • Answer: Explained that a single global mutex is bad for performance. A better approach is a more granular lock, like a std::mutex inside each Topic object. Any operation modifying a topic (publish, subscribe) would lock that specific topic's mutex. This allows different topics to be modified concurrently.
  • Scalability & Memory: "What happens if a topic has millions of messages and one subscriber is offline for a week? What's the problem here?"

    • Answer: The messages vector in the topic would grow indefinitely, leading to an Out-Of-Memory error. I suggested solutions:
      1. Garbage Collection: Track the minimum offset across all subscribers for a topic. Messages with an index less than this minimum can be safely deleted from the front of the vector (or moved to cold storage). This requires a std::deque or a ring buffer for better performance on front deletions.
      2. TTL (Time-To-Live): Configure topics or messages to expire after a certain time.
      3. Backpressure/Quotas: Limit the number of unacknowledged messages a subscriber can have.
  • Guaranteed Delivery: "Your consume method returns a message and moves on. What if the consumer crashes after consuming but before processing? The message is lost."

    • Answer: This was a great question. I explained that true guaranteed delivery requires a two-phase approach:
      1. consume(): Fetches the message but does not advance the offset. It just marks the message as "in-flight."
      2. acknowledge(messageId): A new API method where the consumer confirms it has successfully processed the message. Only upon receiving an ack does the system advance the subscriber's offset. This prevents message loss.

Final Thoughts & Tips

  • Communicate Everything: Talk through your design, your data structure choices, and your implementation strategy. The interviewer is evaluating your thought process as much as your code.
  • Start Simple, Then Elaborate: Don't try to build the most complex, all-features-included solution from the start. Get a solid, working core done first. Then, show you've thought about the complex parts by discussing them.
  • Code Quality Matters: Use clear variable names, keep functions small, and structure your code logically. For a senior role, this is non-negotiable.
  • Test Your Code: A working solution with a solid test driver (main) is infinitely better than a "theoretically perfect" but incomplete or buggy one.

It was a challenging but very fair and practical round that accurately simulated a real-world engineering task. I was informed a couple of days later that I had cleared the round.

Good luck to everyone interviewing! I hope this detailed breakdown helps you prepare.

Comments (3)