Bird
Raised Fist0

Design a Logging Framework - Log Levels, Appenders, Formatters, Log Rotation

LLDmediumInterview freq: medium
Choose your preparation mode4 modes available
๐ŸŽฏ
Design a Logging Framework - Log Levels, Appenders, Formatters, Log Rotation
40 min ยท medium
medium

Design a logging framework in Java that supports five log levels (TRACE, DEBUG, INFO, WARN, ERROR), multiple appenders (console, file, network), customizable formatters, and log rotation based on file size or time. The framework must be thread-safe, extensible without modifying existing code, and allow dynamic log level changes at runtime.

  • Support five log levels: TRACE, DEBUG, INFO, WARN, ERROR with filtering.
  • Multiple appenders: ConsoleAppender, FileAppender, NetworkAppender.
  • Formatters define log message layout; users can plug in custom formatters.
  • FileAppender supports log rotation by size or time.
  • Thread-safe logging from multiple concurrent threads.
  • Dynamic log level changes at runtime without restarting.
๐Ÿ“‹
Guided Steps
1/5
Step 1
Define Log Levels Using Enum
Design the log levels representation ensuring type safety and easy filtering.
๐Ÿ’ก Hint
Use Java enum to represent log levels with an integer severity value for comparison.
Use enum LogLevel with explicit severity int field - enums provide compile-time safety and exhaustive switch support in Java.
Mid: Defines enum LogLevel with severity. Senior: Explains how enum ordinal or explicit severity enables efficient filtering and prevents runtime string typos.
โš ๏ธCommon Wrong Answer
Using String constants for log levels leads to runtime typos and no compile-time exhaustive switch checking.
Step 2
Design Appender Abstraction
Design an abstraction for appenders that can write logs to different destinations.
๐Ÿ’ก Hint
Use an abstract class or interface with a method append(LogEvent event). Consider if appenders need instance state.
abstract class Appender with synchronized void append(LogEvent event) - abstract class chosen because appenders hold instance state like output streams; interface cannot hold state in Java.
Mid: Defines Appender abstraction with append method. Senior: Explains why abstract class is needed for shared state and synchronized append to avoid interleaved writes.
โš ๏ธCommon Wrong Answer
Using interface for Appender with no state leads to duplicated code and inability to hold output stream references.
Step 3
Implement Thread-Safe Logger with Dynamic Log Level
Design Logger class that filters logs by level and dispatches to appenders safely under concurrency.
๐Ÿ’ก Hint
Use volatile for logLevel field to ensure visibility of dynamic changes; synchronize append calls to appenders.
Logger has volatile LogLevel currentLevel to allow atomic visibility of level changes; synchronized void log(...) to atomically check level and dispatch to appenders.
Mid: Uses volatile for logLevel and synchronized log method. Senior: Explains how volatile prevents stale reads of logLevel and synchronized prevents interleaved appender writes.
โš ๏ธCommon Wrong Answer
Not using volatile for logLevel causes threads to cache stale values, ignoring dynamic level changes.
Step 4
Design Formatter Interface and Integration
Design how log messages are formatted before appending, allowing custom formatters.
๐Ÿ’ก Hint
Use interface Formatter with String format(LogEvent event); appenders hold a Formatter reference.
Formatter as interface with format(LogEvent) method - interface used because formatters are stateless or immutable; appenders hold Formatter by composition.
Mid: Defines Formatter interface and composition in Appender. Senior: Explains why interface fits stateless behavior and composition lifecycle (formatter can exist independently).
โš ๏ธCommon Wrong Answer
Using abstract class for Formatter with state leads to unnecessary complexity and less flexibility.
Step 5
Implement Log Rotation in FileAppender
Design log rotation mechanism triggered by file size or time in FileAppender.
๐Ÿ’ก Hint
Use composition: FileAppender owns a Rotator object; synchronize write and rotation to avoid race conditions.
FileAppender composes Rotator; synchronized write method checks rotation condition atomically; uses volatile boolean rotating flag to prevent concurrent rotations.
Mid: Implements rotation check in synchronized block. Senior: Explains thread interleaving where two threads might trigger rotation simultaneously without volatile rotating flag.
โš ๏ธCommon Wrong Answer
Not synchronizing rotation leads to corrupted log files due to concurrent file handle changes.
๐Ÿ”Œ
API Design Challenge
Design a Java logging framework API that allows clients to log messages at different levels, configure multiple appenders with custom formatters, and supports dynamic log level changes and log rotation.
  • Provide a Logger class with methods like log(LogLevel level, String message).
  • Allow configuring appenders and formatters via composition and interfaces.
  • Ensure thread safety using volatile and synchronized keywords where appropriate.
