graph TD
linkStyle default stroke:#000,color:#000
subgraph Without["Without DI (Tight Coupling)"]
A["OrderService"] -->|"new"| B["MySQLOrderRepo()"]
A -->|"new"| C["SmtpEmailService()"]
end
subgraph With["With DI (Spring Container)"]
CONTAINER["Spring IoC Container"]
CONTAINER -->|"injects"| D["OrderService"]
CONTAINER -->|"creates"| E["OrderRepository"]
CONTAINER -->|"creates"| F["EmailService"]
E -.->|"injected into"| D
F -.->|"injected into"| D
end
style Without fill:#ff9f84,stroke:#333,color:#222
style With fill:#8fe0bf,stroke:#333,color:#222
style CONTAINER fill:#9fddea,stroke:#333,color:#222
Design Pattern Interview QA - 2
Spring Framework dependency injection, Inversion of Control, Spring IoC container, template method pattern, state pattern, facade pattern, flyweight pattern, mediator pattern, repository pattern, MVC pattern, CQRS pattern, Spring Boot design patterns
Introduction
This is Part 2 of our Design Pattern Interview QA series, focused on dependency injection frameworks (Spring), Inversion of Control, and additional enterprise patterns frequently asked in backend and full-stack interviews.
For the 10 most commonly asked GoF patterns (Singleton, Factory, Observer, Strategy, Decorator, Adapter, Builder, Command, Chain of Responsibility, Proxy), see Design Pattern Interview QA - 1.
Q1: What is Dependency Injection and how do modern frameworks implement it?
Answer:
Dependency Injection (DI) is a design pattern where an object’s dependencies are provided from the outside rather than created internally. Frameworks such as Spring and FastAPI manage object creation, wiring, and lifecycle automatically.
Python DI: Three Injection Styles
from dataclasses import dataclass
from typing import Protocol
class OrderRepository(Protocol):
def save(self, order: "Order") -> "Order": ...
class EmailService(Protocol):
def send_confirmation(self, order: "Order") -> None: ...
@dataclass
class Order:
id: int | None
item: str
@dataclass
class OrderRequest:
item: str
# 1. CONSTRUCTOR INJECTION (recommended)
class OrderService:
def __init__(self, order_repo: OrderRepository, email_service: EmailService):
self.order_repo = order_repo
self.email_service = email_service
def create_order(self, request: OrderRequest) -> Order:
order = self.order_repo.save(Order(id=None, item=request.item))
self.email_service.send_confirmation(order)
return order
# 2. PARAMETER/FUNCTION INJECTION (common in Python)
def generate_report(data: list[dict], formatter=None) -> str:
if formatter is not None:
return formatter(data)
return str(data)
# 3. FRAMEWORK INJECTION (FastAPI Depends)
from fastapi import Depends, FastAPI
from typing import Annotated
app = FastAPI()
def get_order_service() -> OrderService:
return OrderService(order_repo=SqlOrderRepo(), email_service=SmtpEmailService())
@app.post("/orders")
def create_order(
request: OrderRequest,
service: Annotated[OrderService, Depends(get_order_service)],
):
return service.create_order(request)Python Dependency Scope and Lifecycle
from collections.abc import Generator
# App-level singleton dependency (created once)
class Settings:
def __init__(self):
self.env = "production"
settings = Settings()
# Request-scoped dependency with cleanup (FastAPI)
def get_db() -> Generator["Database", None, None]:
db = Database("postgresql://localhost/app")
try:
yield db # Injected into handlers
finally:
db.close() # Lifecycle cleanup
# Transient/prototype behavior: create each time
def build_request_handler() -> "RequestHandler":
return RequestHandler()Python Configuration Approaches
from pydantic_settings import BaseSettings
# Approach 1: Settings class from env/.env
class AppSettings(BaseSettings):
db_url: str = "sqlite:///app.db"
email_backend: str = "smtp"
settings = AppSettings()
# Approach 2: Dependency provider functions
def get_order_repository() -> "OrderRepository":
return SqlOrderRepo(settings.db_url)
def get_email_service() -> "EmailService":
if settings.email_backend == "smtp":
return SmtpEmailService()
return MockEmailService()
# Approach 3: Environment-based wiring
def get_payment_gateway() -> "PaymentGateway":
if settings.env == "production":
return StripeGateway()
return FakePaymentGateway()Testing Python DI
from unittest.mock import Mock
# Unit test — inject mocks manually
def test_create_order_unit():
mock_repo = Mock()
mock_email = Mock()
service = OrderService(mock_repo, mock_email)
mock_repo.save.return_value = Order(id=1, item="Widget")
order = service.create_order(OrderRequest(item="Widget"))
mock_repo.save.assert_called_once()
mock_email.send_confirmation.assert_called_once_with(order)
# Integration-style test — override FastAPI dependencies
from fastapi.testclient import TestClient
def test_create_order_api(app):
fake_service = Mock()
fake_service.create_order.return_value = {"id": 10, "item": "Gadget"}
app.dependency_overrides[get_order_service] = lambda: fake_service
client = TestClient(app)
res = client.post("/orders", json={"item": "Gadget"})
assert res.status_code == 200
app.dependency_overrides.clear()Why Constructor Injection Is Preferred
| Injection Type | Pros | Cons |
|---|---|---|
| Constructor | Immutable fields; easy to test; fail-fast on missing deps | Verbose with many dependencies |
| Setter | Good for optional deps; reconfigurable | Mutable; easy to forget injection |
| Field | Concise | Cannot test without reflection; hides dependencies |
Q2: What is Inversion of Control (IoC) and how does it relate to DI?
Answer:
Inversion of Control (IoC) is a broad principle where the flow of control is inverted — instead of your code controlling the creation and wiring of objects, a framework or container takes over. Dependency Injection is one specific implementation of IoC.
graph TD
linkStyle default stroke:#000,color:#000
subgraph Traditional["Traditional Control Flow"]
APP["Your Code"]
APP -->|"creates"| DEP1["Database"]
APP -->|"creates"| DEP2["Logger"]
APP -->|"calls"| FRAMEWORK["Framework"]
end
subgraph IoC["Inversion of Control"]
CONTAINER["IoC Container<br/>(Spring / FastAPI)"]
CONTAINER -->|"creates & injects"| DEP3["Database"]
CONTAINER -->|"creates & injects"| DEP4["Logger"]
CONTAINER -->|"calls"| YOUR_CODE["Your Code"]
end
style Traditional fill:#ff9f84,stroke:#333,color:#222
style IoC fill:#8fe0bf,stroke:#333,color:#222
style CONTAINER fill:#9fddea,stroke:#333,color:#222
IoC Implementations
| IoC Type | Mechanism | Example |
|---|---|---|
| Dependency Injection | Container injects dependencies | Spring @Autowired, FastAPI Depends() |
| Service Locator | Object asks container for deps | ServiceLocator.get(UserRepo.class) |
| Template Method | Framework calls your overrides | HttpServlet.doGet(), Django views |
| Event-driven | Framework calls handlers | Spring @EventListener, Observer pattern |
| Strategy via config | Framework picks implementation | Spring profiles, plugin systems |
Spring IoC Container Architecture
// The Spring ApplicationContext IS the IoC container
// It manages the full lifecycle:
// 1. BEAN DEFINITION: Read configuration (annotations, XML, Java config)
// 2. INSTANTIATION: Create bean instances
// 3. DEPENDENCY RESOLUTION: Wire dependencies (DI)
// 4. INITIALIZATION: Call @PostConstruct, InitializingBean
// 5. READY: Beans available for use
// 6. DESTRUCTION: Call @PreDestroy on shutdown
// Two main container types:
// BeanFactory — basic DI, lazy initialization
// ApplicationContext — extends BeanFactory with:
// - Event publishing
// - Internationalization (i18n)
// - Resource loading
// - AOP integration
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
// Creates the IoC container
ApplicationContext ctx = SpringApplication.run(MyApp.class, args);
// Container manages all beans
OrderService service = ctx.getBean(OrderService.class);
service.createOrder(new OrderRequest("Widget"));
}
}IoC in Python (FastAPI Depends)
from fastapi import FastAPI, Depends
from typing import Annotated
app = FastAPI()
# Dependencies are functions — FastAPI's IoC container calls them
def get_db():
db = Database("postgres://localhost/mydb")
try:
yield db # Injected into endpoint
finally:
db.close() # Cleanup (like @PreDestroy)
def get_user_service(db: Database = Depends(get_db)):
return UserService(db) # Composed dependency
# FastAPI resolves the entire dependency tree (IoC)
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
service: Annotated[UserService, Depends(get_user_service)],
):
return service.get_by_id(user_id)DI vs Service Locator
| Aspect | Dependency Injection | Service Locator |
|---|---|---|
| Coupling | Low — deps are explicit in constructor | Medium — class depends on locator |
| Testability | Easy — pass mocks directly | Harder — must configure locator for tests |
| Transparency | Dependencies visible in API | Dependencies hidden inside methods |
| Spring approach | @Autowired / constructor |
ApplicationContext.getBean() (discouraged) |
Q3: What is the Template Method Pattern?
Answer:
The Template Method pattern defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the algorithm’s structure. It’s a key pattern behind many frameworks (Spring, Django, JUnit).
graph TD
linkStyle default stroke:#000,color:#000
ABSTRACT["AbstractClass<br/>(defines template)"]
ABSTRACT --> STEP1["step1() — fixed"]
ABSTRACT --> STEP2["step2() — abstract<br/>(subclass overrides)"]
ABSTRACT --> STEP3["step3() — abstract<br/>(subclass overrides)"]
ABSTRACT --> STEP4["step4() — fixed"]
CONCRETE_A["ConcreteClassA<br/>overrides step2, step3"]
CONCRETE_B["ConcreteClassB<br/>overrides step2, step3"]
ABSTRACT -.-> CONCRETE_A
ABSTRACT -.-> CONCRETE_B
style ABSTRACT fill:#8fe0bf,stroke:#333,color:#222
style CONCRETE_A fill:#9fddea,stroke:#333,color:#222
style CONCRETE_B fill:#ffe29a,stroke:#333,color:#222
Java Implementation
// Template method in an abstract class
public abstract class DataExporter {
// TEMPLATE METHOD — defines the algorithm skeleton
// Marked final so subclasses can't change the overall flow
public final void export(String query) {
List<Map<String, Object>> data = fetchData(query); // Step 1
List<Map<String, Object>> transformed = transform(data); // Step 2
String formatted = format(transformed); // Step 3
write(formatted); // Step 4
cleanup(); // Step 5 (hook)
}
// Concrete step — same for all subclasses
private List<Map<String, Object>> fetchData(String query) {
return database.query(query);
}
// Abstract steps — subclasses MUST implement
protected abstract List<Map<String, Object>> transform(
List<Map<String, Object>> data
);
protected abstract String format(List<Map<String, Object>> data);
protected abstract void write(String content);
// Hook method — optional override (default does nothing)
protected void cleanup() { }
}
// Concrete implementation: CSV exporter
public class CsvExporter extends DataExporter {
@Override
protected List<Map<String, Object>> transform(List<Map<String, Object>> data) {
// Remove sensitive columns
data.forEach(row -> row.remove("password"));
return data;
}
@Override
protected String format(List<Map<String, Object>> data) {
StringBuilder sb = new StringBuilder();
// Header
sb.append(String.join(",", data.get(0).keySet())).append("\n");
// Rows
data.forEach(row ->
sb.append(String.join(",",
row.values().stream().map(Object::toString).toList()
)).append("\n")
);
return sb.toString();
}
@Override
protected void write(String content) {
Files.writeString(Path.of("export.csv"), content);
}
}
// Concrete implementation: JSON exporter
public class JsonExporter extends DataExporter {
@Override
protected List<Map<String, Object>> transform(List<Map<String, Object>> data) {
return data; // No transformation needed for JSON
}
@Override
protected String format(List<Map<String, Object>> data) {
return new ObjectMapper().writeValueAsString(data);
}
@Override
protected void write(String content) {
Files.writeString(Path.of("export.json"), content);
}
}
// Usage — algorithm structure is fixed, details vary
DataExporter csvExporter = new CsvExporter();
csvExporter.export("SELECT * FROM users");
DataExporter jsonExporter = new JsonExporter();
jsonExporter.export("SELECT * FROM users");Python Implementation
from abc import ABC, abstractmethod
class DataPipeline(ABC):
"""Template method: extract → validate → transform → load."""
def run(self, source: str) -> int:
"""Template method — defines the pipeline skeleton."""
raw = self.extract(source)
valid = self.validate(raw)
transformed = self.transform(valid)
count = self.load(transformed)
self.on_complete(count) # Hook
return count
@abstractmethod
def extract(self, source: str) -> list[dict]: ...
def validate(self, data: list[dict]) -> list[dict]:
"""Default validation — subclasses can override."""
return [row for row in data if row] # Remove empty rows
@abstractmethod
def transform(self, data: list[dict]) -> list[dict]: ...
@abstractmethod
def load(self, data: list[dict]) -> int: ...
def on_complete(self, count: int) -> None:
"""Hook — optional override."""
pass
class CsvToPostgresPipeline(DataPipeline):
def extract(self, source: str) -> list[dict]:
import csv
with open(source) as f:
return list(csv.DictReader(f))
def transform(self, data: list[dict]) -> list[dict]:
for row in data:
row["email"] = row["email"].lower().strip()
return data
def load(self, data: list[dict]) -> int:
db.bulk_insert("users", data)
return len(data)
def on_complete(self, count: int) -> None:
print(f"Loaded {count} records into PostgreSQL")
# Run the pipeline
pipeline = CsvToPostgresPipeline()
pipeline.run("users.csv")Template Method in Frameworks
| Framework | Template Method | You Override |
|---|---|---|
| Spring MVC | DispatcherServlet.doDispatch() |
@Controller methods |
| JUnit | TestCase.runBare() |
@Test, @BeforeEach |
| Django | View.dispatch() |
get(), post() |
Python unittest |
TestCase.run() |
setUp(), test_*() |
| Java Servlets | HttpServlet.service() |
doGet(), doPost() |
Q4: What is the State Pattern?
Answer:
The State pattern allows an object to alter its behavior when its internal state changes. The object appears to change its class. It’s used to replace complex if/else or switch chains for state-dependent behavior.
graph TD
linkStyle default stroke:#000,color:#000
CONTEXT["Context<br/>(Order)"]
CONTEXT --> STATE["Current State"]
STATE --> S1["PendingState"]
STATE --> S2["PaidState"]
STATE --> S3["ShippedState"]
STATE --> S4["DeliveredState"]
STATE --> S5["CancelledState"]
S1 -->|"pay()"| S2
S2 -->|"ship()"| S3
S3 -->|"deliver()"| S4
S1 -->|"cancel()"| S5
style CONTEXT fill:#8fe0bf,stroke:#333,color:#222
style STATE fill:#ffe29a,stroke:#333,color:#222
Implementation: Order State Machine
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from order import Order
# State interface
class OrderState(ABC):
@abstractmethod
def pay(self, order: "Order") -> None: ...
@abstractmethod
def ship(self, order: "Order") -> None: ...
@abstractmethod
def deliver(self, order: "Order") -> None: ...
@abstractmethod
def cancel(self, order: "Order") -> None: ...
# Concrete states
class PendingState(OrderState):
def pay(self, order: "Order") -> None:
print("Payment processed. Order is now paid.")
order.set_state(PaidState())
def ship(self, order: "Order") -> None:
raise InvalidOperationError("Cannot ship — order not yet paid")
def deliver(self, order: "Order") -> None:
raise InvalidOperationError("Cannot deliver — order not yet shipped")
def cancel(self, order: "Order") -> None:
print("Order cancelled.")
order.set_state(CancelledState())
class PaidState(OrderState):
def pay(self, order: "Order") -> None:
raise InvalidOperationError("Order already paid")
def ship(self, order: "Order") -> None:
print("Order shipped.")
order.set_state(ShippedState())
def deliver(self, order: "Order") -> None:
raise InvalidOperationError("Cannot deliver — order not yet shipped")
def cancel(self, order: "Order") -> None:
print("Refund issued. Order cancelled.")
order.set_state(CancelledState())
class ShippedState(OrderState):
def pay(self, order: "Order") -> None:
raise InvalidOperationError("Order already paid")
def ship(self, order: "Order") -> None:
raise InvalidOperationError("Order already shipped")
def deliver(self, order: "Order") -> None:
print("Order delivered!")
order.set_state(DeliveredState())
def cancel(self, order: "Order") -> None:
raise InvalidOperationError("Cannot cancel — order already shipped")
class DeliveredState(OrderState):
def pay(self, order): raise InvalidOperationError("Order completed")
def ship(self, order): raise InvalidOperationError("Order completed")
def deliver(self, order): raise InvalidOperationError("Already delivered")
def cancel(self, order): raise InvalidOperationError("Cannot cancel delivered order")
class CancelledState(OrderState):
def pay(self, order): raise InvalidOperationError("Order cancelled")
def ship(self, order): raise InvalidOperationError("Order cancelled")
def deliver(self, order): raise InvalidOperationError("Order cancelled")
def cancel(self, order): raise InvalidOperationError("Already cancelled")
class InvalidOperationError(Exception):
pass
# Context
class Order:
def __init__(self, order_id: str):
self.order_id = order_id
self._state: OrderState = PendingState()
def set_state(self, state: OrderState) -> None:
self._state = state
@property
def status(self) -> str:
return type(self._state).__name__.replace("State", "")
def pay(self) -> None:
self._state.pay(self)
def ship(self) -> None:
self._state.ship(self)
def deliver(self) -> None:
self._state.deliver(self)
def cancel(self) -> None:
self._state.cancel(self)
# Usage
order = Order("ORD-001")
print(order.status) # "Pending"
order.pay()
print(order.status) # "Paid"
order.ship()
print(order.status) # "Shipped"
order.deliver()
print(order.status) # "Delivered"
# Invalid transition raises error
try:
order.cancel() # InvalidOperationError: Cannot cancel delivered order
except InvalidOperationError as e:
print(f"Error: {e}")State vs Strategy
| Aspect | State | Strategy |
|---|---|---|
| Purpose | Object changes behavior as state changes | Client chooses algorithm |
| Transitions | States know about and trigger transitions | Client explicitly sets strategy |
| Awareness | States may know about each other | Strategies are independent |
| Example | Order lifecycle, TCP connection | Sorting algorithm, compression |
Q5: What is the Facade Pattern?
Answer:
The Facade pattern provides a simplified interface to a complex subsystem. It doesn’t hide the subsystem — it provides a convenient default interface while still allowing direct access when needed.
graph TD
linkStyle default stroke:#000,color:#000
CLIENT["Client"]
CLIENT --> FACADE["Facade<br/>(simple interface)"]
FACADE --> S1["VideoCodec"]
FACADE --> S2["AudioCodec"]
FACADE --> S3["Muxer"]
FACADE --> S4["FileWriter"]
FACADE --> S5["MetadataParser"]
CLIENT2["Advanced Client"]
CLIENT2 --> S1
CLIENT2 --> S3
style FACADE fill:#8fe0bf,stroke:#333,color:#222
style CLIENT fill:#9fddea,stroke:#333,color:#222
Implementation
# Complex subsystem classes
class VideoCodec:
def decode(self, file: str) -> bytes:
print(f"Decoding video from {file}")
return b"video_data"
def encode(self, data: bytes, format: str) -> bytes:
print(f"Encoding video to {format}")
return b"encoded_video"
class AudioCodec:
def extract(self, file: str) -> bytes:
print(f"Extracting audio from {file}")
return b"audio_data"
def encode(self, data: bytes, bitrate: int) -> bytes:
print(f"Encoding audio at {bitrate}kbps")
return b"encoded_audio"
class Muxer:
def mux(self, video: bytes, audio: bytes) -> bytes:
print("Multiplexing audio and video streams")
return b"muxed_data"
class FileWriter:
def write(self, data: bytes, output: str) -> None:
print(f"Writing to {output}")
# FACADE — simple interface to the complex subsystem
class VideoConverter:
"""Facade that hides the complexity of video conversion."""
def __init__(self):
self._video = VideoCodec()
self._audio = AudioCodec()
self._muxer = Muxer()
self._writer = FileWriter()
def convert(self, input_file: str, output_file: str, format: str = "mp4") -> None:
"""One method does everything — clients don't need to know the steps."""
video_data = self._video.decode(input_file)
audio_data = self._audio.extract(input_file)
encoded_video = self._video.encode(video_data, format)
encoded_audio = self._audio.encode(audio_data, bitrate=192)
muxed = self._muxer.mux(encoded_video, encoded_audio)
self._writer.write(muxed, output_file)
print(f"Conversion complete: {output_file}")
# Client uses the simple facade
converter = VideoConverter()
converter.convert("input.avi", "output.mp4")
# vs manually orchestrating 5 subsystem objectsJava: Spring Boot as a Facade
// Spring Boot itself is a FACADE over the Spring ecosystem!
// Instead of manually configuring:
// - DispatcherServlet
// - ViewResolver
// - DataSource + JPA EntityManager
// - Transaction manager
// - Embedded server (Tomcat)
// You just write:
@SpringBootApplication // Facade annotation — auto-configures everything
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// Service facade example:
@Service
public class CheckoutFacade {
private final InventoryService inventory;
private final PaymentService payment;
private final ShippingService shipping;
private final NotificationService notifications;
public CheckoutFacade(InventoryService inventory, PaymentService payment,
ShippingService shipping, NotificationService notifications) {
this.inventory = inventory;
this.payment = payment;
this.shipping = shipping;
this.notifications = notifications;
}
// One method hides the complexity of checkout
public OrderResult checkout(Cart cart, PaymentInfo paymentInfo) {
inventory.reserve(cart.getItems());
PaymentResult payResult = payment.charge(paymentInfo, cart.getTotal());
ShipmentTracking tracking = shipping.createShipment(cart);
notifications.sendConfirmation(cart.getUserEmail(), tracking);
return new OrderResult(payResult.getTransactionId(), tracking.getId());
}
}When to Use Facade
| Scenario | Example |
|---|---|
| Simplify complex library APIs | Video conversion, PDF generation |
| Decouple client from subsystem | Checkout process hiding 5 services |
| Provide default configuration | Spring Boot auto-configuration |
| Layer boundaries | Service layer facade over repositories |
| Legacy system wrapping | Clean API over messy legacy code |
Q6: What is the Flyweight Pattern?
Answer:
The Flyweight pattern reduces memory usage by sharing common state across many objects instead of storing it in each instance. It splits object state into intrinsic (shared) and extrinsic (unique per context) parts.
graph TD
linkStyle default stroke:#000,color:#000
FACTORY["Flyweight Factory<br/>(caches shared objects)"]
FACTORY --> FW1["Flyweight 'A'<br/>(intrinsic: font, size, color)"]
FACTORY --> FW2["Flyweight 'B'"]
FACTORY --> FW3["Flyweight 'C'"]
C1["Context 1<br/>(extrinsic: position x=10, y=20)"]
C2["Context 2<br/>(extrinsic: position x=50, y=80)"]
C3["Context 3<br/>(extrinsic: position x=30, y=60)"]
C1 --> FW1
C2 --> FW1
C3 --> FW2
NOTE["Contexts 1 & 2 share<br/>the same Flyweight"]
style FACTORY fill:#8fe0bf,stroke:#333,color:#222
style FW1 fill:#9fddea,stroke:#333,color:#222
style NOTE fill:#ffe29a,stroke:#333,color:#222
Implementation: Text Editor Character Rendering
from dataclasses import dataclass
# Flyweight — stores intrinsic (shared) state
@dataclass(frozen=True) # Immutable — safe to share
class CharacterStyle:
"""Shared state: font properties that many characters share."""
font_family: str
font_size: int
is_bold: bool
is_italic: bool
color: str
# Flyweight Factory — caches and reuses styles
class StyleFactory:
_cache: dict[tuple, CharacterStyle] = {}
@classmethod
def get_style(
cls,
font_family: str = "Arial",
font_size: int = 12,
is_bold: bool = False,
is_italic: bool = False,
color: str = "black",
) -> CharacterStyle:
key = (font_family, font_size, is_bold, is_italic, color)
if key not in cls._cache:
cls._cache[key] = CharacterStyle(*key)
return cls._cache[key]
@classmethod
def cache_size(cls) -> int:
return len(cls._cache)
# Context — stores extrinsic (unique) state
@dataclass
class Character:
"""Each character has unique position but shares style."""
char: str
row: int
col: int
style: CharacterStyle # Reference to shared flyweight
# Usage: 10,000 characters but only a few style objects
document: list[Character] = []
normal = StyleFactory.get_style() # Cached
bold = StyleFactory.get_style(is_bold=True) # Cached
heading = StyleFactory.get_style(font_size=24, is_bold=True) # Cached
# All 'normal' characters share the SAME style object
for i, char in enumerate("This is a long document with many characters..."):
document.append(Character(char, row=0, col=i, style=normal))
# Bold words share the bold style object
for i, char in enumerate("IMPORTANT"):
document.append(Character(char, row=1, col=i, style=bold))
print(f"Characters: {len(document)}") # 56
print(f"Unique styles: {StyleFactory.cache_size()}") # 3 (not 56!)
# Memory saved: 53 fewer CharacterStyle objectsJava: String Pool as Flyweight
// Java's String pool IS the Flyweight pattern!
String a = "hello"; // From string pool
String b = "hello"; // Same object from pool
System.out.println(a == b); // true — same reference
// Integer cache is also Flyweight
Integer x = Integer.valueOf(127); // Cached (-128 to 127)
Integer y = Integer.valueOf(127);
System.out.println(x == y); // true — same object from cache
// Boolean.TRUE, Boolean.FALSE — only 2 instances everWhen to Use Flyweight
| Use When | Don’t Use When |
|---|---|
| Millions of similar objects | Few objects in memory |
| Objects share most of their state | Each object is unique |
| Memory is a bottleneck | CPU is the bottleneck |
| Immutable shared state | Shared state needs modification |
| Game particles, text characters, map tiles | Business entities with distinct state |
Q7: What is the Mediator Pattern?
Answer:
The Mediator pattern reduces chaotic dependencies between objects by forcing them to communicate only through a mediator object. Instead of N objects knowing about each other (N² connections), they all talk through one mediator (N connections).
graph TD
linkStyle default stroke:#000,color:#000
subgraph Without["Without Mediator (N² connections)"]
A1["Button"] <--> B1["TextBox"]
A1 <--> C1["Dropdown"]
B1 <--> C1
A1 <--> D1["Checkbox"]
B1 <--> D1
C1 <--> D1
end
subgraph With["With Mediator (N connections)"]
M["Mediator<br/>(Dialog)"]
A2["Button"] --> M
B2["TextBox"] --> M
C2["Dropdown"] --> M
D2["Checkbox"] --> M
M --> A2
M --> B2
M --> C2
M --> D2
end
style Without fill:#ff9f84,stroke:#333,color:#222
style With fill:#8fe0bf,stroke:#333,color:#222
style M fill:#9fddea,stroke:#333,color:#222
Implementation: Chat Room
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
# Mediator interface
class ChatMediator(ABC):
@abstractmethod
def send_message(self, message: str, sender: "User") -> None: ...
@abstractmethod
def add_user(self, user: "User") -> None: ...
# Concrete mediator
class ChatRoom(ChatMediator):
def __init__(self, name: str):
self.name = name
self._users: list["User"] = []
def add_user(self, user: "User") -> None:
self._users.append(user)
user.chat_room = self
self.send_message(f"{user.name} joined the room", user)
def send_message(self, message: str, sender: "User") -> None:
timestamp = datetime.now().strftime("%H:%M")
for user in self._users:
if user != sender: # Don't echo to sender
user.receive(f"[{timestamp}] {sender.name}: {message}")
# Colleague
@dataclass
class User:
name: str
chat_room: ChatMediator | None = None
messages: list[str] = field(default_factory=list)
def send(self, message: str) -> None:
if self.chat_room:
self.chat_room.send_message(message, self)
def receive(self, message: str) -> None:
self.messages.append(message)
print(f" {self.name} received: {message}")
# Usage — users don't know about each other, only the mediator
room = ChatRoom("Engineering")
alice = User("Alice")
bob = User("Bob")
charlie = User("Charlie")
room.add_user(alice)
room.add_user(bob)
room.add_user(charlie)
alice.send("Hey team, standup in 5 minutes!")
# Bob received: [10:00] Alice: Hey team, standup in 5 minutes!
# Charlie received: [10:00] Alice: Hey team, standup in 5 minutes!
bob.send("On my way!")
# Alice received: [10:00] Bob: On my way!
# Charlie received: [10:00] Bob: On my way!Mediator in Enterprise Systems
// Spring's ApplicationEventPublisher is a Mediator!
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher; // Mediator
public Order createOrder(OrderRequest request) {
Order order = orderRepo.save(new Order(request));
// Publish event — OrderService doesn't know who handles it
eventPublisher.publishEvent(new OrderCreatedEvent(order));
return order;
}
}
// Listeners are decoupled — they don't know about each other
@Component
public class InventoryListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
inventoryService.reserve(event.getOrder().getItems());
}
}
@Component
public class NotificationListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
emailService.sendConfirmation(event.getOrder());
}
}Mediator vs Observer
| Aspect | Mediator | Observer |
|---|---|---|
| Communication | Bidirectional through mediator | One-way (subject → observers) |
| Coupling | Components know the mediator | Observers don’t know each other |
| Complexity | Mediator can become complex (God object) | Simpler, distributed logic |
| Example | Chat room, air traffic control | Event bus, pub/sub |
Q8: What is the Repository Pattern?
Answer:
The Repository pattern mediates between the domain/business layer and data access, providing a collection-like interface for accessing domain objects. It isolates the domain from persistence concerns and is a cornerstone of both Spring Data and Domain-Driven Design.
graph TD
linkStyle default stroke:#000,color:#000
SERVICE["Service Layer<br/>(business logic)"]
SERVICE --> REPO["Repository Interface<br/>findById(), save(), delete()"]
REPO --> IMPL1["JPA Implementation"]
REPO --> IMPL2["MongoDB Implementation"]
REPO --> IMPL3["In-Memory (tests)"]
IMPL1 --> DB1["PostgreSQL"]
IMPL2 --> DB2["MongoDB"]
IMPL3 --> DB3["HashMap"]
style REPO fill:#8fe0bf,stroke:#333,color:#222
style SERVICE fill:#9fddea,stroke:#333,color:#222
Spring Data JPA Repository
// Spring Data auto-generates the implementation!
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Method name IS the query — Spring generates SQL
List<User> findByEmail(String email);
List<User> findByRoleAndActiveTrue(String role);
@Query("SELECT u FROM User u WHERE u.createdAt > :since")
List<User> findRecentUsers(@Param("since") LocalDateTime since);
// Pagination built in
Page<User> findByRole(String role, Pageable pageable);
}
// Entity
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
private String role;
private boolean active;
private LocalDateTime createdAt;
// Getters, setters, constructors...
}
// Service uses repository — doesn't know about JPA/SQL
@Service
public class UserService {
private final UserRepository userRepo;
public UserService(UserRepository userRepo) {
this.userRepo = userRepo;
}
public User createUser(CreateUserRequest request) {
if (!userRepo.findByEmail(request.getEmail()).isEmpty()) {
throw new DuplicateEmailException(request.getEmail());
}
User user = new User(request.getName(), request.getEmail());
return userRepo.save(user);
}
public Page<User> listAdmins(int page, int size) {
return userRepo.findByRole("admin", PageRequest.of(page, size));
}
}Python Repository Pattern
from typing import Protocol, TypeVar, Generic
from dataclasses import dataclass
T = TypeVar("T")
# Generic repository interface
class Repository(Protocol[T]):
def get_by_id(self, id: int) -> T | None: ...
def list_all(self) -> list[T]: ...
def save(self, entity: T) -> T: ...
def delete(self, id: int) -> bool: ...
# Domain entity
@dataclass
class User:
id: int | None
name: str
email: str
role: str = "user"
# PostgreSQL implementation
class PostgresUserRepository:
def __init__(self, db_pool):
self._pool = db_pool
async def get_by_id(self, id: int) -> User | None:
row = await self._pool.fetchrow(
"SELECT * FROM users WHERE id = $1", id
)
return User(**dict(row)) if row else None
async def list_all(self) -> list[User]:
rows = await self._pool.fetch("SELECT * FROM users")
return [User(**dict(r)) for r in rows]
async def save(self, user: User) -> User:
if user.id is None:
row = await self._pool.fetchrow(
"INSERT INTO users (name, email, role) VALUES ($1, $2, $3) RETURNING id",
user.name, user.email, user.role,
)
user.id = row["id"]
else:
await self._pool.execute(
"UPDATE users SET name=$1, email=$2, role=$3 WHERE id=$4",
user.name, user.email, user.role, user.id,
)
return user
async def delete(self, id: int) -> bool:
result = await self._pool.execute(
"DELETE FROM users WHERE id = $1", id
)
return result == "DELETE 1"
# In-memory implementation (for testing)
class InMemoryUserRepository:
def __init__(self):
self._store: dict[int, User] = {}
self._next_id = 1
async def get_by_id(self, id: int) -> User | None:
return self._store.get(id)
async def list_all(self) -> list[User]:
return list(self._store.values())
async def save(self, user: User) -> User:
if user.id is None:
user.id = self._next_id
self._next_id += 1
self._store[user.id] = user
return user
async def delete(self, id: int) -> bool:
return self._store.pop(id, None) is not None
# Service works with either implementation
class UserService:
def __init__(self, repo: Repository[User]):
self._repo = repoRepository vs DAO vs Active Record
| Pattern | Description | Example |
|---|---|---|
| Repository | Domain-oriented collection interface | Spring Data, custom repos |
| DAO | Table-oriented data access | JDBC DAOs, lower-level |
| Active Record | Entity knows how to persist itself | Django ORM, Rails |
| Data Mapper | Separate mapper objects | SQLAlchemy (mapper mode) |
Q9: What is the MVC Pattern and how is it implemented in Python web frameworks?
Answer:
Model-View-Controller (MVC) separates an application into three concerns: the Model (data + business logic), the View (presentation), and the Controller (handles input and coordinates model/view).
graph TD
linkStyle default stroke:#000,color:#000
USER["User (Browser)"]
USER -->|"HTTP Request"| CONTROLLER["Controller<br/>(handles input)"]
CONTROLLER -->|"calls"| MODEL["Model / Service<br/>(business logic)"]
MODEL -->|"returns data"| CONTROLLER
CONTROLLER -->|"selects + populates"| VIEW["View<br/>(renders response)"]
VIEW -->|"HTTP Response"| USER
subgraph Spring["Spring MVC"]
DS["DispatcherServlet<br/>(Front Controller)"]
DS --> HM["Handler Mapping<br/>(route → controller)"]
DS --> HA["Handler Adapter<br/>(invoke controller)"]
DS --> VR["View Resolver<br/>(template → HTML)"]
end
style CONTROLLER fill:#8fe0bf,stroke:#333,color:#222
style MODEL fill:#9fddea,stroke:#333,color:#222
style VIEW fill:#ffe29a,stroke:#333,color:#222
style DS fill:#ff9f84,stroke:#333,color:#222
style Spring fill:#fff
Python MVC Implementation (FastAPI)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
# MODEL
class Product(BaseModel):
id: int | None = None
name: str
price: float
stock: int
class ProductService:
def __init__(self):
self._products: dict[int, Product] = {}
self._next_id = 1
def list_all(self) -> list[Product]:
return list(self._products.values())
def get_by_id(self, product_id: int) -> Product:
product = self._products.get(product_id)
if product is None:
raise KeyError(f"Product {product_id} not found")
return product
def create(self, product: Product) -> Product:
product.id = self._next_id
self._products[self._next_id] = product
self._next_id += 1
return product
service = ProductService()
# CONTROLLER (JSON endpoints)
@app.get("/api/v1/products", response_model=list[Product])
def list_products():
return service.list_all()
@app.get("/api/v1/products/{product_id}", response_model=Product)
def get_product(product_id: int):
try:
return service.get_by_id(product_id)
except KeyError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post("/api/v1/products", response_model=Product, status_code=201)
def create_product(product: Product):
return service.create(product)MVC Variants
| Variant | View Layer | Controller Returns | Example |
|---|---|---|---|
| MVC (classic) | Server-side templates | View name + Model | Spring MVC + Thymeleaf |
| REST API (no view) | Client (React, mobile) | JSON directly | @RestController |
| MVP | Passive view | Updates view directly | Android (legacy) |
| MVVM | Data-bound view | ViewModel + bindings | WPF, Vue.js, SwiftUI |
Q10: What is the CQRS Pattern?
Answer:
Command Query Responsibility Segregation (CQRS) splits the data model into separate command (write) and query (read) models. Writes and reads often have different requirements — CQRS lets you optimize each independently.
graph TD
linkStyle default stroke:#000,color:#000
CLIENT["Client"]
CLIENT -->|"Write (Command)"| CMD["Command Handler<br/>(validates, applies rules)"]
CLIENT -->|"Read (Query)"| QUERY["Query Handler<br/>(optimized for reads)"]
CMD --> WRITE_DB["Write Model<br/>(normalized, consistent)"]
QUERY --> READ_DB["Read Model<br/>(denormalized, fast)"]
WRITE_DB -->|"Events / Sync"| READ_DB
style CMD fill:#ff9f84,stroke:#333,color:#222
style QUERY fill:#8fe0bf,stroke:#333,color:#222
style WRITE_DB fill:#9fddea,stroke:#333,color:#222
style READ_DB fill:#ffe29a,stroke:#333,color:#222
Implementation
from dataclasses import dataclass, field
from datetime import datetime
from abc import ABC, abstractmethod
# COMMANDS (writes)
@dataclass(frozen=True)
class CreateOrderCommand:
user_id: int
items: list[dict]
shipping_address: str
@dataclass(frozen=True)
class CancelOrderCommand:
order_id: int
reason: str
# Command handler — enforces business rules
class OrderCommandHandler:
def __init__(self, write_repo, event_bus):
self._repo = write_repo
self._events = event_bus
def handle_create(self, cmd: CreateOrderCommand) -> int:
# Validate business rules
if not cmd.items:
raise ValueError("Order must have at least one item")
# Create in write model (normalized)
order = Order(
user_id=cmd.user_id,
items=cmd.items,
address=cmd.shipping_address,
status="pending",
created_at=datetime.now(),
)
self._repo.save(order)
# Publish event to sync read model
self._events.publish("order_created", order_id=order.id, data=order.to_dict())
return order.id
def handle_cancel(self, cmd: CancelOrderCommand) -> None:
order = self._repo.get(cmd.order_id)
if order.status == "shipped":
raise ValueError("Cannot cancel shipped order")
order.status = "cancelled"
order.cancel_reason = cmd.reason
self._repo.save(order)
self._events.publish("order_cancelled", order_id=order.id)
# QUERIES (reads)
@dataclass(frozen=True)
class GetOrderQuery:
order_id: int
@dataclass(frozen=True)
class ListUserOrdersQuery:
user_id: int
page: int = 1
page_size: int = 20
# Query handler — optimized for read performance
class OrderQueryHandler:
def __init__(self, read_repo):
self._repo = read_repo # Denormalized read model
def handle_get(self, query: GetOrderQuery) -> dict:
# Read from denormalized view (fast, no JOINs)
return self._repo.get_order_view(query.order_id)
def handle_list(self, query: ListUserOrdersQuery) -> dict:
orders, total = self._repo.list_user_orders(
query.user_id, query.page, query.page_size
)
return {"items": orders, "total": total, "page": query.page}
# Read model sync (event listener)
class OrderReadModelUpdater:
"""Listens to write events and updates the denormalized read model."""
def __init__(self, read_repo):
self._repo = read_repo
def on_order_created(self, event: dict) -> None:
# Denormalize: pre-join user name, item details, totals
self._repo.upsert_order_view({
"order_id": event["order_id"],
"user_name": user_service.get_name(event["data"]["user_id"]),
"items_summary": summarize_items(event["data"]["items"]),
"total": calculate_total(event["data"]["items"]),
"status": "pending",
"created_at": event["data"]["created_at"],
})
# API layer
@app.post("/api/v1/orders", status_code=201)
async def create_order(cmd: CreateOrderCommand):
order_id = command_handler.handle_create(cmd)
return {"order_id": order_id}
@app.get("/api/v1/orders/{order_id}")
async def get_order(order_id: int):
return query_handler.handle_get(GetOrderQuery(order_id))Python CQRS with Separate Models
from sqlalchemy import DateTime, Integer, String, Numeric
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from datetime import datetime
class Base(DeclarativeBase):
pass
# Write model — normalized, validated
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(Integer, nullable=False)
status: Mapped[str] = mapped_column(String(32), default="pending")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
# Read model — denormalized for fast queries
class OrderView(Base):
__tablename__ = "order_views"
order_id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_name: Mapped[str] = mapped_column(String(255), nullable=False)
items_summary: Mapped[str] = mapped_column(String(1024), nullable=False)
total: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
# Repositories are intentionally separate (write repo vs read repo)
class OrderRepository:
def __init__(self, session):
self.session = session
class OrderViewRepository:
def __init__(self, session):
self.session = session
def find_by_user_name(self, name: str, limit: int = 20, offset: int = 0):
stmt = (
self.session.query(OrderView)
.filter(OrderView.user_name.ilike(f"%{name}%"))
.offset(offset)
.limit(limit)
)
return stmt.all()When to Use CQRS
| Use CQRS | Don’t Use CQRS |
|---|---|
| Read and write workloads differ significantly | Simple CRUD applications |
| Read model needs denormalization | Reads and writes have same shape |
| Scaling reads and writes independently | Small teams / simple domains |
| Event sourcing architecture | When eventual consistency is unacceptable |
| Complex reporting / search requirements | When added complexity isn’t justified |
Summary Table
| # | Pattern | Category | Key Idea |
|---|---|---|---|
| 1 | Python DI | IoC / Creational | Dependencies injected via constructors, provider functions, or Depends() |
| 2 | Inversion of Control | Principle | Framework controls object creation and flow |
| 3 | Template Method | Behavioral | Algorithm skeleton in base class, steps in subclasses |
| 4 | State | Behavioral | Object behavior changes with internal state transitions |
| 5 | Facade | Structural | Simplified interface to a complex subsystem |
| 6 | Flyweight | Structural | Share common state across many objects to save memory |
| 7 | Mediator | Behavioral | Central hub reduces N² dependencies to N |
| 8 | Repository | Data Access | Collection-like interface isolating domain from persistence |
| 9 | MVC | Architectural | Separate Model, View, Controller concerns |
| 10 | CQRS | Architectural | Separate read and write models for independent optimization |
What’s Next?
This article covered enterprise and framework-level patterns. For related content:
- Core GoF patterns: Design Pattern Interview QA - 1
- Python design patterns (Pythonic style): Python SWE Interview QA - 3
- Production API patterns: Python SWE Interview QA - 4
- Python fundamentals: Python SWE Interview QA - 1
- LLM architecture: LLM Interview QA - 1