Skip to content

13 — LLD: Alexa Devices with Battery System

Understanding the Problem

There are different types of Alexa devices. One has only audio, one has a screen, one has both audio and screen. These devices may or may not have a battery. Battery devices track battery percentage. Both battery and non-battery devices can be put on charging. The task is to design a show() method that displays the current state — battery percentage if available, charging status, or "battery not available."

What is this problem really testing?

This is an interface segregation and composition problem. The naive approach uses deep inheritance (AudioDevice -> BatteryAudioDevice -> ...) and explodes with combinations. The interviewer wants to see you reach for composition over inheritance — attach capabilities (battery, audio, screen) to a device rather than creating a subclass for each combination.


Requirements

Clarifying Questions

You: Let me make sure I understand the device types. There is an audio-only device, a screen-only device, and a device with both audio and screen. Each of these can either have a battery or not. Is that correct?

Interviewer: Yes, that gives you six combinations, but you should not create six classes. Think about how to model this cleanly.

You: For the show() method, you mentioned four output scenarios. Let me list them:

  1. Device is charging AND has a battery — show "Charging. Battery: X%"
  2. Device is charging AND has NO battery — show "Charging. Battery not available"
  3. Device is NOT charging AND has a battery — show "Battery: X%"
  4. Device is NOT charging AND has NO battery — show "Battery not available"

Is that right?

Interviewer: Exactly those four cases.

You: Can a device without a battery still be put on charging? Like a device that is always plugged in?

Interviewer: Yes. A non-battery device can be "on charging" — think of it as being plugged into power. It just does not have a battery percentage to show.

You: Should I model audio and screen as capabilities that affect show()?

Interviewer: The show() method is primarily about battery status. Audio and screen are capabilities the device has, but they do not change the show output. The key design challenge is battery vs. no-battery and charging vs. not-charging.

You: Should the battery drain or charge over time, or is this a snapshot system?

Interviewer: Keep it simple — you can set the battery percentage directly. No simulation needed.

Final Requirements

  1. Device types: Audio-only, Screen-only, Audio+Screen.
  2. Devices optionally have a battery (composition, not inheritance).
  3. Any device can be put on or taken off charging.
  4. show() method produces exactly one of four outputs based on battery presence and charging state.
  5. Battery percentage is settable (0-100).
  6. Design should avoid class explosion — six combinations should NOT require six classes.

Out of scope: Battery drain/charge simulation, audio/screen functionality, network connectivity, voice recognition.


Core Entities and Relationships

EntityResponsibility
IBatteryInterface for battery capability — get/set percentage
IAudioInterface marker for audio capability
IScreenInterface marker for screen capability
BatteryConcrete battery implementation — stores percentage
DeviceBase class — has a name, optional battery, charging state. Implements show()
AudioDeviceDevice with audio capability
ScreenDeviceDevice with screen capability
AudioScreenDeviceDevice with both capabilities

Why use composition for battery? If you use inheritance (BatteryDevice vs NonBatteryDevice), you need to cross-product with audio/screen types: BatteryAudioDevice, NonBatteryAudioDevice, BatteryScreenDevice, etc. That is 6 classes. With composition, you have 3 device classes (Audio, Screen, AudioScreen) and you optionally attach a Battery object to any of them. 3 classes instead of 6.

Why are IAudio and IScreen interfaces and not fields? They represent capabilities, not data. A device either has audio capability or it does not — this is a type-level distinction. In TypeScript, interfaces let you check capabilities at compile time via type narrowing.


Class Design

Battery

State:

  • percentage: number (0-100)

Methods:

  • getPercentage() -> number
  • setPercentage(value: number)

Device

State:

  • name: string
  • battery: Battery | null — composition, not inheritance
  • isCharging: boolean

Methods:

  • startCharging()
  • stopCharging()
  • show() -> string — the four-case output method

Capability Interfaces

IAudio — marker that the device has audio output
IScreen — marker that the device has a display

In TypeScript, these are interfaces. They exist so that external code can check capabilities via type narrowing without coupling to concrete classes.

Final Class Design

typescript
interface IBattery {
  getPercentage(): number;
  setPercentage(value: number): void;
}

interface IAudio {
  readonly hasAudio: true;
}

interface IScreen {
  readonly hasScreen: true;
}

class Battery implements IBattery {
  private percentage: number;