๐Ÿ“Š
Scoring Rubric
โ–ผ Show
Must Have - 60%
  • enum LogLevel with severity field for filtering
  • abstract class Appender with synchronized append(LogEvent)
  • Logger with volatile LogLevel currentLevel for dynamic changes
  • synchronized Logger.log method for atomic check-and-append
  • Formatter interface with format(LogEvent) method
Good to Have - 30%
  • FileAppender with synchronized log rotation and volatile rotating flag
  • Immutable LogEvent class for thread safety
  • Composition relationships with clear lifecycle ownership
  • Open/Closed Principle adherence for extensibility
Bonus - 10%
  • Support for multiple appenders per Logger
  • Dynamic appender addition/removal at runtime
  • Custom formatter injection
Using String constants for log levels leads to runtime typos, no exhaustive switch, and poor IDE support causing bugs and maintenance issues.
Using interface for Appender with stateful output streams causes code duplication and inability to share state, leading to inconsistent behavior.
Not using volatile for logLevel causes threads to cache stale values, ignoring dynamic changes and resulting in incorrect logging behavior.
Not synchronizing append or rotation methods leads to interleaved writes and corrupted log files due to concurrent file handle access.
Using abstract class for Formatter with state introduces unnecessary complexity and reduces flexibility for stateless formatters.
โœ…
Model Answer
โ–ผ Reveal

The logging framework uses a Java enum LogLevel with explicit severity for type-safe filtering. Appenders are modeled as an abstract class holding state like output streams, with synchronized append methods to prevent interleaved writes. Logger holds a volatile LogLevel field to allow dynamic runtime changes visible across threads, and its log method is synchronized to atomically check level and dispatch logs. Formatters are stateless interfaces composed by appenders, enabling flexible message formatting. FileAppender composes a Rotator object and synchronizes write and rotation methods to prevent race conditions, using a volatile rotating flag to avoid concurrent rotations.

  • Use enum LogLevel with explicit severity field for compile-time safety and efficient filtering
  • Choose abstract class for Appender to hold instance state like output streams and synchronize append to avoid interleaved writes
  • Declare Logger's currentLevel as volatile to ensure visibility of dynamic log level changes across threads
  • Synchronize Logger.log method to atomically check log level and dispatch to appenders safely
  • Define Formatter as a stateless interface to allow flexible and independent formatting implementations
  • Implement FileAppender log rotation by composing a Rotator object and synchronizing write and rotation methods
  • Use volatile boolean rotating flag in FileAppender to prevent concurrent rotations and race conditions
  • Design LogEvent as immutable to avoid synchronization overhead and ensure thread safety
  • Follow Open/Closed Principle by allowing new appenders and formatters without modifying existing code
  • Use enum LogLevel with explicit severity int field for type-safe log level filtering.
  • abstract class Appender holds instance state (e.g., output streams) and defines synchronized append(LogEvent) to avoid interleaved writes.
  • Logger class uses volatile LogLevel currentLevel for dynamic level changes visible across threads.
  • Logger.log method is synchronized to atomically check log level and dispatch to appenders.
  • Formatter defined as interface with format(LogEvent) method; appenders hold Formatter by composition (aggregation).
  • FileAppender composes a Rotator object; synchronized write and rotation methods prevent race conditions.
  • Use volatile boolean rotating flag in FileAppender to prevent concurrent rotations.
  • LogEvent is an immutable class holding log data to avoid synchronization overhead.
  • Extensibility follows Open/Closed Principle by allowing new appenders and formatters without modifying existing code.
  • Lifecycle: Logger aggregates appenders; appenders aggregate formatters; formatters exist independently.

Practice

(1/5)
1. Given a Vehicle abstraction that requires both state (e.g., speed) and behavior (e.g., move()), should Vehicle be an abstract class or an interface? Why?
easy
A. Abstract class, because it can hold shared state and provide default behavior implementations.
B. Interface, because it allows multiple inheritance and defines behavior without state.
C. Interface, because Java interfaces can now hold state with default methods.
D. Abstract class, because interfaces cannot define any methods.

Solution

  1. Step 1: Analyze state and behavior requirements

    Vehicle needs to hold state like speed and provide behavior like move(), which requires shared fields and method implementations.
  2. Step 2: Evaluate abstract class vs interface

    Abstract classes can hold instance variables and provide default method implementations; interfaces cannot hold instance state (only constants) despite default methods.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    State + behavior -> abstract class, not interface [check]
