Design Tutorial: Building an Order Processing Application with WD-DI¶
In this tutorial, we'll build a small order processing application using WD-DI. This guide will demonstrate how to structure your application using dependency injection as the backbone of your architecture. By the end of this tutorial, you'll see how WD-DI helps you achieve separation of concerns, robust configuration management, and a modular, testable codebase.
Tutorial Introduction¶
Dependency injection is a cornerstone of modern software architecture. It enables you to build loosely coupled, maintainable, and testable systems by decoupling the creation of objects from their usage. In this tutorial, we'll use WD-DI to build an Order Processing application that demonstrates:
- Separation of concerns via layered architecture.
- Interface-driven design to allow for flexibility and easy testing.
- Robust configuration management using strongly-typed options.
- Middleware pipelines for cross-cutting concerns (optional extension).
- Proper management of service lifetimes (transient, singleton, and scoped).
Application Overview¶
Our sample application, OrderProcessor, will:
- Accept and process an order.
- Validate and save the order.
- Notify the user via email.
- Log key actions throughout the process.
Project Structure¶
For clarity, our project is organized as follows. This tutorial series will guide you through creating the components within these conceptual directories.
order_processor/
├── main.py # Application entry point (covered in Part 3)
├── domain/ # (Covered in this Part - 01-domain.md)
│ ├── models.py # Domain models (e.g., Order)
│ └── interfaces.py # Domain interfaces (e.g., IOrderService)
├── data/ # (Covered in Part 2 - 02-services.md)
│ └── repository.py # Data access implementation
├── services/ # (Covered in Part 2 - 02-services.md)
│ └── order_service.py # Business logic implementation
├── presentation/ # (Covered in Part 3 - 03-wiring.md)
│ └── controller.py # Application controller (simulated CLI)
└── infrastructure/ # (Covered in Part 2 - 02-services.md)
├── config.py # Configuration classes
└── logging_service.py # Logging service implementation
Note: The original _old_docs/design_tutorial.md
file from which this structure is adapted will be removed as its content is fully migrated.
Tutorial - Step 1: Domain and Project Structure¶
Setting up the domain models and project layout for the order processing application.
Tutorial Part 1: Domain Layer¶
In our Order Processing application, the Domain Layer defines the core business entities and the contracts (interfaces) for how these entities are handled. It should be independent of specific data storage mechanisms or business logic implementations.
Key Components¶
- Models: Dataclasses or simple classes representing your business entities.
- Interfaces: Abstract Base Classes (ABCs) defining the expected operations for services and repositories that will interact with these models.
Defining the Order Model¶
First, let's define our primary business entity, the Order
.
File: domain/models.py
(Conceptual path for the tutorial)
from dataclasses import dataclass
@dataclass
class Order:
order_id: str
item: str
quantity: int
price: float
# You could add customer_id, status, created_at, etc.
This simple dataclass will hold information about an order.
Defining Domain Interfaces¶
Next, we define the interfaces for services that will operate on our domain models. These interfaces will be implemented by other layers (like the Data Access Layer or Service Layer).
File: domain/interfaces.py
(Conceptual path for the tutorial)
from abc import ABC, abstractmethod
from .models import Order # Assuming models.py is in the same conceptual directory
class IOrderRepository(ABC):
"""Interface for data persistence operations related to Orders."""
@abstractmethod
def save_order(self, order: Order):
pass
@abstractmethod
def get_order_by_id(self, order_id: str) -> Order | None:
pass
class IOrderService(ABC):
"""Interface for business logic related to processing Orders."""
@abstractmethod
def process_new_order(self, order: Order) -> bool:
"""Processes a new order and returns True if successful."""
pass
class ILogger(ABC):
"""A generic logger interface for infrastructure concerns."""
@abstractmethod
def log_info(self, message: str):
pass
@abstractmethod
def log_error(self, message: str, exception: Exception | None = None):
pass
In this step, we've laid the groundwork by defining what our application deals with (Order
) and what operations we expect to perform (IOrderRepository
, IOrderService
, and a utility ILogger
). These interfaces ensure that our business logic (which will use IOrderService
) and data access (which will implement IOrderRepository
) are decoupled.
In the next part, we'll look at implementing these interfaces.