  constructor(percentage: number = 100) {
    this.percentage = Math.max(0, Math.min(100, percentage));
  }

  getPercentage(): number {
    return this.percentage;
  }

  setPercentage(value: number): void {
    this.percentage = Math.max(0, Math.min(100, value));
  }
}

class Device {
  public isCharging: boolean = false;

  constructor(
    public readonly name: string,
    public readonly battery: Battery | null = null,
  ) {}

  startCharging(): void {
    this.isCharging = true;
  }

  stopCharging(): void {
    this.isCharging = false;
  }

  get hasBattery(): boolean {
    return this.battery !== null;
  }

  show(): string {
    const parts: string[] = [];
    if (this.isCharging) {
      parts.push("Charging");
    }
    if (this.hasBattery) {
      parts.push(`Battery: ${this.battery!.getPercentage()}%`);
    } else {
      parts.push("Battery not available");
    }
    return parts.join(". ");
  }
}

Design principles at play:

  • Composition over Inheritance: Battery is injected into Device, not inherited. A device with battery and a device without battery are the same class with different constructor arguments.
  • Interface Segregation (ISP): IBattery, IAudio, and IScreen are separate, narrow interfaces. A device is not forced to implement capabilities it does not have.
  • Open/Closed: Adding a new capability (e.g., ICamera) means creating a new interface and a new device subclass that mixes it in. Existing classes are untouched.
  • Dependency Inversion: Device depends on the Battery abstraction (accepts Battery | null), not on a concrete "battery is part of my class hierarchy" relationship.

Implementation

The Naive Approach (What NOT to Do)

Bad approach: Deep inheritance tree.

typescript
// DO NOT DO THIS
class Device {}
class BatteryDevice extends Device {}
class NonBatteryDevice extends Device {}
class BatteryAudioDevice extends BatteryDevice {}
class NonBatteryAudioDevice extends NonBatteryDevice {}
class BatteryScreenDevice extends BatteryDevice {}
class NonBatteryScreenDevice extends NonBatteryDevice {}
class BatteryAudioScreenDevice extends BatteryDevice {}
class NonBatteryAudioScreenDevice extends NonBatteryDevice {}

Eight classes for three device types with one optional capability. Add a second optional capability (e.g., camera) and you have 16 classes. This is the classic "class explosion" anti-pattern.

Good approach: Composition for battery, inheritance for device type.

typescript
class Device {
  constructor(public readonly name: string, public readonly battery: Battery | null = null) {}
  // battery = null means no battery
}

class AudioDevice extends Device implements IAudio {
  readonly hasAudio = true as const;
}
class ScreenDevice extends Device implements IScreen {
  readonly hasScreen = true as const;
}
class AudioScreenDevice extends Device implements IAudio, IScreen {
  readonly hasAudio = true as const;
  readonly hasScreen = true as const;
}

Three classes. Battery is optional. The show() method lives in Device and works for all variants.

Great approach: If device types are just capability combinations, use a single Device class with capability flags or attached components.

typescript
class Device {
  constructor(
    public readonly name: string,
    public readonly battery: Battery | null = null,
    public readonly hasAudio: boolean = false,
    public readonly hasScreen: boolean = false,
  ) {}
}

One class. But you lose the ability to do compile-time capability checks. The "Good approach" with marker interfaces is the sweet spot for interview purposes.

Concrete Device Classes

typescript
class AudioDevice extends Device implements IAudio {
  readonly hasAudio = true as const;

  constructor(name: string, battery: Battery | null = null) {
    super(name, battery);
  }
}

class ScreenDevice extends Device implements IScreen {
  readonly hasScreen = true as const;

  constructor(name: string, battery: Battery | null = null) {
    super(name, battery);
  }
}

class AudioScreenDevice extends Device implements IAudio, IScreen {
  readonly hasAudio = true as const;
  readonly hasScreen = true as const;

  constructor(name: string, battery: Battery | null = null) {
    super(name, battery);
  }
}

The Four show() Output Scenarios

The show() method in the base Device class handles all four cases:

typescript
show(): string {
  const parts: string[] = [];
  if (this.isCharging) {
    parts.push("Charging");
  }
  if (this.hasBattery) {
    parts.push(`Battery: ${this.battery!.getPercentage()}%`);
  } else {
    parts.push("Battery not available");
  }
  return parts.join(". ");
}
Charging?Battery?Output
YesYes"Charging. Battery: 75%"
YesNo"Charging. Battery not available"
NoYes"Battery: 75%"
NoNo"Battery not available"

