Write Core APIs in LLD Interviews

Updated 2026-03-11

TL;DR

Writing core APIs means defining the public contracts (method signatures and interfaces) that your classes expose to the outside world. This is the foundation of good OOP design—it separates what your code does from how it does it. In interviews, defining clean APIs first demonstrates design thinking before diving into implementation.

Prerequisites: Understanding of classes and objects, basic knowledge of methods and parameters, familiarity with access modifiers (public/private), and basic understanding of abstraction concepts.

After this topic: Design clear, minimal public interfaces for classes before implementation, identify which methods should be public versus private, write method signatures that communicate intent, and apply the principle of programming to interfaces rather than implementations.

Core Concept

What is a Core API/Interface?

A core API (Application Programming Interface) is the set of public methods and properties that a class exposes to other parts of your program. Think of it as a contract: “If you call these methods with these parameters, I promise to deliver these results.”

An interface (in the formal sense) is an abstract type that defines method signatures without implementation. In Python, we use abstract base classes; in Java/C++, we use the interface keyword.

Why Define APIs First?

When you write the API before the implementation, you:

  1. Focus on behavior, not details: What should this class do, not how it does it
  2. Enable parallel development: Others can code against your interface while you implement
  3. Create testable code: Clear inputs and outputs make testing straightforward
  4. Reduce coupling: Clients depend on stable contracts, not internal details

The API Design Process

Step 1: Identify Responsibilities

What is this class responsible for? Write it in one sentence. If you need “and” multiple times, you might need multiple classes.

Step 2: Define Public Methods

For each responsibility, ask: “What operations does a client need?” These become your public methods.

Step 3: Choose Method Signatures Carefully

  • Name: Use verbs for actions (calculate, fetch, validate), nouns for queries (get_balance, is_valid)
  • Parameters: Only what’s necessary; avoid long parameter lists
  • Return type: Be specific and consistent

Step 4: Hide Implementation Details

Everything else should be private. Helper methods, internal state, and data structures are implementation details.

Programming to Interfaces

The principle “program to an interface, not an implementation” means: depend on abstract types (interfaces/base classes) rather than concrete classes. This makes your code flexible and easier to test.

Visual Guide

API vs Implementation Separation

graph TB
    Client[Client Code]
    API[Public API<br/>- method signatures<br/>- documented behavior]
    Impl[Private Implementation<br/>- helper methods<br/>- data structures<br/>- algorithms]
    
    Client -->|uses only| API
    API -.->|hides| Impl
    
    style API fill:#90EE90
    style Impl fill:#FFB6C1
    style Client fill:#87CEEB

Clients interact only with the public API. Implementation details remain hidden and can change without affecting clients.

Interface-Based Design

classDiagram
    class PaymentProcessor {
        <<interface>>
        +process_payment(amount, method) bool
        +refund(transaction_id) bool
    }
    
    class StripeProcessor {
        +process_payment(amount, method) bool
        +refund(transaction_id) bool
        -_call_stripe_api()
        -_validate_card()
    }
    
    class PayPalProcessor {
        +process_payment(amount, method) bool
        +refund(transaction_id) bool
        -_authenticate_paypal()
        -_send_request()
    }
    
    class CheckoutService {
        -processor: PaymentProcessor
        +complete_purchase(cart, payment_info)
    }
    
    PaymentProcessor <|.. StripeProcessor
    PaymentProcessor <|.. PayPalProcessor
    CheckoutService --> PaymentProcessor

CheckoutService depends on the PaymentProcessor interface, not concrete implementations. This allows swapping payment providers without changing checkout code.

Examples

Example 1: Shopping Cart API Design

Let’s design a shopping cart API before implementing it.

from abc import ABC, abstractmethod
from typing import List
from decimal import Decimal

# Step 1: Define the interface (API contract)
class ShoppingCart(ABC):
    """Interface for a shopping cart.
    
    Responsibilities:
    - Manage items in the cart
    - Calculate totals
    - Apply discounts
    """
    
    @abstractmethod
    def add_item(self, product_id: str, quantity: int) -> None:
        """Add a product to the cart.
        
        Args:
            product_id: Unique identifier for the product
            quantity: Number of items to add (must be positive)
            
        Raises:
            ValueError: If quantity is not positive
        """
        pass
    
    @abstractmethod
    def remove_item(self, product_id: str) -> None:
        """Remove a product from the cart.
        
        Args:
            product_id: Unique identifier for the product
            
        Raises:
            KeyError: If product not in cart
        """
        pass
    
    @abstractmethod
    def get_total(self) -> Decimal:
        """Calculate the total price of all items.
        
        Returns:
            Total price as Decimal
        """
        pass
    
    @abstractmethod
    def get_item_count(self) -> int:
        """Get the total number of items in cart.
        
        Returns:
            Sum of all item quantities
        """
        pass
    
    @abstractmethod
    def clear(self) -> None:
        """Remove all items from the cart."""
        pass


