Search
⌘K
Common Problems

Logging Service

ByEvan King·Published ·
medium

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

Schedule a mock interview

Meet with a FAANG senior+ engineer or manager and learn exactly what it takes to get the job.

Schedule a Mock Interview