Extensible Logger with Pluggable Filter, Truncate, Uppercase and Array Handlers

Read the full interview experience this question came from →

Quick Overview

Build a logger whose handlers can be added without changing the core Logger class, starting with filter, truncation, uppercase and in-memory array handlers that each process the message independently. Follow-ups cover database and remote-API handlers, performance optimization and flexible handler configuration, testing object-oriented design and extensibility.

Extensible Logger with Pluggable Filter, Truncate, Uppercase and Array Handlers

Company: Rippling

Role: Software Engineer

Category: Software Engineering Fundamentals

Difficulty: medium

Interview Round: Technical Screen

Build a simple logging utility for an application. The core requirement is that the `Logger` supports several different log handlers, and that new handlers can be added later without modifying the core `Logger` class. Implement these handlers first: - **Filter handler:** before printing, removes every occurrence of a configurable string from the message. - **Truncation handler:** before printing, truncates the message to N characters. - **Uppercase handler:** before printing, converts the message to uppercase. - **Array handler:** stores the message in an array instead of printing it. Each handler processes the message it receives independently; the handlers do not depend on one another. When `logger.log("hello world")` is called on a logger with all four handlers, the message is printed to the console several times, in order: the filtered version, the truncated version and the uppercased version. The message is also stored in an array. The design must accommodate future requirements such as: - a database logger that opens a new database connection every time it logs a message; - a logger that sends messages to a remote API endpoint. With an illustrative configuration (the filter removes `"l"`, and N = 5): ```text logger.log("hello world") console: heo word console: hello console: HELLO WORLD array: ["hello world"] ``` ### Constraints and Clarifications - The filter string, N and the implementation language are not fixed by the task; the configuration above is only an example. - Every handler receives the original message, not the output of another handler. ### Clarifying Questions - Is the filter a single left-to-right pass, or repeated until the string no longer occurs (removing `"ab"` from `"aabb"` leaves `"ab"` after one pass)? Is it case-sensitive? - What should truncation do when N is larger than the message, zero or negative, and does it count characters or bytes? - Is the order in which handlers run part of the contract, and can the same handler type be registered twice with different settings? - If one handler fails, for example because the remote endpoint is down, should the others still run, and should `log` raise? - Will `log` be called from several threads at once? ### Part 1 — The Logger and the four handlers Implement the `Logger` and the four handlers so that the `logger.log("hello world")` scenario above produces the output shown, and show how a caller registers handlers. ```hint What the logger needs to know Decide the smallest contract the logger needs from a handler, so that the logger never has to know which kinds of handler exist. ``` #### What This Part Should Cover - A handler abstraction, and a logger that depends only on it - Correct behavior and configuration for each of the four handlers, including edge cases - Handler independence: every handler gets the original message - A test or demo that checks the printed and stored output ### Part 2 — Future handlers, optimization and flexibility Extend the design with a database handler that opens a new connection on every log call and a handler that sends messages to a remote API endpoint. Then answer the follow-up discussion: how would you optimize this design, and how would you make handlers more flexible to pass in and configure? ```hint Not every handler is cheap Compare the cost of printing a line with opening a database connection or making a network call, and ask what the code that calls `log` should have to wait for. ``` ```hint Changing versus delivering Three of the handlers change the message and then print it, and one stores it unchanged. Ask what you would have to write to get an uppercased message into the array, or a truncated one to the remote endpoint. ``` #### What This Part Should Cover - How the new handlers plug in without touching `Logger`, and how they acquire and release connections - The cost of a connection or request per message, and remedies such as reuse, batching and asynchronous delivery, with their risks - Failure isolation, retries, and what happens to messages when a destination is slow or down - Flexible configuration: composing message transformations with destinations, and registering handlers at run time or from configuration ### What a Strong Answer Covers - Settles the filter semantics, truncation bounds, ordering and failure policy before coding - An abstraction that keeps `Logger` closed to modification and open to new handlers - Small, correct handler implementations, tested on the given scenario and on edge cases - A concrete plan for expensive handlers: connection reuse, buffering, asynchronous delivery, backpressure, and flushing at shutdown - A reasoned position on flexibility, such as composition over one class per combination, without over-engineering a short exercise ### Follow-up Questions - The remote endpoint becomes slow and `log` calls start blocking the application. What do you change, and which messages can now be lost? - The process exits while messages are still buffered for the database. How do you make sure they are written? - A new requirement asks for "uppercase, then truncate, then send to the remote endpoint" as one handler. How do you support arbitrary chains without a class for every combination? - How do you unit-test the database and remote handlers without a real database or network?

