βοΈ Python Backend Patterns β π§© Service Layer Pattern π
Posted on: October 12, 2026
Description:
As backend applications grow, business rules often become scattered across routes, controllers, and other application components.
For example, an order-processing API might validate customer information, calculate discounts, and update order statuses directly inside its endpoints.
While this works for smaller applications, it becomes difficult to maintain when multiple endpoints or background processes need to perform the same operations.
The Service Layer Pattern solves this by introducing a dedicated layer responsible for handling application business logic.
Instead of implementing business rules inside controllers, requests are delegated to reusable services.
Conceptually, the flow becomes:
Controller
β
Service
β
Repository
β
Data Source
The controller handles requests, the service enforces business rules, and the repository manages data access.
Defining the Data Model
We'll use a simple Order model representing a customer's purchase.
@dataclass
class Order:
id: int
customer: str
amount: Decimal
discount: Decimal
final_amount: Decimal
status: str = "PENDING"
Each order contains the original amount, calculated discount, final amount, and current status.
For this example, we'll use an in-memory repository instead of an actual database so we can focus on the service layer.
Creating the Service Layer
The OrderService becomes responsible for handling order-related business operations.
class OrderService:
DISCOUNT_THRESHOLD = Decimal("1000.00")
DISCOUNT_RATE = Decimal("0.10")
def __init__(self, repository):
self.repository = repository
The service receives a repository through its constructor and uses it whenever data needs to be stored or retrieved.
More importantly, it centralizes the application's business rules.
For example, orders worth βΉ1,000 or more receive a 10% discount.
discount = Decimal("0.00")
if amount >= self.DISCOUNT_THRESHOLD:
discount = amount * self.DISCOUNT_RATE
final_amount = amount - discount
This calculation belongs inside the service rather than the controller.
If the discount policy changes later, we only need to update the business logic in one place.
Keeping Controllers Lightweight
A controller should focus on handling requests and returning responses.
It shouldn't need to understand how discounts are calculated or which conditions make an order valid.
Our OrderController delegates those responsibilities to the service.
class OrderController:
def __init__(self, service):
self.service = service
def create_order(self, order_id, customer, amount):
try:
order = self.service.create_order(
order_id,
customer,
amount,
)
return {
"success": True,
"order_id": order.id,
"final_amount": str(order.final_amount),
}
except ValueError as error:
return {
"success": False,
"message": str(error),
}
Notice that the controller doesn't contain validation rules or discount calculations.
It simply delegates the operation and prepares the response.
This separation keeps controllers easier to understand and maintain.
Enforcing Business Rules
The service layer can also control which operations are allowed.
For example, an order should only be confirmed when its current status is PENDING.
def confirm_order(self, order_id):
order = self.repository.get_by_id(order_id)
if not order:
raise ValueError(f"Order {order_id} does not exist")
if order.status != "PENDING":
raise ValueError("Only pending orders can be confirmed")
order.status = "CONFIRMED"
self.repository.update(order)
return order
Because this rule exists inside the service, every component using the service follows the same confirmation logic.
The controller doesn't need to duplicate these checks.
Reusing Business Logic
One of the biggest advantages of the Service Layer Pattern is reusability.
We configure the application by creating the repository, service, and controller.
repository = OrderRepository()
service = OrderService(repository)
controller = OrderController(service)
Orders can then be created through the controller:
controller.create_order(1, "Alice", "1500.00")
But the same service can also be used directly:
order = service.create_order(4, "David", "2000.00")
This is useful when the application needs to perform the same business operation from different entry points, such as:
- REST API endpoints
- Background workers
- Scheduled jobs
- Command-line tools
Regardless of where the operation originates, the same business rules are applied.
Why Use the Service Layer Pattern?
The Service Layer Pattern provides a clear separation between request handling and business logic.
It helps backend applications become:
- Easier to maintain: Business rules are centralized instead of scattered across controllers.
- More reusable: The same service can support multiple application entry points.
- Easier to test: Business operations can be tested independently of HTTP frameworks.
- More consistent: Validation and business rules are enforced through shared services.
- Easier to extend: New controllers or background processes can reuse existing functionality.
For very small applications, introducing a dedicated service layer may be unnecessary. However, it becomes valuable as business rules grow and multiple components need to perform the same operations.
Key Takeaways
- The Service Layer Pattern moves business logic out of routes and controllers.
- Controllers focus on request handling and response generation.
- Services centralize validation, calculations, and application rules.
- Repositories remain responsible for storing and retrieving data.
- Business operations become reusable across APIs, background jobs, and other application components.
Code Snippet:
# Import dataclass for defining the data model
from dataclasses import dataclass
# Import Decimal for accurate monetary calculations
from decimal import Decimal, InvalidOperation
# Import Optional for values that may not exist
from typing import Optional
# -----------------------------------------
# Step 1: Define the application data model
# -----------------------------------------
@dataclass
class Order:
id: int
customer: str
amount: Decimal
discount: Decimal
final_amount: Decimal
status: str = "PENDING"
# -----------------------------------------
# Step 2: Create the repository
# -----------------------------------------
class OrderRepository:
"""Handles all order data access operations."""
def __init__(self):
# Use a dictionary as an in-memory data source
self._orders = {}
def add(self, order: Order) -> None:
"""Store a new order."""
self._orders[order.id] = order
def get_by_id(self, order_id: int) -> Optional[Order]:
"""Retrieve an order by ID."""
return self._orders.get(order_id)
def get_all(self) -> list[Order]:
"""Retrieve all orders."""
return list(self._orders.values())
def update(self, order: Order) -> None:
"""Update an existing order."""
self._orders[order.id] = order
# -----------------------------------------
# Step 3: Create the service layer
# -----------------------------------------
class OrderService:
"""Contains reusable order-processing business rules."""
DISCOUNT_THRESHOLD = Decimal("1000.00")
DISCOUNT_RATE = Decimal("0.10")
def __init__(self, repository: OrderRepository):
# Depend on the repository for data access
self.repository = repository
def create_order(
self,
order_id: int,
customer: str,
amount: Decimal,
) -> Order:
"""Validate and create a new order."""
# Prevent duplicate order IDs
if self.repository.get_by_id(order_id):
raise ValueError(f"Order {order_id} already exists")
# Validate customer information
if not isinstance(customer, str) or not customer.strip():
raise ValueError("Customer name cannot be empty")
# Convert the amount into Decimal
try:
amount = Decimal(str(amount))
except (InvalidOperation, ValueError, TypeError):
raise ValueError("Invalid order amount")
# Validate the order amount
if not amount.is_finite() or amount <= 0:
raise ValueError("Order amount must be positive")
# Apply a 10% discount for qualifying orders
discount = Decimal("0.00")
if amount >= self.DISCOUNT_THRESHOLD:
discount = amount * self.DISCOUNT_RATE
final_amount = amount - discount
# Create the order after applying business rules
order = Order(
id=order_id,
customer=customer.strip(),
amount=amount,
discount=discount,
final_amount=final_amount,
)
self.repository.add(order)
return order
def confirm_order(self, order_id: int) -> Order:
"""Confirm an existing pending order."""
order = self.repository.get_by_id(order_id)
if not order:
raise ValueError(f"Order {order_id} does not exist")
# Only pending orders can be confirmed
if order.status != "PENDING":
raise ValueError("Only pending orders can be confirmed")
order.status = "CONFIRMED"
self.repository.update(order)
return order
def find_order(self, order_id: int) -> Optional[Order]:
"""Find an existing order."""
return self.repository.get_by_id(order_id)
def list_orders(self) -> list[Order]:
"""Return all orders."""
return self.repository.get_all()
# -----------------------------------------
# Step 4: Create the controller
# -----------------------------------------
class OrderController:
"""Handles requests by delegating business logic to services."""
def __init__(self, service: OrderService):
# The controller depends on the service layer
self.service = service
def create_order(
self,
order_id: int,
customer: str,
amount: Decimal,
) -> dict:
"""Handle an order creation request."""
try:
# Delegate validation and calculations to the service
order = self.service.create_order(
order_id,
customer,
amount,
)
return {
"success": True,
"message": "Order created successfully",
"order_id": order.id,
"final_amount": str(order.final_amount),
}
except ValueError as error:
return {
"success": False,
"message": str(error),
}
def confirm_order(self, order_id: int) -> dict:
"""Handle an order confirmation request."""
try:
# Delegate order confirmation to the service
order = self.service.confirm_order(order_id)
return {
"success": True,
"message": "Order confirmed successfully",
"status": order.status,
}
except ValueError as error:
return {
"success": False,
"message": str(error),
}
# -----------------------------------------
# Step 5: Configure application components
# -----------------------------------------
repository = OrderRepository()
service = OrderService(repository)
controller = OrderController(service)
# -----------------------------------------
# Step 6: Execute application operations
# -----------------------------------------
print("Creating Orders:")
# Create orders through the controller
response1 = controller.create_order(1, "Alice", "1500.00")
response2 = controller.create_order(2, "Bob", "500.00")
response3 = controller.create_order(3, "Charlie", "-100.00")
print("Alice:", response1)
print("Bob:", response2)
print("Charlie:", response3)
# Confirm an existing order
confirmation = controller.confirm_order(1)
print("\nOrder Confirmation:")
print(confirmation)
# -----------------------------------------
# Step 7: Reuse the service independently
# -----------------------------------------
# Business logic can also be used by background jobs,
# scheduled tasks, or other application components.
order = service.create_order(4, "David", "2000.00")
print("\nDirect Service Access:")
print(
f"Customer: {order.customer} | "
f"Amount: {order.amount} | "
f"Discount: {order.discount} | "
f"Final Amount: {order.final_amount}"
)
# -----------------------------------------
# Step 8: Display all orders
# -----------------------------------------
print("\nAll Orders:")
for order in service.list_orders():
print(
f"{order.id}: {order.customer} | "
f"Amount: {order.final_amount} | "
f"Status: {order.status}"
)
No comments yet. Be the first to comment!