AW Dev Rethought

βœ‚οΈ Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away - Antoine de Saint-ExupΓ©ry

βš™οΈ Python Backend Patterns – 🧩 Service Layer Pattern 🐍


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}"
    )

Link copied!

Comments

Add Your Comment

Comment Added!