# Step 2: Implement the interface
class InMemoryShoppingCart(ShoppingCart):
    """Shopping cart implementation using in-memory storage."""
    
    def __init__(self, price_lookup: dict):
        self._items = {}  # Private: {product_id: quantity}
        self._price_lookup = price_lookup  # Private: {product_id: price}
    
    def add_item(self, product_id: str, quantity: int) -> None:
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        
        if product_id in self._items:
            self._items[product_id] += quantity
        else:
            self._items[product_id] = quantity
    
    def remove_item(self, product_id: str) -> None:
        if product_id not in self._items:
            raise KeyError(f"Product {product_id} not in cart")
        del self._items[product_id]
    
    def get_total(self) -> Decimal:
        total = Decimal('0')
        for product_id, quantity in self._items.items():
            price = self._price_lookup.get(product_id, Decimal('0'))
            total += price * quantity
        return total
    
    def get_item_count(self) -> int:
        return sum(self._items.values())
    
    def clear(self) -> None:
        self._items.clear()


# Usage example
prices = {
    "apple": Decimal('1.50'),
    "banana": Decimal('0.75'),
    "orange": Decimal('2.00')
}

cart = InMemoryShoppingCart(prices)
cart.add_item("apple", 3)
cart.add_item("banana", 2)

print(f"Items in cart: {cart.get_item_count()}")  # Output: Items in cart: 5
print(f"Total: ${cart.get_total()}")  # Output: Total: $6.00

cart.remove_item("banana")
print(f"Total after removal: ${cart.get_total()}")  # Output: Total after removal: $4.50

Key Points:

  • The interface defines WHAT operations are available
  • The implementation defines HOW they work
  • Clients code against ShoppingCart, not InMemoryShoppingCart
  • We can create DatabaseShoppingCart later without changing client code

Java/C++ Note: In Java, use interface ShoppingCart { ... }. In C++, use pure virtual functions: virtual void add_item(...) = 0;


Example 2: Cache API with Multiple Implementations

from abc import ABC, abstractmethod
from typing import Optional, Any
from datetime import datetime, timedelta

# Define the interface first
class Cache(ABC):
    """Interface for a key-value cache with expiration."""
    
    @abstractmethod
    def set(self, key: str, value: Any, ttl_seconds: int = 3600) -> None:
        """Store a value with optional time-to-live.
        
        Args:
            key: Cache key
            value: Value to store
            ttl_seconds: Time to live in seconds (default: 1 hour)
        """
        pass
    
    @abstractmethod
    def get(self, key: str) -> Optional[Any]:
        """Retrieve a value from cache.
        
        Args:
            key: Cache key
            
        Returns:
            Cached value or None if not found or expired
        """
        pass
    
    @abstractmethod
    def delete(self, key: str) -> bool:
        """Remove a key from cache.
        
        Args:
            key: Cache key
            
        Returns:
            True if key existed and was deleted, False otherwise
        """
        pass
    
    @abstractmethod
    def clear(self) -> None:
        """Remove all entries from cache."""
        pass


# Implementation 1: In-memory cache
class InMemoryCache(Cache):
    def __init__(self):
        self._store = {}  # Private: {key: (value, expiry_time)}
    
    def set(self, key: str, value: Any, ttl_seconds: int = 3600) -> None:
        expiry = datetime.now() + timedelta(seconds=ttl_seconds)
        self._store[key] = (value, expiry)
    
    def get(self, key: str) -> Optional[Any]:
        if key not in self._store:
            return None
        
        value, expiry = self._store[key]
        if datetime.now() > expiry:
            del self._store[key]  # Lazy cleanup
            return None
        
        return value
    
    def delete(self, key: str) -> bool:
        if key in self._store:
            del self._store[key]
            return True
        return False
    
    def clear(self) -> None:
        self._store.clear()


# Implementation 2: No-op cache (for testing/development)
class NoOpCache(Cache):
    """Cache that doesn't actually cache anything."""
    
    def set(self, key: str, value: Any, ttl_seconds: int = 3600) -> None:
        pass  # Do nothing
    
    def get(self, key: str) -> Optional[Any]:
        return None  # Always miss
    
    def delete(self, key: str) -> bool:
        return False
    
    def clear(self) -> None:
        pass


# Client code that works with any Cache implementation
class UserService:
    def __init__(self, cache: Cache):
        self._cache = cache  # Depends on interface, not implementation
    
    def get_user(self, user_id: str) -> dict:
        # Try cache first
        cached = self._cache.get(f"user:{user_id}")
        if cached:
            print("Cache hit!")
            return cached
        
        # Simulate database fetch
        print("Cache miss - fetching from database")
        user = {"id": user_id, "name": "John Doe", "email": "john@example.com"}
        
        # Store in cache
        self._cache.set(f"user:{user_id}", user, ttl_seconds=300)
        return user


# Usage - can swap cache implementations easily
print("=== Using InMemoryCache ===")
service1 = UserService(InMemoryCache())
user1 = service1.get_user("123")  # Output: Cache miss - fetching from database
user2 = service1.get_user("123")  # Output: Cache hit!

print("\n=== Using NoOpCache ===")
service2 = UserService(NoOpCache())
user3 = service2.get_user("123")  # Output: Cache miss - fetching from database
user4 = service2.get_user("123")  # Output: Cache miss - fetching from database

Expected Output:

=== Using InMemoryCache ===
Cache miss - fetching from database
Cache hit!

=== Using NoOpCache ===
Cache miss - fetching from database
Cache miss - fetching from database

Why This Works:

  • UserService depends on the Cache interface
  • We can inject different implementations (InMemory, NoOp, Redis, Memcached)
  • Testing is easy: use NoOpCache or a mock
  • Production flexibility: switch cache backends without changing UserService

Try it yourself: Create a LoggingCache that wraps another cache and prints every operation. It should implement the Cache interface and delegate to an inner cache.

Common Mistakes

1. Exposing Implementation Details

Mistake: Making internal data structures or helper methods public.

# BAD: Exposing internal list
class Stack:
    def __init__(self):
        self.items = []  # Public! Clients can do stack.items.clear()
    
    def push(self, item):
        self.items.append(item)

# GOOD: Hiding implementation
class Stack:
    def __init__(self):
        self._items = []  # Private
    
    def push(self, item):
        self._items.append(item)
    
    def pop(self):
        if not self._items:
            raise IndexError("Pop from empty stack")
        return self._items.pop()

Why it matters: Once something is public, you can’t change it without breaking client code. Keep implementation details private.


2. Overly Large Interfaces

Mistake: Creating “god interfaces” with too many methods.

# BAD: Interface doing too much
class UserManager(ABC):
    @abstractmethod
    def create_user(self, data): pass
    
    @abstractmethod
    def delete_user(self, id): pass
    
    @abstractmethod
    def send_email(self, user, message): pass  # Email responsibility
    
    @abstractmethod
    def log_activity(self, user, action): pass  # Logging responsibility
    
    @abstractmethod
    def calculate_discount(self, user): pass  # Business logic responsibility

# GOOD: Separate interfaces by responsibility
class UserRepository(ABC):
    @abstractmethod
    def create(self, user): pass
    
    @abstractmethod
    def delete(self, user_id): pass

class EmailService(ABC):
    @abstractmethod
    def send(self, recipient, message): pass

class DiscountCalculator(ABC):
    @abstractmethod
    def calculate(self, user): pass

Why it matters: Large interfaces are hard to implement, test, and maintain. Follow the Interface Segregation Principle: clients shouldn’t depend on methods they don’t use.


3. Inconsistent Method Naming

Mistake: Using different naming patterns for similar operations.

# BAD: Inconsistent naming
class DataStore:
    def add_item(self, item): pass
    def fetch_by_id(self, id): pass  # Should be get_item
    def remove(self, id): pass  # Should be remove_item or delete_item
    def item_count(self): pass  # Should be get_item_count

# GOOD: Consistent naming
class DataStore:
    def add_item(self, item): pass
    def get_item(self, id): pass
    def remove_item(self, id): pass
    def get_item_count(self): pass

Why it matters: Consistency makes APIs predictable and easier to learn. Use standard prefixes: get_, set_, is_, has_, add_, remove_.


4. Missing or Vague Documentation

Mistake: Not documenting expected behavior, parameters, or exceptions.

# BAD: No documentation
class Queue:
    def enqueue(self, item):
        pass
    
    def dequeue(self):
        pass

# GOOD: Clear documentation
class Queue:
    def enqueue(self, item: Any) -> None:
        """Add an item to the back of the queue.
        
        Args:
            item: The item to add (can be any type)
        """
        pass
    
    def dequeue(self) -> Any:
        """Remove and return the item at the front of the queue.
        
        Returns:
            The front item
            
        Raises:
            IndexError: If the queue is empty
        """
        pass

Why it matters: Documentation IS part of your API. It tells users how to use your code correctly.


