Search
⌘K
Common Problems
Logging Service
Try This Problem Yourself
Practice with guided hints and real-time feedback
Understanding the Problem
📝 What is a Logger?
A logger is the in-process library an application uses to record what's happening at runtime. Code calls logger.info("user signed in") from anywhere in the app, and the library timestamps the message, attaches the severity level, and writes it to one or more places like the console, a file, or both. Think Log4j, SLF4J, or Python's logging module. We're designing the library that lives inside one application, not a distributed log aggregation service.
Requirements
You sit down for the interview and the prompt comes in deliberately short:
"Design a logging service. Or call it a logger, whichever you prefer."
Most of the design is hidden in what they didn't say. Spend the first few minutes pulling it apart before drawing anything.
Clarifying Questions
The first thing to nail down is what kind of logging system this is. "Logger" can mean wildly different things.
You: "When you say 'logging service,' do you mean an in-process library the application links against? Or something that ships logs over the network to a central aggregator?"Interviewer: "In-process library. Network shipping, ingestion pipelines, and central aggregation are someone else's problem."
That answer cuts most of the design space. No queues, no schema registries, no fan-out across services. The deliverable is an object model that runs inside one application's process and writes to local destinations like stdout and files.
You: "What severity levels should we support, and is there an ordering between them?"Interviewer: "DEBUG, INFO, WARN, ERROR, FATAL. Ordered from least to most severe in that order."
Five levels with a natural ordering. That's a finite set with no per-level behavior, which is a textbook enum. If you reach for a Level class hierarchy with DebugLevel, InfoLevel, and so on, you're doing too much.
You: "Can a single logger write to multiple destinations at the same time? Like sending the same record to both the console and a file?"Interviewer: "Yes. That's the common case. A developer running locally wants logs in the console and also persisted to a file for later inspection. Each call should fan out to every configured destination."
Now you know "destination" is a first-class concept and that one log call hits all of them. That implies the library holds a list of destinations and iterates over them on every call.
You: "Does each destination decide its own filter level, or is there one global level on the logger?"Interviewer: "Per destination. Each destination has its own minimum level. The console might want everything from DEBUG up, and the file destination might only care about WARN and above. Records below a destination's threshold should be dropped before being written."
This rules out putting the level filter on the logger and pushes it down to the destination. It also means the same record gets evaluated independently by each destination, which is fine because the record itself is immutable once created.
You: "And the format the records are written in. Is that fixed, or does it vary?"Interviewer: "Varies. Sometimes plain text, sometimes JSON. And the format is independent of the destination type. You should be able to write JSON to the console, plain text to a file, or any combination."
This is the requirement that shapes the class model. If format and destination were coupled, you'd end up with a class for every (format, target) pair like JsonFileDestination, PlainConsoleDestination, and so on. Add a third format and a third target and you have nine classes. Since the requirement says they vary independently, the right move is to compose them instead of multiplying classes.
When a requirement gives you two dimensions that vary independently, that's almost always a signal to use composition over inheritance. Two interfaces composed together let you mix any combination without writing N×M classes. Watch for these axes-of-variation hints in any LLD prompt.
You: "What about concurrency? Multiple threads in the same app are going to be calling log() simultaneously. What's the expectation?"Interviewer: "Thread-safe. Each record's bytes have to land on a destination atomically — one record's bytes can't be split across or mixed with another's. For a single thread, records appear in call order. Across threads, no strict ordering beyond each record's timestamp."
Concurrency is in scope, so locking is part of the design, not a cleanup pass at the end. Per-record atomicity is the bar — two threads racing to a stdout buffer can't smear one record's bytes across another's. Strict global submission order across threads is a harder requirement that would push toward queues and single-writer threads, and they're not asking for that.
You: "Last one. Is configuration static, set at startup, or do we need to handle hot-reloading destinations and levels at runtime?"Interviewer: "Static. Configured once at startup. Hot-reload, async or buffered writes, log rotation, and network destinations are all out of scope, though the design should not block adding a remote destination later."
The last clause is the one that shapes the design. You don't build remote destinations now, but the model needs an extension point so adding one later doesn't force a rewrite of Logger. As long as destinations are pluggable behind an interface, you're fine.
Final Requirements
After that back-and-forth, you'd write this on the whiteboard:
Final Requirements
Requirements:
1. Five severity levels: DEBUG < INFO < WARN < ERROR < FATAL.
2. Each record carries timestamp, level, message, emitting thread name.
3. Logger writes each record to one or more destinations, set at startup.
4. Each destination has its own min-level threshold and its own format.
Format and destination type vary independently.
5. Concurrent calls are safe. A record's bytes never interleave with
another record's bytes on the same destination.
Out of scope:
- Hot-reloading config at runtime
- Async / buffered writes
- Remote / network destinations in v1 (design should accommodate)
- Hierarchical / named loggers (com.app.service inheriting from com.app)Notice we explicitly scoped out async writes, hot-reload, and remote destinations. Each of those is a real production concern, but each is a layer that sits on top of the core object model rather than being part of it. Calling them out by name signals that you considered them and chose not to build them, which reads very differently from forgetting they exist. Most of them come back as natural extensions in the follow-up section anyway.
Core Entities and Relationships
Class Design
Logger
LogRecord
Formatter
Destination
Sink
Final Class Design
Implementation
Logger
Destination
Formatter implementations
Sink implementations
LogRecord
Complete Code Implementation
Verification
Extensibility
1. "How would you make log() non-blocking?"
2. "How would you support hierarchical named loggers?"
What is Expected at Each Level?
Junior
Mid-level
Senior
Purchase Premium to Keep Reading
Unlock this article and so much more with Hello Interview Premium
Currently up to 20% off
Hello Interview Premium
Reading Progress
On This Page
Understanding the Problem
Requirements
Clarifying Questions
Final Requirements
Core Entities and Relationships
Class Design
Logger
LogRecord
Formatter
Destination
Sink
Final Class Design
Implementation
Logger
Destination
Formatter implementations
Sink implementations
LogRecord
Complete Code Implementation
Verification
Extensibility
1. "How would you make `log()` non-blocking?"
2. "How would you support hierarchical named loggers?"
What is Expected at Each Level?
Junior
Mid-level
Senior
Schedule a mock interview
Meet with a FAANG senior+ engineer or manager and learn exactly what it takes to get the job.