Edge Cases

  1. Battery at 0%show() returns "Battery: 0%". The device is not dead, it just has no charge.
  2. Battery set to values outside 0-100Battery.setPercentage clamps to [0, 100].
  3. Charging a non-battery device — Valid. Think of a plugged-in Echo. show() returns "Charging. Battery not available".
  4. Checking capabilities — In TypeScript, use type guards: 'hasAudio' in device works correctly for AudioDevice and AudioScreenDevice but not ScreenDevice.

Verification

Step 1: Create an Echo Dot (audio, with battery at 75%).

device = new AudioDevice("Echo Dot", new Battery(75))
  • device.hasBattery = true
  • device.isCharging = false
  • device.show() = "Battery: 75%"

Step 2: Put it on charging.

device.startCharging()
  • device.isCharging = true
  • device.show() = "Charging. Battery: 75%"

Step 3: Create an Echo (audio, no battery — always plugged in).

echo = new AudioDevice("Echo")
  • echo.hasBattery = false
  • echo.isCharging = false
  • echo.show() = "Battery not available"

Step 4: Plug in the Echo.

echo.startCharging()
  • echo.show() = "Charging. Battery not available"

Step 5: Create an Echo Show (audio + screen, with battery at 45%).

showDevice = new AudioScreenDevice("Echo Show", new Battery(45))
  • 'hasAudio' in showDevice = true
  • 'hasScreen' in showDevice = true
  • showDevice.show() = "Battery: 45%"

All four output scenarios are covered.


Complete Code Implementation

typescript
interface IBattery {
  getPercentage(): number;
  setPercentage(value: number): void;
}

interface IAudio {
  readonly hasAudio: true;
}

interface IScreen {
  readonly hasScreen: true;
}

class Battery implements IBattery {
  private percentage: number;

  constructor(percentage: number = 100) {
    this.percentage = Math.max(0, Math.min(100, percentage));
  }

  getPercentage(): number {
    return this.percentage;
  }

  setPercentage(value: number): void {
    this.percentage = Math.max(0, Math.min(100, value));
  }

  toString(): string {
    return `Battery(${this.percentage}%)`;
  }
}

class Device {
  public isCharging: boolean = false;

  constructor(
    public readonly name: string,
    public readonly battery: Battery | null = null,
  ) {}

  startCharging(): void {
    this.isCharging = true;
  }

  stopCharging(): void {
    this.isCharging = false;
  }

  get hasBattery(): boolean {
    return this.battery !== null;
  }

  show(): string {
    const parts: string[] = [];
    if (this.isCharging) {
      parts.push("Charging");
    }
    if (this.hasBattery) {
      parts.push(`Battery: ${this.battery!.getPercentage()}%`);
    } else {
      parts.push("Battery not available");
    }
    return parts.join(". ");
  }

  toString(): string {
    const caps: string[] = [];
    if ("hasAudio" in this) caps.push("Audio");
    if ("hasScreen" in this) caps.push("Screen");
    const batteryStr = this.battery ? this.battery.toString() : "No battery";
    return `Device(${this.name}, caps=[${caps.join(", ")}], ${batteryStr})`;
  }
}

class AudioDevice extends Device implements IAudio {
  readonly hasAudio = true as const;

  constructor(name: string, battery: Battery | null = null) {
    super(name, battery);
  }
}

class ScreenDevice extends Device implements IScreen {
  readonly hasScreen = true as const;

  constructor(name: string, battery: Battery | null = null) {
    super(name, battery);
  }
}

class AudioScreenDevice extends Device implements IAudio, IScreen {
  readonly hasAudio = true as const;
  readonly hasScreen = true as const;

  constructor(name: string, battery: Battery | null = null) {
    super(name, battery);
  }
}

// ---- Device Factory (optional, for cleaner creation) ----

class DeviceFactory {
  static create(
    name: string,
    options: {
      hasAudio?: boolean;
      hasScreen?: boolean;
      batteryPercentage?: number;
    } = {},
  ): Device {
    const battery =
      options.batteryPercentage !== undefined
        ? new Battery(options.batteryPercentage)
        : null;

    if (options.hasAudio && options.hasScreen) {
      return new AudioScreenDevice(name, battery);
    } else if (options.hasAudio) {
      return new AudioDevice(name, battery);
    } else if (options.hasScreen) {
      return new ScreenDevice(name, battery);
    } else {
      return new Device(name, battery);
    }
  }
}

