Services
Services are the business logic layer of Tasteful flavors. They contain all the core business rules, orchestrate operations between different components, and provide a clean interface for controllers to interact with your application’s functionality. Services are where ALL business logic should live.What is a Service?
A Service in Tasteful is a class that inherits fromBaseService and encapsulates business logic. Services are responsible for:
- Business Logic Implementation: All core business rules and operations
- Data Orchestration: Coordinating operations between repositories and external systems
- Validation: Ensuring data integrity and business rule compliance
- Transaction Management: Handling complex operations that span multiple data sources
- Error Handling: Converting low-level errors into meaningful business exceptions
- Service Composition: Coordinating with other services for complex workflows
Core Concepts
BaseService Foundation
All services inherit fromBaseService, which provides the foundation for dependency injection:
Copy
from tasteful.base_flavor import BaseService
class UserService(BaseService):
def __init__(self, user_repository: UserRepository):
super().__init__()
self.user_repository = user_repository
BaseService:
- Dependency Injection Support: Automatically receives dependencies through constructor injection
- Clean Architecture: Provides a clear separation between business logic and infrastructure
- Testability: Easy to mock and test in isolation
- Composition: Services can depend on other services and repositories
Service Dependencies
Services can depend on repositories, other services, and configuration objects:Copy
class OrderService(BaseService):
def __init__(
self,
order_repository: OrderRepository,
user_service: UserService,
payment_service: PaymentService,
order_config: OrderConfig
):
super().__init__()
self.order_repository = order_repository
self.user_service = user_service
self.payment_service = payment_service
self.order_config = order_config
Implementation
Basic Service Implementation
Here’s a complete example of a service implementation:Copy
from tasteful.base_flavor import BaseService
from typing import Dict, List, Optional
class ProductService(BaseService):
"""Service for product management business logic."""
def __init__(self, product_repository: ProductRepository):
super().__init__()
self.product_repository = product_repository
def list_products(self, skip: int = 0, limit: int = 100) -> List[Dict]:
"""List products with business logic applied."""
# Business logic: Apply default sorting and filtering
products = self.product_repository.list_products(
skip=skip,
limit=min(limit, 1000), # Business rule: max 1000 items
order_by="created_at",
active_only=True
)
# Business logic: Add computed fields
for product in products:
product["display_price"] = self._format_price(product["price"])
product["in_stock"] = product["quantity"] > 0
return products
def get_product(self, product_id: int) -> Optional[Dict]:
"""Get a product with business logic validation."""
if product_id <= 0:
raise ValueError("Product ID must be positive")
product = self.product_repository.get_by_id(product_id)
if not product:
raise ProductNotFoundError(f"Product {product_id} not found")
# Business logic: Add computed fields
product["display_price"] = self._format_price(product["price"])
product["availability_status"] = self._get_availability_status(product)
return product
def create_product(self, product_data: Dict) -> Dict:
"""Create a new product with validation."""
# Business validation
self._validate_product_data(product_data)
# Business logic: Set defaults
product_data["status"] = "active"
product_data["created_at"] = datetime.utcnow()
# Business logic: Generate SKU if not provided
if not product_data.get("sku"):
product_data["sku"] = self._generate_sku(product_data["name"])
return self.product_repository.create(product_data)
def update_product(self, product_id: int, product_data: Dict) -> Dict:
"""Update a product with business validation."""
# Check if product exists
existing_product = self.get_product(product_id)
# Business validation
self._validate_product_update(existing_product, product_data)
# Business logic: Update timestamp
product_data["updated_at"] = datetime.utcnow()
return self.product_repository.update(product_id, product_data)
def delete_product(self, product_id: int) -> Dict:
"""Soft delete a product with business rules."""
product = self.get_product(product_id)
# Business rule: Can't delete products with active orders
if self._has_active_orders(product_id):
raise BusinessRuleError("Cannot delete product with active orders")
# Business logic: Soft delete instead of hard delete
return self.product_repository.update(product_id, {
"status": "deleted",
"deleted_at": datetime.utcnow()
})
def _validate_product_data(self, product_data: Dict) -> None:
"""Private method for product validation."""
if not product_data.get("name"):
raise ValidationError("Product name is required")
if product_data.get("price", 0) < 0:
raise ValidationError("Product price cannot be negative")
def _format_price(self, price: float) -> str:
"""Format price for display."""
return f"${price:.2f}"
def _get_availability_status(self, product: Dict) -> str:
"""Determine product availability status."""
if product["quantity"] == 0:
return "out_of_stock"
elif product["quantity"] < 10:
return "low_stock"
else:
return "in_stock"
Advanced Patterns
Service Composition
Services can orchestrate complex operations by coordinating with multiple other services:Copy
class OrderService(BaseService):
def __init__(
self,
order_repository: OrderRepository,
user_service: UserService,
product_service: ProductService,
payment_service: PaymentService,
inventory_service: InventoryService,
notification_service: NotificationService
):
super().__init__()
self.order_repository = order_repository
self.user_service = user_service
self.product_service = product_service
self.payment_service = payment_service
self.inventory_service = inventory_service
self.notification_service = notification_service
def create_order(self, order_data: Dict) -> Dict:
"""Create an order with complex business logic."""
# Step 1: Validate user
user = self.user_service.get_user(order_data["user_id"])
if not user["is_active"]:
raise BusinessRuleError("User account is not active")
# Step 2: Validate products and calculate totals
total_amount = 0
for item in order_data["items"]:
product = self.product_service.get_product(item["product_id"])
if not product:
raise ValidationError(f"Product {item['product_id']} not found")
# Business logic: Apply discounts, calculate totals
item_total = self._calculate_item_total(product, item["quantity"])
total_amount += item_total
# Step 3: Check inventory availability
inventory_check = self.inventory_service.check_availability(order_data["items"])
if not inventory_check["available"]:
raise BusinessRuleError("Some items are not available")
# Step 4: Create order record
order_data.update({
"total_amount": total_amount,
"status": "pending",
"created_at": datetime.utcnow()
})
order = self.order_repository.create(order_data)
# Step 5: Reserve inventory
self.inventory_service.reserve_items(order["id"], order_data["items"])
# Step 6: Send confirmation
self.notification_service.send_order_confirmation(user["email"], order)
return order
def process_payment(self, order_id: int, payment_data: Dict) -> Dict:
"""Process payment for an order."""
order = self.order_repository.get_by_id(order_id)
if not order:
raise OrderNotFoundError(f"Order {order_id} not found")
if order["status"] != "pending":
raise BusinessRuleError("Order is not in pending status")
try:
# Process payment through payment service
payment_result = self.payment_service.process_payment(
amount=order["total_amount"],
payment_data=payment_data
)
if payment_result["success"]:
# Update order status
self.order_repository.update(order_id, {
"status": "paid",
"payment_id": payment_result["payment_id"],
"paid_at": datetime.utcnow()
})
# Confirm inventory reservation
self.inventory_service.confirm_reservation(order_id)
# Send success notification
user = self.user_service.get_user(order["user_id"])
self.notification_service.send_payment_success(user["email"], order)
return {"success": True, "order": order}
else:
# Release inventory reservation
self.inventory_service.release_reservation(order_id)
return {"success": False, "error": payment_result["error"]}
except Exception as e:
# Release inventory on any error
self.inventory_service.release_reservation(order_id)
raise PaymentProcessingError(f"Payment processing failed: {str(e)}")
Service with Repository Integration
Services should always interact with data through repositories:Copy
class UserService(BaseService):
def __init__(self, user_repository: UserRepository, auth_service: AuthService):
super().__init__()
self.user_repository = user_repository
self.auth_service = auth_service
def create_user(self, user_data: Dict) -> Dict:
"""Create a new user with business validation."""
# Business validation
self._validate_user_data(user_data)
# Business rule: Check if email already exists
if self.user_repository.email_exists(user_data["email"]):
raise BusinessRuleError("Email already exists")
# Business logic: Hash password
if "password" in user_data:
user_data["password_hash"] = self.auth_service.hash_password(user_data["password"])
del user_data["password"] # Remove plain password
# Business logic: Set defaults
user_data.update({
"is_active": True,
"created_at": datetime.utcnow(),
"email_verified": False
})
# Create user through repository
user = self.user_repository.create(user_data)
# Business logic: Send welcome email
self._send_welcome_email(user)
return user
def authenticate_user(self, email: str, password: str) -> Optional[Dict]:
"""Authenticate a user with business logic."""
user = self.user_repository.get_by_email(email)
if not user:
return None
# Business rule: Check if account is active
if not user["is_active"]:
raise BusinessRuleError("Account is deactivated")
# Verify password
if not self.auth_service.verify_password(password, user["password_hash"]):
# Business logic: Track failed login attempts
self._track_failed_login(user["id"])
return None
# Business logic: Update last login
self.user_repository.update(user["id"], {
"last_login": datetime.utcnow(),
"failed_login_attempts": 0
})
return user
Integration with Other Components
Service-Controller Relationship
Controllers should always delegate business logic to services:Copy
# In Controller
class UserController(BaseController):
def __init__(self, user_service: UserService):
super().__init__(prefix="/users", tags=["users"])
self.user_service = user_service
@Post("/")
async def create_user(self, user_data: Dict) -> Dict:
# Controller delegates to service
return self.user_service.create_user(user_data)
@Post("/login")
async def login(self, credentials: Dict) -> Dict:
# Controller handles HTTP, service handles business logic
user = self.user_service.authenticate_user(
credentials["email"],
credentials["password"]
)
if user:
return {"success": True, "user": user}
else:
raise HTTPException(status_code=401, detail="Invalid credentials")
Service-Repository Relationship
Services should interact with data only through repositories:Copy
# ✅ Good: Service uses repository for data access
class ProductService(BaseService):
def __init__(self, product_repository: ProductRepository):
super().__init__()
self.product_repository = product_repository
def get_product(self, product_id: int) -> Dict:
# Service uses repository for data access
product = self.product_repository.get_by_id(product_id)
if not product:
raise ProductNotFoundError("Product not found")
# Service adds business logic
product["display_price"] = self._format_price(product["price"])
return product
# ❌ Bad: Service directly accessing database
class ProductService(BaseService):
def __init__(self, database_connection):
super().__init__()
self.db = database_connection
def get_product(self, product_id: int) -> Dict:
# Don't access database directly from service
result = self.db.execute("SELECT * FROM products WHERE id = ?", product_id)
return result.fetchone()
Dependency Injection
Services receive their dependencies through constructor injection:Copy
class OrderService(BaseService):
def __init__(
self,
order_repository: OrderRepository, # ← Repository dependency
user_service: UserService, # ← Service dependency
payment_service: PaymentService, # ← Service dependency
order_config: OrderConfig # ← Configuration dependency
):
super().__init__()
self.order_repository = order_repository
self.user_service = user_service
self.payment_service = payment_service
self.order_config = order_config
# In your flavor definition
class OrderFlavor(BaseFlavor):
def __init__(self):
super().__init__(
controller=OrderController,
services=[
OrderService,
UserService,
PaymentService
], # ← Services registered for injection
repositories=[OrderRepository, UserRepository],
config=OrderConfig
)
Best Practices
Do’s
- Put ALL business logic in services - Never put business logic in controllers or repositories
- Use meaningful method names - Service methods should clearly describe what business operation they perform
- Validate input data - Services should validate all input according to business rules
- Handle errors appropriately - Convert low-level errors into meaningful business exceptions
- Keep services focused - Each service should have a single responsibility
- Use dependency injection - Let the framework inject dependencies rather than creating them manually
Copy
class UserService(BaseService):
def __init__(self, user_repository: UserRepository, email_service: EmailService):
super().__init__()
self.user_repository = user_repository
self.email_service = email_service
def create_user(self, user_data: Dict) -> Dict:
"""Create a new user with full business logic."""
# Validate business rules
self._validate_user_creation(user_data)
# Apply business logic
user_data["status"] = "active"
user_data["created_at"] = datetime.utcnow()
# Create through repository
user = self.user_repository.create(user_data)
# Handle side effects
self.email_service.send_welcome_email(user["email"])
return user
def _validate_user_creation(self, user_data: Dict) -> None:
"""Private validation method."""
if not user_data.get("email"):
raise ValidationError("Email is required")
if self.user_repository.email_exists(user_data["email"]):
raise BusinessRuleError("Email already exists")
Don’ts
- Don’t put HTTP logic in services - Services shouldn’t know about HTTP requests/responses
- Don’t access external APIs directly - Use dedicated service classes for external integrations
- Don’t handle database connections directly - Always use repositories for data access
- Don’t make services too large - Break down complex services into smaller, focused services
- Don’t ignore error handling - Always handle and convert exceptions appropriately
Copy
# ❌ Bad: HTTP logic in service
class UserService(BaseService):
def create_user(self, request: HTTPRequest) -> HTTPResponse:
# Don't handle HTTP in services
user_data = request.json()
user = self.user_repository.create(user_data)
return HTTPResponse(json=user, status=201)
# ❌ Bad: Direct database access
class UserService(BaseService):
def __init__(self, database_url: str):
self.db = create_engine(database_url)
def get_user(self, user_id: int):
# Don't access database directly
with self.db.connect() as conn:
result = conn.execute("SELECT * FROM users WHERE id = ?", user_id)
return result.fetchone()
# ✅ Good: Clean service with proper separation
class UserService(BaseService):
def __init__(self, user_repository: UserRepository):
super().__init__()
self.user_repository = user_repository
def create_user(self, user_data: Dict) -> Dict:
# Pure business logic
self._validate_user_data(user_data)
return self.user_repository.create(user_data)
Common Patterns
CRUD Service Pattern
Most services follow standard CRUD patterns with business logic:Copy
class ResourceService(BaseService):
def __init__(self, resource_repository: ResourceRepository):
super().__init__()
self.resource_repository = resource_repository
def list_resources(self, filters: Dict = None, pagination: Dict = None) -> List[Dict]:
"""List resources with business filtering."""
# Apply business rules to filters
safe_filters = self._sanitize_filters(filters or {})
safe_pagination = self._validate_pagination(pagination or {})
return self.resource_repository.list(safe_filters, safe_pagination)
def get_resource(self, resource_id: int) -> Dict:
"""Get a resource with business validation."""
if resource_id <= 0:
raise ValidationError("Invalid resource ID")
resource = self.resource_repository.get_by_id(resource_id)
if not resource:
raise ResourceNotFoundError("Resource not found")
return resource
def create_resource(self, resource_data: Dict) -> Dict:
"""Create a resource with business validation."""
self._validate_resource_data(resource_data)
# Apply business defaults
resource_data["status"] = "active"
resource_data["created_at"] = datetime.utcnow()
return self.resource_repository.create(resource_data)
def update_resource(self, resource_id: int, resource_data: Dict) -> Dict:
"""Update a resource with business validation."""
existing_resource = self.get_resource(resource_id)
# Business validation for updates
self._validate_resource_update(existing_resource, resource_data)
resource_data["updated_at"] = datetime.utcnow()
return self.resource_repository.update(resource_id, resource_data)
def delete_resource(self, resource_id: int) -> bool:
"""Delete a resource with business rules."""
resource = self.get_resource(resource_id)
# Business rule: Check if resource can be deleted
if self._has_dependencies(resource_id):
raise BusinessRuleError("Cannot delete resource with dependencies")
return self.resource_repository.delete(resource_id)
Service Orchestration Pattern
Services can orchestrate complex workflows:Copy
class CheckoutService(BaseService):
def __init__(
self,
cart_service: CartService,
inventory_service: InventoryService,
payment_service: PaymentService,
order_service: OrderService,
notification_service: NotificationService
):
super().__init__()
self.cart_service = cart_service
self.inventory_service = inventory_service
self.payment_service = payment_service
self.order_service = order_service
self.notification_service = notification_service
def process_checkout(self, user_id: int, payment_data: Dict) -> Dict:
"""Process a complete checkout workflow."""
try:
# Step 1: Get cart
cart = self.cart_service.get_user_cart(user_id)
if not cart["items"]:
raise BusinessRuleError("Cart is empty")
# Step 2: Validate inventory
inventory_check = self.inventory_service.check_availability(cart["items"])
if not inventory_check["available"]:
raise BusinessRuleError("Some items are no longer available")
# Step 3: Create order
order = self.order_service.create_order_from_cart(cart)
# Step 4: Process payment
payment_result = self.payment_service.process_payment(
order["total"], payment_data
)
if not payment_result["success"]:
# Rollback order
self.order_service.cancel_order(order["id"])
raise PaymentError("Payment failed")
# Step 5: Confirm order
self.order_service.confirm_order(order["id"], payment_result["payment_id"])
# Step 6: Update inventory
self.inventory_service.reserve_items(order["items"])
# Step 7: Clear cart
self.cart_service.clear_cart(user_id)
# Step 8: Send notifications
self.notification_service.send_order_confirmation(user_id, order)
return {
"success": True,
"order_id": order["id"],
"payment_id": payment_result["payment_id"]
}
except Exception as e:
# Handle any errors and rollback if necessary
self._handle_checkout_error(e, locals())
raise
Testing
Services are highly testable thanks to dependency injection:Copy
import pytest
from unittest.mock import Mock, patch
from datetime import datetime
def test_user_service_create_user():
# Arrange
mock_user_repository = Mock(spec=UserRepository)
mock_user_repository.email_exists.return_value = False
mock_user_repository.create.return_value = {"id": 1, "email": "test@example.com"}
mock_email_service = Mock(spec=EmailService)
user_service = UserService(
user_repository=mock_user_repository,
email_service=mock_email_service
)
user_data = {"email": "test@example.com", "name": "Test User"}
# Act
result = user_service.create_user(user_data)
# Assert
assert result["id"] == 1
assert result["email"] == "test@example.com"
mock_user_repository.email_exists.assert_called_once_with("test@example.com")
mock_user_repository.create.assert_called_once()
mock_email_service.send_welcome_email.assert_called_once_with("test@example.com")
def test_user_service_create_user_duplicate_email():
# Arrange
mock_user_repository = Mock(spec=UserRepository)
mock_user_repository.email_exists.return_value = True
user_service = UserService(user_repository=mock_user_repository)
user_data = {"email": "test@example.com", "name": "Test User"}
# Act & Assert
with pytest.raises(BusinessRuleError, match="Email already exists"):
user_service.create_user(user_data)
mock_user_repository.create.assert_not_called()
def test_order_service_complex_workflow():
# Arrange
mock_order_repository = Mock(spec=OrderRepository)
mock_user_service = Mock(spec=UserService)
mock_payment_service = Mock(spec=PaymentService)
mock_user_service.get_user.return_value = {"id": 1, "is_active": True}
mock_payment_service.process_payment.return_value = {"success": True, "payment_id": "pay_123"}
mock_order_repository.create.return_value = {"id": 1, "total_amount": 100.0}
order_service = OrderService(
order_repository=mock_order_repository,
user_service=mock_user_service,
payment_service=mock_payment_service
)
order_data = {"user_id": 1, "items": [{"product_id": 1, "quantity": 2}]}
# Act
result = order_service.create_order(order_data)
# Assert
assert result["id"] == 1
mock_user_service.get_user.assert_called_once_with(1)
mock_order_repository.create.assert_called_once()