Write Core APIs in LLD Interviews
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.
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:
- Focus on behavior, not details: What should this class do, not how it does it
- Enable parallel development: Others can code against your interface while you implement
- Create testable code: Clear inputs and outputs make testing straightforward
- 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, notInMemoryShoppingCart - We can create
DatabaseShoppingCartlater 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:
UserServicedepends on theCacheinterface- We can inject different implementations (InMemory, NoOp, Redis, Memcached)
- Testing is easy: use
NoOpCacheor 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_itemthrow an exception if the item already exists, or update it?” - “Should
get_userreturn 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
interfacewhen defining pure contracts (no implementation) - Use
abstract classwhen 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.