Overview: Build a logger whose handlers can be added without changing the core Logger class, starting with filter, truncation, uppercase and in-memory array handlers that each process the message independently. Follow-ups cover database and remote-API handlers, performance optimization and flexible handler configuration, testing object-oriented design and extensibility.

Read the full Rippling Software Engineer interview experience this question came from

Rippling logo
Rippling
Oct 13, 2026
mediumSoftware EngineerTechnical ScreenSoftware Engineering Fundamentals
3
0

Build a simple logging utility for an application. The core requirement is that the Logger supports several different log handlers, and that new handlers can be added later without modifying the core Logger class.

Implement these handlers first:

  • Filter handler: before printing, removes every occurrence of a configurable string from the message.
  • Truncation handler: before printing, truncates the message to N characters.
  • Uppercase handler: before printing, converts the message to uppercase.
  • Array handler: stores the message in an array instead of printing it.

Each handler processes the message it receives independently; the handlers do not depend on one another. When logger.log("hello world") is called on a logger with all four handlers, the message is printed to the console several times, in order: the filtered version, the truncated version and the uppercased version. The message is also stored in an array.

The design must accommodate future requirements such as:

  • a database logger that opens a new database connection every time it logs a message;
  • a logger that sends messages to a remote API endpoint.

With an illustrative configuration (the filter removes "l", and N = 5):

logger.log("hello world")

console:  heo word
console:  hello
console:  HELLO WORLD
array:    ["hello world"]

Constraints and Clarifications

  • The filter string, N and the implementation language are not fixed by the task; the configuration above is only an example.
  • Every handler receives the original message, not the output of another handler.

Clarifying Questions Guidance

  • Is the filter a single left-to-right pass, or repeated until the string no longer occurs (removing "ab" from "aabb" leaves "ab" after one pass)? Is it case-sensitive?
  • What should truncation do when N is larger than the message, zero or negative, and does it count characters or bytes?
  • Is the order in which handlers run part of the contract, and can the same handler type be registered twice with different settings?
  • If one handler fails, for example because the remote endpoint is down, should the others still run, and should log raise?
  • Will log be called from several threads at once?

Part 1 — The Logger and the four handlers

Implement the Logger and the four handlers so that the logger.log("hello world") scenario above produces the output shown, and show how a caller registers handlers.

What This Part Should Cover Guidance

  • A handler abstraction, and a logger that depends only on it
  • Correct behavior and configuration for each of the four handlers, including edge cases
  • Handler independence: every handler gets the original message
  • A test or demo that checks the printed and stored output

Part 2 — Future handlers, optimization and flexibility

Extend the design with a database handler that opens a new connection on every log call and a handler that sends messages to a remote API endpoint. Then answer the follow-up discussion: how would you optimize this design, and how would you make handlers more flexible to pass in and configure?

What This Part Should Cover Guidance

  • How the new handlers plug in without touching Logger , and how they acquire and release connections
  • The cost of a connection or request per message, and remedies such as reuse, batching and asynchronous delivery, with their risks
  • Failure isolation, retries, and what happens to messages when a destination is slow or down
  • Flexible configuration: composing message transformations with destinations, and registering handlers at run time or from configuration

What a Strong Answer Covers Guidance

  • Settles the filter semantics, truncation bounds, ordering and failure policy before coding
  • An abstraction that keeps Logger closed to modification and open to new handlers
  • Small, correct handler implementations, tested on the given scenario and on edge cases
  • A concrete plan for expensive handlers: connection reuse, buffering, asynchronous delivery, backpressure, and flushing at shutdown
  • A reasoned position on flexibility, such as composition over one class per combination, without over-engineering a short exercise

Follow-up Questions Guidance

  • The remote endpoint becomes slow and log calls start blocking the application. What do you change, and which messages can now be lost?
  • The process exits while messages are still buffered for the database. How do you make sure they are written?
  • A new requirement asks for "uppercase, then truncate, then send to the remote endpoint" as one handler. How do you support arbitrary chains without a class for every combination?
  • How do you unit-test the database and remote handlers without a real database or network?