Hint: State needs abstract class; interfaces lack instance variables [check]
Common Mistakes:
2. In a UML class diagram, should Vehicle be modeled as an abstract class or an interface if it has state like speed and behavior like accelerate()? Why?
easy
A. Interface, because it allows multiple inheritance and defines behavior without state.
B. Abstract class, because it can hold shared state and provide default behavior implementations.
C. Interface, because state can be stored externally and behavior defined via default methods.
D. Abstract class, because interfaces cannot declare methods with parameters.

Solution

  1. Step 1: Analyze state and behavior requirements

    Vehicle has state (speed) and behavior (accelerate()), so it needs a construct that supports instance variables and method implementations.
  2. Step 2: Evaluate abstract class vs interface

    Abstract classes can hold state and provide default behavior; interfaces cannot hold instance state (only constants) and default methods can't maintain state.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    State + behavior with shared code -> abstract class [check]
Hint: Stateful base class -> abstract class, not interface [check]
Common Mistakes:
3. In the Meeting Scheduler, the RoomBookingManager class has a bookRoom() method that checks if a room is available and then marks it booked. Without synchronizing bookRoom(), which exact sequence of events between Thread 1 and Thread 2 causes a double-booking race condition?
medium
A. Thread 1 reads availability as false; Thread 2 reads availability as true; both proceed to book.
B. Thread 1 and Thread 2 both wait on a lock, causing deadlock and no booking.
C. Thread 1 books room; Thread 2 reads availability after booking and cancels its booking.
D. Thread 1 checks isAvailable() -> true, context switch before booking; Thread 2 checks isAvailable() -> true, both book simultaneously.

Solution

  1. Step 1: Identify check-then-act gap

    Without synchronization, Thread 1 reads availability as true but is preempted before booking; Thread 2 also reads true and proceeds.
  2. Step 2: Both threads book simultaneously causing double-booking

    Because the availability check and booking are not atomic, both threads can book the same room concurrently.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Unsynchronized check-then-act -> double-booking race [check]
Hint: Check-then-act without sync -> double-booking race [check]
Common Mistakes:
4. To support ElectricCar players who require ChargingSpot in the Tic-Tac-Toe game parking analogy, what is the minimum design change that respects extensibility without modifying existing classes?
hard
A. Add ELECTRIC_CAR to VehicleType enum, CHARGING to SpotType, create ElectricCar subclass, and update SpotCompatibility.COMPATIBILITY_MAP accordingly.
B. Modify existing Vehicle and Spot classes to include electric-specific logic and fields.
C. Create a new ElectricCar class with embedded charging logic and modify the parking manager to handle it.
D. Use a flag in the Vehicle class to indicate electric type and add conditional logic in parking spots.

Solution

  1. Step 1: Identify extensibility requirements

    Adding ElectricCar and ChargingSpot should not require modifying existing classes to avoid violating OCP and creating class explosion.
  2. Step 2: Why Add ELECTRIC_CAR to VehicleType enum, CHARGING to SpotType, create ElectricCar subclass, and update SpotCompatibility.COMPATIBILITY_MAP accordingly. is minimal and extensible

    Adding new enum values and a subclass keeps existing classes unchanged. Updating the compatibility map centralizes compatibility logic without scattering changes.
  3. Step 3: Why other options violate extensibility

    Options B, C, and D modify existing classes or add conditional logic, increasing coupling and violating OCP.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    New enums + subclass + compatibility map update -> minimal, extensible [check]
Hint: New enums + subclass + map update -> extend without modify [check]
Common Mistakes:
5. ElectricCar requires ChargingSpots in the parking system using Prototype pattern. What is the minimum change to support this without modifying existing classes?
hard
A. Modify Vehicle base class to include charging logic and add ChargingSpot as a subclass of Spot
B. Add charging logic inside Vehicle clone() and modify SpotCompatibility map inside existing classes
C. Add ELECTRIC_CAR to VehicleType enum, CHARGING to SpotType enum, create ElectricCar subclass, and update SpotCompatibility map externally
D. Create a god-class that handles both ElectricCar and ChargingSpot logic to avoid modifying existing classes

Solution

  1. Step 1: Identify extensibility requirements

    Adding ElectricCar and ChargingSpot must avoid modifying existing classes to respect OCP.
  2. Step 2: Evaluate options for minimal invasive change

    Add ELECTRIC_CAR to VehicleType enum, CHARGING to SpotType enum, create ElectricCar subclass, and update SpotCompatibility map externally adds new enum values and subclasses externally, updating compatibility map without changing existing code. Options B and C modify base classes, violating OCP. Create a god-class that handles both ElectricCar and ChargingSpot logic to avoid modifying existing classes creates god-class, violating SRP and maintainability.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    New enums + subclass + external map update -> minimal change, OCP respected [check]
Hint: New enums + subclass + external map -> minimal invasive change [check]
Common Mistakes:
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard