Skip to content
LowLevelDesign Mastery

Assign Responsibilities

Master responsibility assignment - the key to clean, maintainable code design!

Assigning responsibilities correctly is crucial for creating maintainable, testable, and scalable code. Poor responsibility assignment leads to god classes, tight coupling, and code that’s hard to understand and modify.

Assigning responsibilities correctly turns a hard to test god class design into single responsibility classes that are easy to maintain

A responsibility is a reason for a class to change. Each class should have one primary responsibility - one reason to exist and one reason to change.

  1. Data Management - Storing and managing data
  2. Business Logic - Implementing business rules
  3. Coordination - Coordinating between other classes
  4. Validation - Validating inputs and states
  5. Communication - Interacting with external systems
Five responsibility types in LLD: data management, business logic, coordination, validation and communication

Definition: A class should have only one reason to change.

What this means:

  • Each class should do one thing and do it well
  • If a class has multiple reasons to change, split it
  • Changes to one responsibility shouldn’t affect others
Single Responsibility Principle in action: UserManager split into UserValidator, UserRepository, EmailService and ReportGenerator
  1. Identify what each entity needs to do
  2. Group related operations
  3. Assign to appropriate class
  4. Ensure single responsibility
  5. Check for cohesion

Entities Identified:

  • Book
  • Member
  • Loan
  • Reservation
  • Fine

Let’s assign responsibilities:

What does a Book need to do?

  • Store book information (title, author, ISBN)
  • Track availability status
  • Know its location in library

Book Responsibilities:

💡 Tip: Click dropdown to switch between languages
book.py
class Book:
# Data Management
def __init__(self, isbn: str, title: str, author: str):
self.isbn = isbn
self.title = title
self.author = author
self.is_available = True
# State Management
def mark_as_borrowed(self) -> None:
self.is_available = False
def mark_as_available(self) -> None:
self.is_available = True
def is_available(self) -> bool:
return self.is_available

Responsibility: Manage book data and availability state

What does a Loan need to do?

  • Store loan information (book, member, dates)
  • Calculate due date
  • Check if overdue
  • Calculate fine amount

Loan Responsibilities:

💡 Tip: Click dropdown to switch between languages
loan.py
class Loan:
def __init__(self, book: Book, member: Member, loan_date: datetime):
self.book = book
self.member = member
self.loan_date = loan_date
self.due_date = self._calculate_due_date()
self.return_date = None
# Business Logic
def _calculate_due_date(self) -> datetime:
return self.loan_date + timedelta(days=14)
def is_overdue(self) -> bool:
if self.return_date:
return False
return datetime.now() > self.due_date
def calculate_fine(self) -> float:
if not self.is_overdue():
return 0.0
days_overdue = (datetime.now() - self.due_date).days
return days_overdue * 1.0 # $1 per day
# State Management
def return_book(self) -> None:
self.return_date = datetime.now()

Responsibility: Manage loan lifecycle and calculate fines

Library system responsibility assignment: Book tracks availability, Loan computes due dates and fines, Member checks eligibility, Library coordinates

Responsibility: Store and manage data

Example:

💡 Tip: Click dropdown to switch between languages
member.py
class Member:
def __init__(self, member_id: str, name: str, email: str):
self.member_id = member_id
self.name = name
self.email = email
self.membership_type = "STANDARD"
def update_membership(self, new_type: str) -> None:
self.membership_type = new_type

When to use: When you need to store data with minimal logic

Responsibility: Implement business rules and calculations

Example:

💡 Tip: Click dropdown to switch between languages
fine_calculator.py
class FineCalculator:
def calculate_fine(self, loan: Loan) -> float:
if not loan.is_overdue():
return 0.0
days_overdue = (datetime.now() - loan.due_date).days
# Business rule: $1 per day for first 7 days, $2 per day after
if days_overdue <= 7:
return days_overdue * 1.0
else:
return 7.0 + (days_overdue - 7) * 2.0

When to use: When you have complex business rules

Responsibility: Coordinate between multiple classes

Example:

💡 Tip: Click dropdown to switch between languages
library_service.py
class LibraryService:
def __init__(self, book_repository: BookRepository,
loan_repository: LoanRepository,
fine_calculator: FineCalculator):
self.book_repository = book_repository
self.loan_repository = loan_repository
self.fine_calculator = fine_calculator
def borrow_book(self, book_id: str, member_id: str) -> Loan:
# Coordinate between multiple classes
book = self.book_repository.find_by_id(book_id)
member = self.member_repository.find_by_id(member_id)
if not book.is_available():
raise ValueError("Book not available")
loan = Loan(book, member, datetime.now())
self.loan_repository.save(loan)
book.mark_as_borrowed()
return loan

When to use: When you need to coordinate multiple operations

Responsibility: Validate inputs and states

Example:

💡 Tip: Click dropdown to switch between languages
loan_validator.py
class LoanValidator:
def validate_loan(self, book: Book, member: Member) -> bool:
if not book.is_available():
raise ValueError("Book is not available")
if member.has_overdue_books():
raise ValueError("Member has overdue books")
if member.get_active_loans_count() >= 5:
raise ValueError("Member has reached loan limit")
return True

When to use: When you need to validate before operations

Responsibility: Handle data persistence

Example:

💡 Tip: Click dropdown to switch between languages
book_repository.py
class BookRepository:
def save(self, book: Book) -> None:
# Save to database
pass
def find_by_id(self, book_id: str) -> Book:
# Retrieve from database
pass
def find_available_books(self) -> List[Book]:
# Query database
pass

When to use: When you need to abstract data access


Part 4: Common Mistakes and How to Avoid Them

Section titled “Part 4: Common Mistakes and How to Avoid Them”

Mistake 1: God Class (Too Many Responsibilities)

Section titled “Mistake 1: God Class (Too Many Responsibilities)”

Bad Example:

💡 Tip: Click dropdown to switch between languages
god_class_bad.py
class LibraryManager:
# Too many responsibilities!
def add_book(self, book: Book): pass
def remove_book(self, book_id: str): pass
def borrow_book(self, book_id: str, member_id: str): pass
def return_book(self, loan_id: str): pass
def calculate_fine(self, loan_id: str): float: pass
def send_notification(self, member_id: str, message: str): pass
def generate_report(self) -> Report: pass
def process_payment(self, member_id: str, amount: float): pass

Problems:

  • Too many reasons to change
  • Hard to test
  • Hard to maintain
  • Violates SRP

Good Example:

💡 Tip: Click dropdown to switch between languages
god_class_good.py
class BookService:
def add_book(self, book: Book): pass
def remove_book(self, book_id: str): pass
class LoanService:
def borrow_book(self, book_id: str, member_id: str): pass
def return_book(self, loan_id: str): pass
class FineService:
def calculate_fine(self, loan_id: str) -> float: pass
class NotificationService:
def send_notification(self, member_id: str, message: str): pass
class ReportService:
def generate_report(self) -> Report: pass
class PaymentService:
def process_payment(self, member_id: str, amount: float): pass
God class vs good design: a LibraryManager with six responsibilities split into BookService, LoanService and FineService

Mistake 2: Anemic Domain Model (Too Few Responsibilities)

Section titled “Mistake 2: Anemic Domain Model (Too Few Responsibilities)”

Bad Example:

💡 Tip: Click dropdown to switch between languages
anemic_bad.py
class Book:
# Only data, no behavior!
def __init__(self, isbn: str, title: str):
self.isbn = isbn
self.title = title
self.is_available = True
class BookManager: # All logic in manager
def mark_as_borrowed(self, book: Book):
book.is_available = False
def mark_as_available(self, book: Book):
book.is_available = True

Problems:

  • Data and behavior separated
  • Objects are just data containers
  • Logic scattered across managers

Good Example:

💡 Tip: Click dropdown to switch between languages
anemic_good.py
class Book:
def __init__(self, isbn: str, title: str):
self.isbn = isbn
self.title = title
self._is_available = True
# Behavior with data
def mark_as_borrowed(self) -> None:
self._is_available = False
def mark_as_available(self) -> None:
self._is_available = True
def is_available(self) -> bool:
return self._is_available

Bad Example:

💡 Tip: Click dropdown to switch between languages
wrong_class_bad.py
class Book:
def send_overdue_notification(self, member: Member):
# Book shouldn't send notifications!
email_service.send(member.email, "Book is overdue")

Problem: Book is responsible for sending notifications (should be NotificationService)

Good Example:

💡 Tip: Click dropdown to switch between languages
wrong_class_good.py
class Book:
def is_overdue(self) -> bool:
return datetime.now() > self.due_date
class NotificationService:
def send_overdue_notification(self, book: Book, member: Member):
if book.is_overdue():
email_service.send(member.email, "Book is overdue")

Part 5: Responsibility Assignment Framework

Section titled “Part 5: Responsibility Assignment Framework”
Responsibility assignment decision flowchart mapping each task to a data holder, business logic class, coordinator, validator or adapter

For each responsibility, ask:

  1. Who owns this data? → Assign to that class
  2. What is the primary purpose? → Assign to class with that purpose
  3. Does it need coordination? → Create a coordinator/service
  4. Is it reusable? → Create a separate utility/service
  5. Does it violate SRP? → Split into multiple classes

Entities:

  • ParkingLot
  • ParkingSpot
  • Vehicle
  • Ticket
  • Payment

Responsibility Assignment:

What should ParkingLot do?

  • Manage collection of parking spots
  • Find available spots
  • Assign spots to vehicles
  • Release spots when vehicles leave

ParkingLot Class:

💡 Tip: Click dropdown to switch between languages
parking_lot.py
class ParkingLot:
def __init__(self, capacity: int):
self.spots: List[ParkingSpot] = []
self.capacity = capacity
# Coordination responsibility
def park_vehicle(self, vehicle: Vehicle) -> Ticket:
spot = self._find_available_spot(vehicle.get_type())
if not spot:
raise ValueError("No available spots")
spot.park_vehicle(vehicle)
return Ticket(vehicle, spot, datetime.now())
def unpark_vehicle(self, ticket: Ticket) -> Payment:
spot = ticket.spot
vehicle = spot.unpark_vehicle()
duration = ticket.calculate_duration()
amount = self._calculate_payment(duration, vehicle.get_type())
return Payment(ticket, amount)
# Helper methods
def _find_available_spot(self, vehicle_type: VehicleType) -> Optional[ParkingSpot]:
for spot in self.spots:
if spot.is_available() and spot.get_type() == vehicle_type:
return spot
return None
def _calculate_payment(self, duration: float, vehicle_type: VehicleType) -> float:
base_rate = 10.0 # $10 per hour
if vehicle_type == VehicleType.TRUCK:
base_rate *= 1.5 # Trucks pay 50% more
return duration * base_rate

Responsibility: Coordinate parking operations

What should ParkingSpot do?

  • Track its own state (occupied/available)
  • Know its type and location
  • Park/unpark vehicles

ParkingSpot Class:

💡 Tip: Click dropdown to switch between languages
parking_spot.py
class ParkingSpot:
def __init__(self, spot_id: str, spot_type: VehicleType):
self.spot_id = spot_id
self.spot_type = spot_type
self._is_occupied = False
self._vehicle: Optional[Vehicle] = None
# State management responsibility
def park_vehicle(self, vehicle: Vehicle) -> None:
if not self.is_available():
raise ValueError("Spot is already occupied")
if vehicle.get_type() != self.spot_type:
raise ValueError("Vehicle type doesn't match spot type")
self._vehicle = vehicle
self._is_occupied = True
def unpark_vehicle(self) -> Vehicle:
if not self._is_occupied:
raise ValueError("Spot is not occupied")
vehicle = self._vehicle
self._vehicle = None
self._is_occupied = False
return vehicle
def is_available(self) -> bool:
return not self._is_occupied
def get_type(self) -> VehicleType:
return self.spot_type

Responsibility: Manage spot state and vehicle assignment

What should Ticket do?

  • Store entry information
  • Calculate parking duration
  • Reference vehicle and spot

Ticket Class:

💡 Tip: Click dropdown to switch between languages
ticket.py
class Ticket:
def __init__(self, vehicle: Vehicle, spot: ParkingSpot, entry_time: datetime):
self.ticket_id = self._generate_id()
self.vehicle = vehicle
self.spot = spot
self.entry_time = entry_time
# Business logic responsibility
def calculate_duration(self) -> float:
return (datetime.now() - self.entry_time).total_seconds() / 3600
def _generate_id(self) -> str:
return f"TICKET_{uuid.uuid4().hex[:8]}"

Responsibility: Track parking session and calculate duration

Visual: Complete Responsibility Assignment

Section titled “Visual: Complete Responsibility Assignment”
Complete parking lot responsibility assignment across ParkingLot, ParkingSpot, Ticket and Payment class methods

Cohesion measures how related the responsibilities within a class are.

Types:

  • High Cohesion - All methods work together for one purpose
  • Low Cohesion - Methods are unrelated
High vs low cohesion: ParkingSpot with related parking methods compared to a class mixing parking, email, reports and payments

Coupling measures how dependent classes are on each other.

Types:

  • Low Coupling - Classes are independent
  • High Coupling - Classes are tightly dependent
Low vs high coupling: classes interacting through interfaces compared to classes depending directly on each other

Aim for:


  • Single Responsibility Principle - One reason to change
  • Identify what each class needs to do - Group related operations
  • Avoid God Classes - Split large classes
  • Avoid Anemic Models - Include behavior with data
  • High Cohesion - Related responsibilities together
  • Low Coupling - Independent classes
  • Use patterns - Data Holder, Business Logic, Coordinator, Validator, Repository

When assigning responsibilities, ensure:

  • Each class has one primary responsibility
  • Responsibilities are related (high cohesion)
  • Classes are independent (low coupling)
  • No God Classes - Split if too many responsibilities
  • No Anemic Models - Include behavior with data
  • Clear purpose - Easy to understand what class does
Summary of assigning responsibilities: identified entities get one clear responsibility per class, resulting in a clean design

Now that you’ve mastered assigning responsibilities, let’s learn how to create class diagrams:

Next: Class Diagrams →

This next guide will teach you how to visualize your design with class diagrams, showing relationships, inheritance, and composition!


💡 Time to Practice!

Now that you understand the concepts, put them into practice with our interactive playground. Build UML diagrams, write code, and get AI-powered feedback.

Browse All Problems