5. Returning Implementation Types

Mistake: Returning concrete types instead of interfaces/abstract types.

# BAD: Returns concrete list
class TaskManager:
    def get_tasks(self) -> list:  # Clients now depend on list
        return self._tasks

# GOOD: Returns abstract type
from typing import Iterable

class TaskManager:
    def get_tasks(self) -> Iterable[Task]:  # Can return list, tuple, generator, etc.
        return iter(self._tasks)

Why it matters: Returning abstract types gives you flexibility to change the implementation later. You might want to return a generator for large datasets.

Interview Tips

1. Start with the Interface in Design Questions

When asked to “design a class” or “implement a system,” always start by defining the public API:

Interviewer: “Design a URL shortener service.”

You: “Let me start by defining the core interface:”

class URLShortener(ABC):
    @abstractmethod
    def shorten(self, long_url: str) -> str:
        """Convert a long URL to a short code."""
        pass
    
    @abstractmethod
    def expand(self, short_code: str) -> str:
        """Retrieve the original URL from a short code."""
        pass

Then ask: “Does this interface capture the requirements? Should we add analytics or expiration?”

This shows you think about design before diving into implementation details.


2. Verbalize the “Why” Behind Your API Choices

Don’t just write code—explain your reasoning:

  • “I’m making this method return an iterator instead of a list because the dataset might be large.”
  • “I’m using a separate validate() method instead of validating in the constructor so clients can check validity before creating objects.”
  • “I’m keeping this helper method private because it’s an implementation detail that might change.”

Interviewers want to see your thought process.


3. Ask About Interface Requirements

Before implementing, clarify what the API should do:

  • “Should add_item throw an exception if the item already exists, or update it?”
  • “Should get_user return None or raise an exception when the user doesn’t exist?”
  • “Do we need thread safety in this API?”

This shows you understand that API design involves trade-offs and decisions.


4. Demonstrate Interface Segregation

If you’re designing a complex system, show you can break it into focused interfaces:

# Instead of one large interface:
class PaymentSystem(ABC):
    # 15 methods here...

# Show you can separate concerns:
class PaymentProcessor(ABC):
    @abstractmethod
    def process(self, amount, method): pass

class PaymentValidator(ABC):
    @abstractmethod
    def validate(self, payment_info): pass

class PaymentLogger(ABC):
    @abstractmethod
    def log_transaction(self, transaction): pass

Say: “I’m separating these concerns so each interface has a single responsibility.”


5. Show You Can Evolve APIs Safely

If asked to “add a feature” to existing code, demonstrate backward compatibility:

# Original API
def search(self, query: str) -> List[Result]:
    pass

# Adding a feature without breaking existing code
def search(self, query: str, max_results: int = 10) -> List[Result]:
    """Search with optional result limit.
    
    Args:
        query: Search query
        max_results: Maximum results to return (default: 10)
    """
    pass

Explain: “I’m using a default parameter so existing callers don’t break.”


6. Practice Common API Design Patterns

Be ready to discuss and implement these patterns:

  • Builder pattern: For objects with many optional parameters
  • Factory pattern: For creating objects through an interface
  • Strategy pattern: For swappable algorithms
  • Repository pattern: For data access abstraction

Interviewers often ask: “How would you make this more flexible?” These patterns are your answer.


7. Know When to Use Abstract Classes vs Interfaces

In Python:

  • Use ABC (abstract base class) for both
  • Can include concrete methods with shared implementation

In Java:

  • Use interface when defining pure contracts (no implementation)
  • Use abstract class when you want to share code among subclasses

Be ready to explain: “I chose an abstract class here because I want to provide a default implementation of validate() that subclasses can override.”


8. Red Flags to Avoid

  • Don’t start coding implementation before discussing the interface
  • Don’t make everything public “just in case”
  • Don’t ignore error cases in your API (what happens when input is invalid?)
  • Don’t forget to discuss testing: “This interface makes testing easy because we can mock it.”

Key Takeaways

  • Define the API first, implementation second: Write method signatures and documentation before writing code. This forces you to think about what your class should do, not how it does it.

  • Keep interfaces small and focused: Each interface should have a single, clear responsibility. Large interfaces are hard to implement and violate the Interface Segregation Principle.

  • Hide implementation details: Only expose what clients need. Make fields and helper methods private. Once something is public, you can’t change it without breaking code.

  • Program to interfaces, not implementations: Depend on abstract types (interfaces/base classes) rather than concrete classes. This makes code flexible, testable, and easier to extend.

  • Document your API thoroughly: Method signatures alone aren’t enough. Document parameters, return values, exceptions, and expected behavior. Documentation is part of your contract with clients.