Building a [System Name] - A Complete Interview Walkthrough
"Let me walk you through how I would design this system step by step, thinking through the problem like a senior engineer."
Phase 1: Problem Understanding & Requirements Gathering
π― Clarifying Questions (Interview Strategy)
"Before I start coding, let me make sure I understand the problem correctly..."
Functional Requirements:
- What are the core use cases? (List 3-5 primary flows)
- Who are the different types of users/actors?
- What are the key business rules and constraints?
- Are there any specific workflow requirements?
Non-Functional Requirements:
- Expected scale (users, transactions per second)
- Performance expectations (response times)
- Availability requirements
- Consistency vs. Eventual consistency needs
- Security considerations
Assumptions & Constraints:
- Technology stack preferences
- Integration requirements
- Budget/resource constraints
Phase 2: Domain Modeling & Entity Extraction
ποΈ Identifying Core Entities
"Let me start by identifying the key domain objects and their relationships..."
Entity Discovery Process:
- Noun Extraction: From requirements, extract key nouns
- Responsibility Assignment: What does each entity own/manage?
- Relationship Mapping: How do entities interact?
- Lifecycle Management: Creation, updates, state transitions
Core Entities Identified:
// Example structure - adapt based on your system
public class EntityA {
private String id;
private EntityStatus status;
private LocalDateTime createdAt;
// Core properties
}
public class EntityB {
private String id;
private List<EntityA> relatedEntities;
// Relationships
}
Entity Relationship Diagram:
[EntityA] ----< has many >---- [EntityB]
| |
| |
[EntityC] ----< belongs to >---- [EntityD]
Phase 3: Use Case Analysis & Service Boundaries
π¬ Core Use Cases
"Now let me break down the key user journeys and identify service boundaries..."
Use Case 1: [Primary Flow]
- Actor: [User Type]
- Preconditions: [What must be true]
-
Main Flow:
- Step 1
- Step 2
- Step 3
- Postconditions: [End state]
- Exception Flows: [Error scenarios]
Use Case 2: [Secondary Flow]
- Similar structure...
Service Boundary Analysis:
// High-level service interfaces
public interface UserManagementService {
User createUser(CreateUserRequest request);
User getUserById(String userId);
void updateUser(String userId, UpdateUserRequest request);
}
public interface CoreBusinessService {
// Main business operations
BusinessResult performPrimaryOperation(OperationRequest request);
List<BusinessEntity> getEntitiesByFilter(FilterCriteria criteria);
}
Phase 4: Core Class Design with SOLID Principles
π¨ Applying SOLID Principles
"Let me design the core classes following SOLID principles to ensure maintainability..."
Single Responsibility Principle (SRP)
// β Violates SRP - handling multiple concerns
public class UserManager {
public void createUser() { /* user creation */ }
public void sendEmail() { /* email sending */ }
public void logActivity() { /* logging */ }
}
// β
Follows SRP - each class has one reason to change
public class UserService {
private final UserRepository userRepository;
private final NotificationService notificationService;
private final AuditService auditService;
public User createUser(CreateUserRequest request) {
// Focus only on user business logic
User user = new User(request.getName(), request.getEmail());
User savedUser = userRepository.save(user);
notificationService.sendWelcomeEmail(savedUser);
auditService.logUserCreation(savedUser);
return savedUser;
}
}
public class NotificationService {
public void sendWelcomeEmail(User user) {
// Focus only on notifications
}
}
public class AuditService {
public void logUserCreation(User user) {
// Focus only on auditing
}
}
Open/Closed Principle (OCP)
// β
Open for extension, closed for modification
public abstract class PaymentProcessor {
public final PaymentResult processPayment(PaymentRequest request) {
validateRequest(request);
PaymentResult result = executePayment(request);
logTransaction(result);
return result;
}
protected abstract PaymentResult executePayment(PaymentRequest request);
protected abstract void validateRequest(PaymentRequest request);
private void logTransaction(PaymentResult result) {
// Common logging logic
}
}
public class CreditCardProcessor extends PaymentProcessor {
@Override
protected PaymentResult executePayment(PaymentRequest request) {
// Credit card specific implementation
return new PaymentResult(SUCCESS, "CC-" + UUID.randomUUID());
}
@Override
protected void validateRequest(PaymentRequest request) {
// Credit card specific validation
}
}
public class PayPalProcessor extends PaymentProcessor {
@Override
protected PaymentResult executePayment(PaymentRequest request) {
// PayPal specific implementation
return new PaymentResult(SUCCESS, "PP-" + UUID.randomUUID());
}
@Override
protected void validateRequest(PaymentRequest request) {
// PayPal specific validation
}
}
Liskov Substitution Principle (LSP)
// β
Subtypes are substitutable for their base types
public interface Vehicle {
void start();
void stop();
int getMaxSpeed();
}
public class Car implements Vehicle {
@Override
public void start() {
// Start engine
}
@Override
public void stop() {
// Apply brakes
}
@Override
public int getMaxSpeed() {
return 200; // km/h
}
}
public class ElectricCar implements Vehicle {
@Override
public void start() {
// Start electric motor - behaves consistently
}
@Override
public void stop() {
// Regenerative braking - still stops the vehicle
}
@Override
public int getMaxSpeed() {
return 250; // km/h
}
}
// Client code works with any Vehicle implementation
public class VehicleService {
public void operateVehicle(Vehicle vehicle) {
vehicle.start();
System.out.println("Max speed: " + vehicle.getMaxSpeed());
vehicle.stop();
}
}
Interface Segregation Principle (ISP)
// β Fat interface - forces implementations to depend on methods they don't need
public interface Worker {
void work();
void eat();
void sleep();
}
// β
Segregated interfaces - clients depend only on what they need
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public class Human implements Workable, Eatable, Sleepable {
@Override
public void work() { /* human work */ }
@Override
public void eat() { /* human eat */ }
@Override
public void sleep() { /* human sleep */ }
}
public class Robot implements Workable {
@Override
public void work() { /* robot work */ }
// Robot doesn't need to eat or sleep
}
Dependency Inversion Principle (DIP)
// β
Depend on abstractions, not concretions
public interface UserRepository {
User save(User user);
Optional<User> findById(String id);
List<User> findByStatus(UserStatus status);
}
public interface EmailService {
void sendEmail(String to, String subject, String body);
}
public class UserService {
private final UserRepository userRepository; // Abstraction
private final EmailService emailService; // Abstraction
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public User createUser(CreateUserRequest request) {
User user = new User(request.getName(), request.getEmail());
User savedUser = userRepository.save(user);
emailService.sendEmail(
savedUser.getEmail(),
"Welcome!",
"Welcome to our platform!"
);
return savedUser;
}
}
// Concrete implementations
public class DatabaseUserRepository implements UserRepository {
// Database-specific implementation
}
public class SMTPEmailService implements EmailService {
// SMTP-specific implementation
}
Phase 5: Design Patterns Integration
π Strategic Pattern Application
"Let me integrate relevant design patterns to solve specific design challenges..."
Factory Pattern - Object Creation
public interface PaymentProcessor {
PaymentResult process(PaymentRequest request);
}
public class PaymentProcessorFactory {
private final Map<PaymentMethod, Supplier<PaymentProcessor>> processors;
public PaymentProcessorFactory() {
processors = Map.of(
PaymentMethod.CREDIT_CARD, CreditCardProcessor::new,
PaymentMethod.PAYPAL, PayPalProcessor::new,
PaymentMethod.BANK_TRANSFER, BankTransferProcessor::new
);
}
public PaymentProcessor createProcessor(PaymentMethod method) {
Supplier<PaymentProcessor> processorSupplier = processors.get(method);
if (processorSupplier == null) {
throw new UnsupportedPaymentMethodException("Unsupported payment method: " + method);
}
return processorSupplier.get();
}
}
// Usage
public class PaymentService {
private final PaymentProcessorFactory factory;
public PaymentResult processPayment(PaymentRequest request) {
PaymentProcessor processor = factory.createProcessor(request.getPaymentMethod());
return processor.process(request);
}
}
Strategy Pattern - Algorithm Selection
public interface PricingStrategy {
BigDecimal calculatePrice(PricingContext context);
}
public class RegularPricingStrategy implements PricingStrategy {
@Override
public BigDecimal calculatePrice(PricingContext context) {
return context.getBasePrice();
}
}
public class PremiumPricingStrategy implements PricingStrategy {
@Override
public BigDecimal calculatePrice(PricingContext context) {
return context.getBasePrice().multiply(BigDecimal.valueOf(1.2));
}
}
public class DiscountPricingStrategy implements PricingStrategy {
private final BigDecimal discountPercentage;
public DiscountPricingStrategy(BigDecimal discountPercentage) {
this.discountPercentage = discountPercentage;
}
@Override
public BigDecimal calculatePrice(PricingContext context) {
BigDecimal discount = context.getBasePrice().multiply(discountPercentage);
return context.getBasePrice().subtract(discount);
}
}
public class PricingService {
public BigDecimal calculatePrice(PricingContext context, PricingStrategy strategy) {
return strategy.calculatePrice(context);
}
}
Observer Pattern - Event Handling
public interface EventPublisher {
void subscribe(EventType eventType, EventListener listener);
void unsubscribe(EventType eventType, EventListener listener);
void publish(Event event);
}
public interface EventListener {
void handle(Event event);
}
public class EventPublisherImpl implements EventPublisher {
private final Map<EventType, List<EventListener>> listeners = new HashMap<>();
@Override
public void subscribe(EventType eventType, EventListener listener) {
listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener);
}
@Override
public void publish(Event event) {
List<EventListener> eventListeners = listeners.get(event.getType());
if (eventListeners != null) {
eventListeners.forEach(listener -> {
try {
listener.handle(event);
} catch (Exception e) {
// Log error but don't stop other listeners
log.error("Error handling event", e);
}
});
}
}
}
// Usage
public class UserService {
private final EventPublisher eventPublisher;
public User createUser(CreateUserRequest request) {
User user = userRepository.save(new User(request));
// Publish event for other services to react
eventPublisher.publish(new UserCreatedEvent(user));
return user;
}
}
public class NotificationService implements EventListener {
@Override
public void handle(Event event) {
if (event instanceof UserCreatedEvent) {
UserCreatedEvent userEvent = (UserCreatedEvent) event;
sendWelcomeEmail(userEvent.getUser());
}
}
}
Command Pattern - Operation Encapsulation
public interface Command {
CommandResult execute();
void undo();
String getDescription();
}
public class CreateUserCommand implements Command {
private final UserService userService;
private final CreateUserRequest request;
private User createdUser;
public CreateUserCommand(UserService userService, CreateUserRequest request) {
this.userService = userService;
this.request = request;
}
@Override
public CommandResult execute() {
try {
createdUser = userService.createUser(request);
return CommandResult.success(createdUser);
} catch (Exception e) {
return CommandResult.failure(e.getMessage());
}
}
@Override
public void undo() {
if (createdUser != null) {
userService.deleteUser(createdUser.getId());
}
}
@Override
public String getDescription() {
return "Create user: " + request.getEmail();
}
}
public class CommandExecutor {
private final Stack<Command> executedCommands = new Stack<>();
public CommandResult execute(Command command) {
CommandResult result = command.execute();
if (result.isSuccess()) {
executedCommands.push(command);
}
return result;
}
public void undoLast() {
if (!executedCommands.isEmpty()) {
Command lastCommand = executedCommands.pop();
lastCommand.undo();
}
}
}
Phase 6: Advanced Architecture Patterns
ποΈ Architectural Considerations
"Let me address some key architectural patterns for scalability and maintainability..."
Repository Pattern - Data Access Abstraction
public interface Repository<T, ID> {
T save(T entity);
Optional<T> findById(ID id);
List<T> findAll();
void delete(T entity);
void deleteById(ID id);
}
public interface UserRepository extends Repository<User, String> {
List<User> findByStatus(UserStatus status);
Optional<User> findByEmail(String email);
List<User> findCreatedAfter(LocalDateTime date);
}
public class DatabaseUserRepository implements UserRepository {
private final JdbcTemplate jdbcTemplate;
private final RowMapper<User> userRowMapper;
@Override
public User save(User user) {
if (user.getId() == null) {
return insert(user);
} else {
return update(user);
}
}
@Override
public Optional<User> findById(String id) {
String sql = "SELECT * FROM users WHERE id = ?";
try {
User user = jdbcTemplate.queryForObject(sql, userRowMapper, id);
return Optional.of(user);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
@Override
public List<User> findByStatus(UserStatus status) {
String sql = "SELECT * FROM users WHERE status = ?";
return jdbcTemplate.query(sql, userRowMapper, status.name());
}
}
Unit of Work Pattern - Transaction Management
public interface UnitOfWork {
void begin();
void commit();
void rollback();
boolean isActive();
}
public class TransactionalUnitOfWork implements UnitOfWork {
private final DataSource dataSource;
private Connection connection;
private boolean active = false;
@Override
public void begin() {
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false);
active = true;
} catch (SQLException e) {
throw new TransactionException("Failed to begin transaction", e);
}
}
@Override
public void commit() {
if (!active) {
throw new IllegalStateException("No active transaction");
}
try {
connection.commit();
active = false;
} catch (SQLException e) {
rollback();
throw new TransactionException("Failed to commit transaction", e);
} finally {
closeConnection();
}
}
@Override
public void rollback() {
if (active) {
try {
connection.rollback();
active = false;
} catch (SQLException e) {
throw new TransactionException("Failed to rollback transaction", e);
} finally {
closeConnection();
}
}
}
}
// Usage in service layer
public class UserService {
private final UserRepository userRepository;
private final UnitOfWork unitOfWork;
@Transactional
public User createUserWithProfile(CreateUserRequest request) {
unitOfWork.begin();
try {
User user = userRepository.save(new User(request));
Profile profile = profileRepository.save(new Profile(user.getId(), request.getProfile()));
unitOfWork.commit();
return user;
} catch (Exception e) {
unitOfWork.rollback();
throw new UserCreationException("Failed to create user with profile", e);
}
}
}
Phase 7: Error Handling & Validation
π‘οΈ Robust Error Handling
"Let me implement comprehensive error handling and validation..."
Custom Exception Hierarchy
// Base application exception
public abstract class ApplicationException extends Exception {
private final ErrorCode errorCode;
private final Map<String, Object> context;
protected ApplicationException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
protected ApplicationException(ErrorCode errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
public ErrorCode getErrorCode() { return errorCode; }
public Map<String, Object> getContext() { return context; }
public ApplicationException withContext(String key, Object value) {
this.context.put(key, value);
return this;
}
}
// Domain-specific exceptions
public class UserNotFoundException extends ApplicationException {
public UserNotFoundException(String userId) {
super(ErrorCode.USER_NOT_FOUND, "User not found with ID: " + userId);
withContext("userId", userId);
}
}
public class ValidationException extends ApplicationException {
public ValidationException(String message, List<ValidationError> errors) {
super(ErrorCode.VALIDATION_FAILED, message);
withContext("validationErrors", errors);
}
}
public enum ErrorCode {
USER_NOT_FOUND("USER_001", "User not found"),
VALIDATION_FAILED("VAL_001", "Validation failed"),
PAYMENT_FAILED("PAY_001", "Payment processing failed"),
INSUFFICIENT_BALANCE("BAL_001", "Insufficient balance");
private final String code;
private final String description;
ErrorCode(String code, String description) {
this.code = code;
this.description = description;
}
}
Validation Framework
public interface Validator<T> {
ValidationResult validate(T object);
}
public class ValidationResult {
private final boolean valid;
private final List<ValidationError> errors;
public static ValidationResult success() {
return new ValidationResult(true, Collections.emptyList());
}
public static ValidationResult failure(List<ValidationError> errors) {
return new ValidationResult(false, errors);
}
public ValidationResult combine(ValidationResult other) {
if (this.valid && other.valid) {
return success();
}
List<ValidationError> combinedErrors = new ArrayList<>(this.errors);
combinedErrors.addAll(other.errors);
return failure(combinedErrors);
}
}
public class CreateUserRequestValidator implements Validator<CreateUserRequest> {
@Override
public ValidationResult validate(CreateUserRequest request) {
List<ValidationError> errors = new ArrayList<>();
if (StringUtils.isBlank(request.getEmail())) {
errors.add(new ValidationError("email", "Email is required"));
} else if (!EmailValidator.isValid(request.getEmail())) {
errors.add(new ValidationError("email", "Email format is invalid"));
}
if (StringUtils.isBlank(request.getName())) {
errors.add(new ValidationError("name", "Name is required"));
} else if (request.getName().length() < 2) {
errors.add(new ValidationError("name", "Name must be at least 2 characters"));
}
if (request.getAge() != null && request.getAge() < 18) {
errors.add(new ValidationError("age", "Age must be at least 18"));
}
return errors.isEmpty() ? ValidationResult.success() : ValidationResult.failure(errors);
}
}
// Integration in service layer
public class UserService {
private final Validator<CreateUserRequest> createUserValidator;
public User createUser(CreateUserRequest request) throws ValidationException {
ValidationResult validation = createUserValidator.validate(request);
if (!validation.isValid()) {
throw new ValidationException("User creation validation failed", validation.getErrors());
}
// Proceed with user creation
return userRepository.save(new User(request));
}
}
Phase 8: Testing Strategy
π§ͺ Comprehensive Testing Approach
"Let me outline the testing strategy for this design..."
Unit Tests
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private NotificationService notificationService;
@Mock
private Validator<CreateUserRequest> validator;
@InjectMocks
private UserService userService;
@Test
void createUser_ValidRequest_ReturnsCreatedUser() {
// Given
CreateUserRequest request = new CreateUserRequest("john@example.com", "John Doe");
User expectedUser = new User("john@example.com", "John Doe");
when(validator.validate(request)).thenReturn(ValidationResult.success());
when(userRepository.save(any(User.class))).thenReturn(expectedUser);
// When
User actualUser = userService.createUser(request);
// Then
assertThat(actualUser).isEqualTo(expectedUser);
verify(notificationService).sendWelcomeEmail(expectedUser);
verify(userRepository).save(any(User.class));
}
@Test
void createUser_InvalidRequest_ThrowsValidationException() {
// Given
CreateUserRequest request = new CreateUserRequest("", "");
List<ValidationError> errors = Arrays.asList(
new ValidationError("email", "Email is required"),
new ValidationError("name", "Name is required")
);
when(validator.validate(request)).thenReturn(ValidationResult.failure(errors));
// When & Then
ValidationException exception = assertThrows(
ValidationException.class,
() -> userService.createUser(request)
);
assertThat(exception.getContext().get("validationErrors")).isEqualTo(errors);
verify(userRepository, never()).save(any(User.class));
}
}
Integration Tests
@SpringBootTest
@Testcontainers
class UserServiceIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
@Transactional
void createUser_EndToEndFlow_PersistsUserCorrectly() {
// Given
CreateUserRequest request = new CreateUserRequest("john@example.com", "John Doe");
// When
User createdUser = userService.createUser(request);
// Then
assertThat(createdUser.getId()).isNotNull();
assertThat(createdUser.getEmail()).isEqualTo("john@example.com");
assertThat(createdUser.getName()).isEqualTo("John Doe");
// Verify persistence
Optional<User> retrievedUser = userRepository.findById(createdUser.getId());
assertThat(retrievedUser).isPresent();
assertThat(retrievedUser.get()).isEqualTo(createdUser);
}
}
Phase 9: Performance & Scalability Considerations
β‘ Optimization Strategies
"Let me address key performance and scalability considerations..."
Caching Strategy
@Service
public class CachedUserService implements UserService {
private final UserService delegate;
private final Cache<String, User> userCache;
public CachedUserService(UserService delegate, CacheManager cacheManager) {
this.delegate = delegate;
this.userCache = cacheManager.getCache("users", String.class, User.class);
}
@Override
public User getUserById(String userId) {
return userCache.get(userId, () -> {
return delegate.getUserById(userId);
});
}
@Override
public User createUser(CreateUserRequest request) {
User user = delegate.createUser(request);
userCache.put(user.getId(), user);
return user;
}
@Override
public User updateUser(String userId, UpdateUserRequest request) {
User user = delegate.updateUser(userId, request);
userCache.put(userId, user);
return user;
}
}
Async Processing
@Service
public class AsyncNotificationService implements NotificationService {
private final EmailService emailService;
private final ExecutorService executorService;
public AsyncNotificationService(EmailService emailService) {
this.emailService = emailService;
this.executorService = Executors.newFixedThreadPool(10);
}
@Override
public void sendWelcomeEmail(User user) {
CompletableFuture.runAsync(() -> {
try {
emailService.sendEmail(
user.getEmail(),
"Welcome!",
generateWelcomeMessage(user)
);
} catch (Exception e) {
// Log error but don't fail the main operation
log.error("Failed to send welcome email to user: " + user.getId(), e);
}
}, executorService);
}
}
Phase 10: Monitoring & Observability
π Production-Ready Monitoring
"Let me add monitoring and observability features..."
Metrics and Health Checks
@Component
public class UserServiceMetrics {
private final Counter userCreationCounter;
private final Timer userCreationTimer;
private final Gauge activeUsersGauge;
public UserServiceMetrics(MeterRegistry meterRegistry) {
this.userCreationCounter = Counter.builder("user.creation.count")
.description("Number of users created")
.register(meterRegistry);
this.userCreationTimer = Timer.builder("user.creation.duration")
.description("Time taken to create user")
.register(meterRegistry);
this.activeUsersGauge = Gauge.builder("user.active.count")
.description("Number of active users")
.register(meterRegistry, this, UserServiceMetrics::getActiveUserCount);
}
public void recordUserCreation(Duration duration) {
userCreationCounter.increment();
userCreationTimer.record(duration);
}
private double getActiveUserCount() {
// Implementation to get active user count
return 0.0;
}
}
@Service
public class InstrumentedUserService implements UserService {
private final UserService delegate;
private final UserServiceMetrics metrics;
@Override
public User createUser(CreateUserRequest request) {
Timer.Sample sample = Timer.start();
try {
User user = delegate.createUser(request);
metrics.recordUserCreation(sample.stop(metrics.getUserCreationTimer()));
return user;
} catch (Exception e) {
// Record error metrics
throw e;
}
}
}
Summary: Key Design Decisions & Trade-offs
π― Architecture Highlights
SOLID Principles Applied:
- SRP: Each class has a single, well-defined responsibility
- OCP: System is extensible without modifying existing code
- LSP: Implementations are substitutable for their abstractions
- ISP: Interfaces are focused and cohesive
- DIP: Dependencies flow toward abstractions, not concretions
Design Patterns Utilized:
- Factory: Clean object creation with extensible product families
- Strategy: Flexible algorithm selection and business rule management
- Observer: Loose coupling for event-driven architecture
- Command: Operation encapsulation with undo capabilities
- Repository: Clean separation between business logic and data access
Scalability Features:
- Caching strategy for performance optimization
- Async processing for non-critical operations
- Event-driven architecture for loose coupling
- Comprehensive monitoring and observability
Quality Attributes Addressed:
- Maintainability: Clean code structure with clear responsibilities
- Extensibility: Open for extension through interfaces and patterns
- Testability: Dependency injection enables comprehensive testing
- Reliability: Robust error handling and validation
- Performance: Caching and async processing optimizations
π Next Steps & Enhancements
Immediate Improvements:
- Add API rate limiting and security
- Implement distributed caching (Redis)
- Add database connection pooling
- Implement circuit breaker pattern for external service calls
Advanced Enhancements:
- Microservices decomposition with proper service boundaries
- Event sourcing for audit trails and state reconstruction
- CQRS (Command Query Responsibility Segregation) for read/write optimization
- API versioning strategy for backward compatibility
- Distributed tracing for request flow visibility
Phase 11: Security & Resilience Patterns
π Security Implementation
"Let me add enterprise-grade security patterns..."
Authentication & Authorization
public interface SecurityContext {
User getCurrentUser();
boolean hasPermission(String permission);
boolean hasRole(String role);
}
public class JWTSecurityContext implements SecurityContext {
private final JWTToken token;
private final UserService userService;
private final PermissionService permissionService;
public JWTSecurityContext(String jwtToken, UserService userService, PermissionService permissionService) {
this.token = JWTToken.parse(jwtToken);
this.userService = userService;
this.permissionService = permissionService;
}
@Override
public User getCurrentUser() {
return userService.getUserById(token.getUserId());
}
@Override
public boolean hasPermission(String permission) {
return permissionService.userHasPermission(token.getUserId(), permission);
}
}
// Security interceptor
@Component
public class SecurityInterceptor {
private final SecurityContext securityContext;
@Before("@annotation(RequiresPermission)")
public void checkPermission(JoinPoint joinPoint, RequiresPermission annotation) {
if (!securityContext.hasPermission(annotation.value())) {
throw new AccessDeniedException("Insufficient permissions: " + annotation.value());
}
}
}
// Usage
@Service
public class UserService {
@RequiresPermission("USER_CREATE")
public User createUser(CreateUserRequest request) {
// Implementation
}
@RequiresPermission("USER_DELETE")
public void deleteUser(String userId) {
// Implementation
}
}
Input Sanitization & Validation
@Component
public class InputSanitizer {
private final Set<String> allowedTags = Set.of("b", "i", "u", "p", "br");
public String sanitizeHtml(String input) {
if (input == null) return null;
return Jsoup.clean(input, Whitelist.relaxed()
.addTags(allowedTags.toArray(String[]::new))
.removeAttributes("script", "onclick", "onload"));
}
public String sanitizeSql(String input) {
if (input == null) return null;
// Remove SQL injection patterns
return input.replaceAll("(?i)(union|select|insert|delete|update|drop|create|alter|exec|script)", "");
}
}
@Component
public class SecureCreateUserRequestValidator implements Validator<CreateUserRequest> {
private final InputSanitizer sanitizer;
@Override
public ValidationResult validate(CreateUserRequest request) {
// Sanitize inputs first
request.setName(sanitizer.sanitizeHtml(request.getName()));
request.setEmail(sanitizer.sanitizeSql(request.getEmail()));
// Then validate
List<ValidationError> errors = new ArrayList<>();
// Email validation with additional security checks
if (!EmailValidator.isValid(request.getEmail())) {
errors.add(new ValidationError("email", "Invalid email format"));
}
if (containsSuspiciousPatterns(request.getName())) {
errors.add(new ValidationError("name", "Name contains invalid characters"));
}
return errors.isEmpty() ? ValidationResult.success() : ValidationResult.failure(errors);
}
private boolean containsSuspiciousPatterns(String input) {
String[] suspiciousPatterns = {"<script", "javascript:", "data:", "vbscript:"};
String lowerInput = input.toLowerCase();
return Arrays.stream(suspiciousPatterns).anyMatch(lowerInput::contains);
}
}
Circuit Breaker Pattern
public class CircuitBreaker {
private final String name;
private final int failureThreshold;
private final long timeoutDuration;
private final long retryTimePeriod;
private int failureCount = 0;
private long lastFailureTime = 0;
private CircuitBreakerState state = CircuitBreakerState.CLOSED;
public enum CircuitBreakerState {
CLOSED, OPEN, HALF_OPEN
}
public <T> T execute(Supplier<T> operation) {
if (state == CircuitBreakerState.OPEN) {
if (System.currentTimeMillis() - lastFailureTime > retryTimePeriod) {
state = CircuitBreakerState.HALF_OPEN;
} else {
throw new CircuitBreakerOpenException("Circuit breaker is OPEN for: " + name);
}
}
try {
T result = operation.get();
onSuccess();
return result;
} catch (Exception e) {
onFailure();
throw new CircuitBreakerException("Operation failed in circuit breaker: " + name, e);
}
}
private void onSuccess() {
failureCount = 0;
state = CircuitBreakerState.CLOSED;
}
private void onFailure() {
failureCount++;
lastFailureTime = System.currentTimeMillis();
if (failureCount >= failureThreshold) {
state = CircuitBreakerState.OPEN;
}
}
}
// Usage in external service calls
@Service
public class ExternalPaymentService {
private final PaymentGatewayClient paymentClient;
private final CircuitBreaker circuitBreaker;
public ExternalPaymentService(PaymentGatewayClient paymentClient) {
this.paymentClient = paymentClient;
this.circuitBreaker = new CircuitBreaker("payment-gateway", 5, 5000, 60000);
}
public PaymentResult processPayment(PaymentRequest request) {
return circuitBreaker.execute(() -> {
return paymentClient.processPayment(request);
});
}
}
Phase 12: Advanced Architectural Patterns
ποΈ Domain-Driven Design Integration
"Let me incorporate DDD concepts for complex business domains..."
Domain Model with Aggregates
// Value Objects
@Immutable
public class Email {
private final String value;
public Email(String value) {
if (!EmailValidator.isValid(value)) {
throw new IllegalArgumentException("Invalid email format: " + value);
}
this.value = value.toLowerCase().trim();
}
public String getValue() { return value; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Email)) return false;
Email email = (Email) o;
return Objects.equals(value, email.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
@Immutable
public class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
this.amount = amount.setScale(2, RoundingMode.HALF_UP);
this.currency = currency;
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(BigDecimal multiplier) {
return new Money(this.amount.multiply(multiplier), this.currency);
}
public boolean isGreaterThan(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot compare different currencies");
}
return this.amount.compareTo(other.amount) > 0;
}
}
// Entity
public class User {
private final UserId id;
private Email email;
private String name;
private UserStatus status;
private final LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Constructor for new users
public User(Email email, String name) {
this.id = UserId.generate();
this.email = email;
this.name = name;
this.status = UserStatus.ACTIVE;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
// Domain event
DomainEvents.publish(new UserCreatedEvent(this));
}
// Business methods
public void updateEmail(Email newEmail) {
if (!this.email.equals(newEmail)) {
Email oldEmail = this.email;
this.email = newEmail;
this.updatedAt = LocalDateTime.now();
DomainEvents.publish(new UserEmailChangedEvent(this.id, oldEmail, newEmail));
}
}
public void deactivate(String reason) {
if (this.status != UserStatus.ACTIVE) {
throw new IllegalStateException("Cannot deactivate user that is not active");
}
this.status = UserStatus.INACTIVE;
this.updatedAt = LocalDateTime.now();
DomainEvents.publish(new UserDeactivatedEvent(this.id, reason));
}
public boolean canPerformAction(String action) {
return this.status == UserStatus.ACTIVE;
}
}
// Aggregate Root
public class Order {
private final OrderId id;
private final UserId userId;
private final List<OrderItem> items;
private OrderStatus status;
private Money totalAmount;
private final LocalDateTime createdAt;
public Order(UserId userId) {
this.id = OrderId.generate();
this.userId = userId;
this.items = new ArrayList<>();
this.status = OrderStatus.DRAFT;
this.totalAmount = Money.zero(Currency.USD);
this.createdAt = LocalDateTime.now();
}
public void addItem(Product product, int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
if (this.status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot modify confirmed order");
}
OrderItem item = new OrderItem(product, quantity);
this.items.add(item);
this.totalAmount = calculateTotal();
DomainEvents.publish(new OrderItemAddedEvent(this.id, item));
}
public void confirm() {
if (this.items.isEmpty()) {
throw new IllegalStateException("Cannot confirm empty order");
}
if (this.status != OrderStatus.DRAFT) {
throw new IllegalStateException("Order is already confirmed");
}
this.status = OrderStatus.CONFIRMED;
DomainEvents.publish(new OrderConfirmedEvent(this));
}
private Money calculateTotal() {
return items.stream()
.map(item -> item.getProduct().getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(Money.zero(Currency.USD), Money::add);
}
// Invariant checks
private void checkInvariants() {
if (status == OrderStatus.CONFIRMED && items.isEmpty()) {
throw new IllegalStateException("Confirmed order cannot be empty");
}
if (totalAmount.isLessThan(Money.zero(Currency.USD))) {
throw new IllegalStateException("Order total cannot be negative");
}
}
}
Domain Services
@DomainService
public class OrderPricingService {
private final DiscountService discountService;
private final TaxService taxService;
public OrderPricing calculatePricing(Order order, User user) {
Money subtotal = order.getSubtotal();
Money discount = discountService.calculateDiscount(order, user);
Money tax = taxService.calculateTax(subtotal.subtract(discount), user.getAddress());
Money total = subtotal.subtract(discount).add(tax);
return new OrderPricing(subtotal, discount, tax, total);
}
}
@DomainService
public class UserRegistrationService {
private final UserRepository userRepository;
private final EmailService emailService;
public User registerUser(RegisterUserCommand command) {
// Business rule: Email must be unique
if (userRepository.existsByEmail(command.getEmail())) {
throw new DuplicateEmailException("Email already registered: " + command.getEmail());
}
// Business rule: New users start with basic role
User user = new User(command.getEmail(), command.getName());
user.assignRole(Role.BASIC_USER);
User savedUser = userRepository.save(user);
// Send welcome email as part of registration process
emailService.sendWelcomeEmail(savedUser);
return savedUser;
}
}
Domain Events
public abstract class DomainEvent {
private final String eventId;
private final LocalDateTime occurredAt;
private final String aggregateId;
protected DomainEvent(String aggregateId) {
this.eventId = UUID.randomUUID().toString();
this.occurredAt = LocalDateTime.now();
this.aggregateId = aggregateId;
}
// Getters...
}
public class UserCreatedEvent extends DomainEvent {
private final String userId;
private final String email;
private final String name;
public UserCreatedEvent(User user) {
super(user.getId().getValue());
this.userId = user.getId().getValue();
this.email = user.getEmail().getValue();
this.name = user.getName();
}
}
public class OrderConfirmedEvent extends DomainEvent {
private final String orderId;
private final String userId;
private final Money totalAmount;
private final List<OrderItem> items;
public OrderConfirmedEvent(Order order) {
super(order.getId().getValue());
this.orderId = order.getId().getValue();
this.userId = order.getUserId().getValue();
this.totalAmount = order.getTotalAmount();
this.items = new ArrayList<>(order.getItems());
}
}
// Domain Event Publisher
public class DomainEvents {
private static final ThreadLocal<List<DomainEvent>> events = new ThreadLocal<>();
public static void publish(DomainEvent event) {
if (events.get() == null) {
events.set(new ArrayList<>());
}
events.get().add(event);
}
public static List<DomainEvent> getEvents() {
return events.get() != null ? events.get() : Collections.emptyList();
}
public static void clear() {
events.remove();
}
}
// Application Service handling domain events
@Service
@Transactional
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final DomainEventPublisher eventPublisher;
public void confirmOrder(ConfirmOrderCommand command) {
Order order = orderRepository.findById(command.getOrderId())
.orElseThrow(() -> new OrderNotFoundException(command.getOrderId()));
order.confirm();
orderRepository.save(order);
// Publish domain events
List<DomainEvent> events = DomainEvents.getEvents();
events.forEach(eventPublisher::publish);
DomainEvents.clear();
}
}
Phase 13: Microservices Considerations
π Service Decomposition Strategy
"Let me show how this design could evolve into microservices..."
Service Boundaries
// User Service
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
User user = userService.createUser(request);
return ResponseEntity.ok(UserResponse.from(user));
}
@GetMapping("/{userId}")
public ResponseEntity<UserResponse> getUser(@PathVariable String userId) {
User user = userService.getUserById(userId);
return ResponseEntity.ok(UserResponse.from(user));
}
}
// Order Service
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@Valid @RequestBody CreateOrderRequest request) {
Order order = orderService.createOrder(request);
return ResponseEntity.ok(OrderResponse.from(order));
}
@PostMapping("/{orderId}/confirm")
public ResponseEntity<Void> confirmOrder(@PathVariable String orderId) {
orderService.confirmOrder(orderId);
return ResponseEntity.ok().build();
}
}
Inter-Service Communication
// Service Client with Circuit Breaker
@Component
public class UserServiceClient {
private final WebClient webClient;
private final CircuitBreaker circuitBreaker;
public UserServiceClient(WebClient.Builder webClientBuilder,
@Value("${services.user.base-url}") String userServiceUrl) {
this.webClient = webClientBuilder.baseUrl(userServiceUrl).build();
this.circuitBreaker = new CircuitBreaker("user-service", 5, 5000, 60000);
}
public Optional<User> getUserById(String userId) {
return circuitBreaker.execute(() -> {
return webClient.get()
.uri("/api/v1/users/{userId}", userId)
.retrieve()
.bodyToMono(UserResponse.class)
.map(UserResponse::toUser)
.blockOptional(Duration.ofSeconds(5));
});
}
}
// Async Event-Based Communication
@Component
public class OrderEventHandler {
private final NotificationService notificationService;
private final InventoryService inventoryService;
@EventListener
@Async
public void handleOrderConfirmed(OrderConfirmedEvent event) {
// Send notification
CompletableFuture.runAsync(() -> {
notificationService.sendOrderConfirmation(event.getUserId(), event.getOrderId());
});
// Update inventory
CompletableFuture.runAsync(() -> {
event.getItems().forEach(item -> {
inventoryService.reserveItem(item.getProductId(), item.getQuantity());
});
});
}
}
Saga Pattern for Distributed Transactions
public interface SagaStep {
void execute();
void compensate();
String getStepName();
}
public class CreateOrderSaga {
private final List<SagaStep> steps;
private final List<SagaStep> executedSteps;
public CreateOrderSaga() {
this.steps = new ArrayList<>();
this.executedSteps = new ArrayList<>();
}
public CreateOrderSaga addStep(SagaStep step) {
this.steps.add(step);
return this;
}
public void execute() {
try {
for (SagaStep step : steps) {
step.execute();
executedSteps.add(step);
}
} catch (Exception e) {
compensate();
throw new SagaExecutionException("Saga failed at step: " + e.getMessage(), e);
}
}
private void compensate() {
// Compensate in reverse order
Collections.reverse(executedSteps);
for (SagaStep step : executedSteps) {
try {
step.compensate();
} catch (Exception e) {
// Log compensation failure but continue
log.error("Compensation failed for step: " + step.getStepName(), e);
}
}
}
}
// Usage
@Service
public class OrderSagaService {
public void processOrder(CreateOrderRequest request) {
CreateOrderSaga saga = new CreateOrderSaga()
.addStep(new ValidateUserStep(request.getUserId()))
.addStep(new ReserveInventoryStep(request.getItems()))
.addStep(new ProcessPaymentStep(request.getPayment()))
.addStep(new CreateOrderStep(request))
.addStep(new SendNotificationStep(request.getUserId()));
saga.execute();
}
}
public class ReserveInventoryStep implements SagaStep {
private final InventoryService inventoryService;
private final List<OrderItem> items;
private String reservationId;
@Override
public void execute() {
reservationId = inventoryService.reserveItems(items);
}
@Override
public void compensate() {
if (reservationId != null) {
inventoryService.releaseReservation(reservationId);
}
}
@Override
public String getStepName() {
return "ReserveInventory";
}
}
Phase 14: Production Deployment Considerations
π Deployment & Operations
"Let me address production deployment and operational concerns..."
Health Checks & Readiness Probes
@Component
public class ApplicationHealthIndicator implements HealthIndicator {
private final DataSource dataSource;
private final RedisTemplate redisTemplate;
@Override
public Health health() {
Health.Builder builder = new Health.Builder();
try {
checkDatabase();
checkRedis();
checkExternalServices();
return builder.up()
.withDetail("database", "Available")
.withDetail("cache", "Available")
.withDetail("external-services", "Available")
.build();
} catch (Exception e) {
return builder.down()
.withDetail("error", e.getMessage())
.build();
}
}
private void checkDatabase() throws Exception {
try (Connection connection = dataSource.getConnection()) {
if (!connection.isValid(5)) {
throw new Exception("Database connection invalid");
}
}
}
private void checkRedis() throws Exception {
redisTemplate.opsForValue().set("health-check", "ok", Duration.ofSeconds(10));
String result = redisTemplate.opsForValue().get("health-check");
if (!"ok".equals(result)) {
throw new Exception("Redis health check failed");
}
}
}
@RestController
@RequestMapping("/actuator")
public class CustomHealthController {
@GetMapping("/health/detailed")
public ResponseEntity<Map<String, Object>> detailedHealth() {
Map<String, Object> health = new HashMap<>();
health.put("status", "UP");
health.put("timestamp", Instant.now());
health.put("version", getClass().getPackage().getImplementationVersion());
health.put("uptime", ManagementFactory.getRuntimeMXBean().getUptime());
return ResponseEntity.ok(health);
}
}
Configuration Management
@ConfigurationProperties(prefix = "app")
@Validated
public class ApplicationConfig {
@NotBlank
private String name;
@Valid
private DatabaseConfig database;
@Valid
private SecurityConfig security;
@Valid
private ExternalServicesConfig externalServices;
public static class DatabaseConfig {
@Min(1) @Max(100)
private int maxPoolSize = 20;
@Min(1000)
private long connectionTimeoutMs = 5000;
@Min(1)
private int maxRetryAttempts = 3;
}
public static class SecurityConfig {
@NotBlank
private String jwtSecret;
@Min(300) // At least 5 minutes
private long jwtExpirationSeconds = 3600;
private boolean enableCsrfProtection = true;
}
public static class ExternalServicesConfig {
@Valid
private ServiceConfig paymentService;
@Valid
private ServiceConfig notificationService;
public static class ServiceConfig {
@NotBlank
private String baseUrl;
@Min(1000)
private long timeoutMs = 10000;
@Min(1)
private int maxRetries = 3;
@Min(1)
private int circuitBreakerThreshold = 5;
}
}
}
Graceful Shutdown
@Component
public class GracefulShutdownManager implements ApplicationListener<ContextClosedEvent> {
private final ExecutorService taskExecutor;
private final List<Closeable> resources;
public GracefulShutdownManager(ExecutorService taskExecutor) {
this.taskExecutor = taskExecutor;
this.resources = new ArrayList<>();
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
log.info("Starting graceful shutdown...");
// Stop accepting new requests
shutdownTaskExecutor();
// Close external resources
closeResources();
log.info("Graceful shutdown completed");
}
private void shutdownTaskExecutor() {
taskExecutor.shutdown();
try {
if (!taskExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
log.warn("Tasks didn't finish within 30 seconds, forcing shutdown");
taskExecutor.shutdownNow();
}
} catch (InterruptedException e) {
taskExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
private void closeResources() {
for (Closeable resource : resources) {
try {
resource.close();
} catch (Exception e) {
log.error("Error closing resource: " + resource.getClass().getSimpleName(), e);
}
}
}
public void addResource(Closeable resource) {
resources.add(resource);
}
}
Interview Tips & Best Practices
π‘ Storytelling Strategy
1. Start with Clarifying Questions
- "Let me make sure I understand the requirements correctly..."
- "What's the expected scale and performance requirements?"
- "Are there any specific constraints I should be aware of?"
2. Think Out Loud
- "I'm identifying the core entities from the problem statement..."
- "Let me apply the Single Responsibility Principle here..."
- "I'm choosing the Strategy pattern because we need flexible algorithm selection..."
3. Justify Design Decisions
- "I'm using the Repository pattern to abstract data access because..."
- "The Observer pattern fits here because we need loose coupling for..."
- "I'm implementing caching at this layer to improve performance..."
4. Address Trade-offs
- "The trade-off here is between consistency and performance..."
- "We could use eventual consistency here, but that means..."
- "This approach scales well but increases complexity..."
5. Show Evolution Thinking
- "If we needed to scale this further, we could..."
- "For microservices, we'd break this down along these boundaries..."
- "The monitoring strategy would include these metrics..."
π― Key Success Factors
Technical Excellence:
- Clean, readable, production-quality code
- Proper application of SOLID principles
- Strategic use of design patterns
- Comprehensive error handling and validation
- Performance and scalability considerations
System Design Thinking:
- Clear separation of concerns
- Proper abstraction layers
- Scalable architecture patterns
- Resilience and fault tolerance
- Monitoring and observability
Interview Performance:
- Clear communication and reasoning
- Collaborative problem-solving approach
- Ability to handle follow-up questions
- Demonstration of real-world experience
- Balance between depth and breadth
Conclusion
This template provides a comprehensive framework for tackling any low-level design interview. The key is to adapt the specific examples to your target system while maintaining the same structured approach:
- Understand the problem thoroughly
- Model the domain with proper entities and relationships
- Design with SOLID principles and clean code practices
- Apply relevant design patterns strategically
- Consider scalability, performance, and operational concerns
- Communicate your reasoning clearly throughout
Remember: The goal isn't to memorize this template, but to internalize the thinking process and adapt it to any system you're asked to design. Practice with different domains (e-commerce, social media, booking systems, etc.) to build confidence and fluency.
Final tip: Always be prepared to dive deeper into any component you design. Interviewers often pick one area and ask for more detailed implementation, so ensure you can explain the internals of your design choices.
Top comments (0)