// ---- Demo ----

// Scenario 1: Charging + Battery
const dot = new AudioDevice("Echo Dot", new Battery(75));
dot.startCharging();
console.log(`${dot.name}: ${dot.show()}`);
// Output: Echo Dot: Charging. Battery: 75%

// Scenario 2: Charging + No Battery
const echo = new AudioDevice("Echo");
echo.startCharging();
console.log(`${echo.name}: ${echo.show()}`);
// Output: Echo: Charging. Battery not available

// Scenario 3: Not Charging + Battery
const showDev = new AudioScreenDevice("Echo Show", new Battery(45));
console.log(`${showDev.name}: ${showDev.show()}`);
// Output: Echo Show: Battery: 45%

// Scenario 4: Not Charging + No Battery
const screen = new ScreenDevice("Fire TV");
console.log(`${screen.name}: ${screen.show()}`);
// Output: Fire TV: Battery not available

// Capability checks
console.log(`\n${dot.name} has audio: ${"hasAudio" in dot}`);
console.log(`${dot.name} has screen: ${"hasScreen" in dot}`);
console.log(`${showDev.name} has audio: ${"hasAudio" in showDev}`);
console.log(`${showDev.name} has screen: ${"hasScreen" in showDev}`);

// Factory usage
const device = DeviceFactory.create("Portable Echo", {
  hasAudio: true,
  batteryPercentage: 90,
});
console.log(`\n${device.name}: ${device.show()}`);
device.startCharging();
console.log(`${device.name}: ${device.show()}`);

Extensibility

1. Adding a Camera Capability

Create an ICamera marker interface and an AudioScreenCameraDevice class:

typescript
interface ICamera {
  readonly hasCamera: true;
}

class AudioScreenCameraDevice extends Device implements IAudio, IScreen, ICamera {
  readonly hasAudio = true as const;
  readonly hasScreen = true as const;
  readonly hasCamera = true as const;

  constructor(name: string, battery: Battery | null = null) {
    super(name, battery);
  }
}

The show() method does not change because camera does not affect battery display. The factory gains one more combination. With 4 capabilities you have 15 non-empty combinations — but in practice only a few are real products.

Tradeoff: If you find yourself creating many combination classes, switch to the "capabilities as components" approach where capabilities are attached dynamically. The marker interface approach works well when the number of real product configurations is small.

2. Battery Health / Degradation

Add a health field to Battery that degrades over charge cycles:

typescript
class Battery implements IBattery {
  private percentage: number;
  private health: number;
  private chargeCycles: number = 0;

  constructor(percentage: number = 100, health: number = 100) {
    this.percentage = Math.max(0, Math.min(100, percentage));
    this.health = health;
  }

  charge(): void {
    this.chargeCycles += 1;
    if (this.chargeCycles % 500 === 0) {
      this.health = Math.max(0, this.health - 1);
    }
  }

  // ... getPercentage, setPercentage ...
}

Tradeoff: More realistic but adds complexity to a class that was intentionally simple. Only add if the interviewer asks about device lifecycle.

3. Decorator Pattern for Enhanced show()

If different device types should show additional info (e.g., AudioDevice shows "Playing music" if active), use a decorator:

typescript
interface ShowDecorator {
  show(): string;
}

class NowPlayingDecorator implements ShowDecorator {
  constructor(
    private readonly device: Device,
    private readonly track: string,
  ) {}

  show(): string {
    const base = this.device.show();
    return `${base}. Now playing: ${this.track}`;
  }
}

Tradeoff: Adds a layer of indirection. Only worthwhile if the show() output varies significantly across device types and you want to compose display behaviors.


What is Expected at Each Level

LevelExpectations
JuniorIdentify the four output cases. May use if/else branches or a class hierarchy that works but has code duplication. Might create separate classes for battery and non-battery variants.
Mid-levelComposition for battery (inject or not). Single show() method that handles all four cases. Marker interfaces for capabilities. Discuss why composition beats inheritance here.
SeniorEverything above plus: Interface Segregation Principle by name, analysis of class explosion in the inheritance approach with concrete numbers, factory pattern for device creation, discussion of how capabilities could be fully dynamic (component-entity system) if the number of combinations grows, decorator pattern for extending show().

Frontend interview preparation reference.