Skip to main content

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 from BaseService 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
Services act as the single source of truth for business operations and should contain all logic that isn’t purely HTTP-related (controllers) or data-access related (repositories).

Core Concepts

BaseService Foundation

All services inherit from BaseService, which provides the foundation for dependency injection:
from tasteful.base_flavor import BaseService

class UserService(BaseService):
    def __init__(self, user_repository: UserRepository):
        super().__init__()
        self.user_repository = user_repository
Key features of 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:
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:
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:
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:
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:
# 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:
# ✅ 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:
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
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
# ❌ 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:
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:
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:
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()
Services are the heart of your Tasteful application’s business logic. They provide a clean, testable, and maintainable way to implement complex business operations while maintaining proper separation of concerns with controllers and repositories.
I