Skip to main content

Architectural Patterns

MVC (Model-View-Controller)

MVC separates an application into three interconnected components:

  • Model: Manages data and business logic.
  • View: Represents UI and presentation layer.
  • Controller: Handles user input and updates the Model and View.

Benefits

  • Separation of concerns
  • Easier to test and maintain
  • Promotes modularity

Example

# Model
class User:
def __init__(self, username):
self.username = username

# View
class UserView:
def show_user(self, user):
print(f"User: {user.username}")

# Controller
class UserController:
def __init__(self, user, view):
self.user = user
self.view = view

def update_username(self, new_name):
self.user.username = new_name

def display_user(self):
self.view.show_user(self.user)

# Usage
user = User("abhishek")
view = UserView()
controller = UserController(user, view)

controller.display_user()
controller.update_username("abhi_updated")
controller.display_user()

Layered Architecture (N-tier)

Divides the application into layers:

  • Presentation Layer: UI
  • Business Logic Layer: Core functionalities
  • Data Access Layer: Interacts with DB

Benefits

  • Clear separation of responsibilities
  • Testability and maintainability

Example

# Data Layer
class UserRepository:
def get_user(self, user_id):
return {"id": user_id, "name": "Abhishek"}

# Business Layer
class UserService:
def __init__(self, repository):
self.repository = repository

def get_user_name(self, user_id):
user = self.repository.get_user(user_id)
return user["name"]

# Presentation Layer
def display_user(user_service, user_id):
name = user_service.get_user_name(user_id)
print(f"User Name: {name}")

repo = UserRepository()
service = UserService(repo)
display_user(service, 1)

Repository Pattern

Abstracts the data layer, encapsulating the logic to access data from a data source.

Benefits

  • Centralized data access logic
  • Decouples business logic from database logic

Example

class User:
def __init__(self, user_id, name):
self.id = user_id
self.name = name

# Repository
class UserRepository:
def __init__(self):
self.users = {} # Simulated database

def add_user(self, user):
self.users[user.id] = user

def get_user_by_id(self, user_id):
return self.users.get(user_id)

repo = UserRepository()
repo.add_user(User(1, "Abhishek"))

user = repo.get_user_by_id(1)
print(user.name) # Output: Abhishek

Dependency Injection (DI) / Inversion of Control (IoC)

  • DI: Objects receive their dependencies from an external source rather than creating them internally.
  • IoC: The control flow of a program is inverted — the framework calls your code instead of your code calling the framework.

Benefits

  • Improved testability
  • Decoupled code

Example

# Service
class EmailService:
def send_email(self, user, message):
print(f"Sending email to {user}: {message}")

# Controller with DI
class UserController:
def __init__(self, email_service):
self.email_service = email_service

def notify_user(self, user):
self.email_service.send_email(user, "Hello!")

# Injecting the dependency
email_service = EmailService()
controller = UserController(email_service)
controller.notify_user("abhishek@example.com")

Microservices vs Monolith

Monolith

  • A single codebase handling all business logic.
  • Simpler to deploy initially, tightly coupled.

Microservices

  • Application is broken into smaller, independent services.
  • Each service runs in its own process and communicates via HTTP or messaging queues.

Example (Conceptual)

# Monolith
class App:
def register_user(self):
pass

def process_payment(self):
pass

# Microservices
# user_service.py
def register_user():
pass

# payment_service.py
def process_payment():
pass

# These services would run independently and communicate over HTTP or message queues

Event-Driven Architecture

Components communicate by emitting and listening to events asynchronously.

Benefits

  • Loose coupling
  • High scalability

Example using a simple event bus

# Simple Event Bus
class EventBus:
def __init__(self):
self.listeners = {}

def subscribe(self, event_type, callback):
self.listeners.setdefault(event_type, []).append(callback)

def publish(self, event_type, data):
for callback in self.listeners.get(event_type, []):
callback(data)

# Usage
bus = EventBus()

def handle_user_registered(data):
print(f"Sending welcome email to {data['email']}")

bus.subscribe("user_registered", handle_user_registered)

# Emit the event
bus.publish("user_registered", {"email": "abhi@example.com"})

CQRS (Command Query Responsibility Segregation)

  • Segregates read and write operations into separate models.
  • Command: Updates data (write)
  • Query: Reads data

Benefits

  • Optimized for reads and writes independently
  • Good for complex domains

Example

# Command - Write model
class UserCommandHandler:
def __init__(self):
self.store = {}

def create_user(self, user_id, name):
self.store[user_id] = name
print(f"User created: {name}")

# Query - Read model
class UserQueryHandler:
def __init__(self, store):
self.store = store

def get_user(self, user_id):
return self.store.get(user_id)

# Usage
command_handler = UserCommandHandler()
command_handler.create_user(1, "Abhishek")

query_handler = UserQueryHandler(command_handler.store)
print(query_handler.get_user(1)) # Output: Abhishek