Services
Services encapsulate the business logic of the application. Their purpose is to coordinate operations across repositories, enforce domain rules, and orchestrate the workflows that power each endpoint. Service methods run inside an active database session, allowing them to perform multiple repository operations within a single transactional context via SQLAlchemy.
In Atlas, each service is a class with each of its methods wrapped by the @service_method decorator for automatically stacking service calls. Each service can either be:
- Domain-bound: associated with a domain by extending the base class Service[T] for a given T entity;
- Domainless: defined as a plain class without extending Service[T], used for cross-cutting operations, integrations, utilities, or workflows that do not directly map to a specific domain entity.
Service Calls Stack
Atlas' services works with a service calls stack: when a service method is invoked, the service layer ensures that a database session exists. Nested service calls that comes after automatically reuse the same session, pushing onto the "service call stack" without opening additional connections.
Only the first (root) service call is responsible for committing or rolling back the transaction and closing the session. This guarantees transactional integrity across complex workflows while keeping database usage efficient and predictable.
Initializing the services package
Before creating services, create the src/services directory. This will be the Python package where all the API's services will be stored. Also, create a __init__.py file inside it for convenient importing.
Example:
from .product_service import ProductService
from .category_service import CategoryService
from .price_service import PriceService
from .stock_service import StockService
from .user_service import UserService
from .admin_service import AdminService
from .customer_service import CustomerService
__all__ = [
"ProductService",
"CategoryService",
"PriceService",
"StockService",
"UserService",
"AdminService",
"CustomerService"
]
Creating a Domain-bound Service
To create a domain-bound service, follow the steps below:
- Create a file inside
src/serviceswith the name of the service in snake_case; - Declare the service's class by extending Service[T] and naming it with the same name as the file, but in PascalCase β for the T type, use the model that will be bound as the service's domain entity;
- Create a
__init__method for initializing the domain entity's repository and for dependency injection of other services, if needed; - For creating methods, wrap each of them with @service_method;
Note: A domain-bound service has useful, generic default methods inherited from the base Service[T] class (that interact with the domain's repository only). Check them out before implementing a method that sounds too generic.
Example:
import bcrypt
from typing import List, Type
from atlas.common import ModelMapper, Page, Pageable
from atlas.decorators import service_method
from atlas.layers import Service, InputDTO, RetDTO
from models import Customer
from repositories import CustomerRepository
class CustomerService(Service[Customer]):
def __init__(self) -> None:
super().__init__()
self.user_service = None
self.customer_repository = CustomerRepository()
@service_method
def list_by_criteria(
self,
search: str = None,
uuids_to_exclude: List[str] = None,
pageable: Pageable = Pageable(),
ret_dto_type: Type[RetDTO] = None
) -> RetDTO | Page:
uuids_to_exclude = uuids_to_exclude or []
page = self.customer_repository.list_by_criteria(search=search, uuids_to_exclude=uuids_to_exclude, pageable=pageable)
return self.map_or_self(page, ret_dto_type)
@service_method
def create(self, input_dto_instance: InputDTO, ret_dto_type: Type[RetDTO] = None) -> RetDTO | Customer:
self.validate_uniqueness_by_fields(input_dto_instance, ["document"])
self.user_service.validate_uniqueness_by_fields(input_dto_instance.user, ["email", "username"])
input_dto_instance.user.password = bcrypt.hashpw(input_dto_instance.user.password.encode(), bcrypt.gensalt()).decode()
customer = ModelMapper.map_instance_to_type(input_dto_instance, Customer)
customer = self.customer_repository.add(customer)
return self.map_or_self(customer, ret_dto_type)
@service_method
def update(self, uuid: str, input_dto_instance: InputDTO, ret_dto_type: Type[RetDTO] = None) -> RetDTO | Customer:
existing_customer = self.get_by_uuid(uuid)
self.validate_uniqueness_by_fields(input_dto_instance, ["document"], uuid_to_exclude=existing_customer.uuid)
if input_dto_instance.user:
self.user_service.validate_uniqueness_by_fields(input_dto_instance.user, ["email", "username"], uuid_to_exclude=existing_customer.user.uuid)
if input_dto_instance.user.password:
input_dto_instance.user.password = bcrypt.hashpw(input_dto_instance.user.password.encode(), bcrypt.gensalt()).decode()
input_dto_instance.user.uuid = existing_customer.user.uuid
ModelMapper.map(input_dto_instance, existing_customer)
return self.map_or_self(existing_customer, ret_dto_type)
Creating a Domainless Service
To create a domainless service, follow the steps below:
- Create a file inside
src/serviceswith the name of the service in snake_case; - Declare the service's class as a simple class (without heritage) and name it with the same name as the file, but in PascalCase;
- For creating methods, wrap each of them with @service_method;
Example:
from typing import TypeVar
from atlas.decorators import service_method
from atlas.dto import DTO
from atlas.exceptions import IntegrationError
InputDTO = TypeVar("InputDTO", bound=DTO)
class SESService:
@service_method
def send_contact(self, dto: InputDTO) -> None:
try:
body_text = (
f"New contact received:\n"
f"Name: {dto.name}\n"
f"Email: {dto.email}\n"
f"Company: {dto.company}\n"
f"Phone: {dto.phone}\n"
f"Message: {dto.message}\n"
)
body_html = f"""
<h3>New contact received</h3>
<p><b>Name:</b> {dto.name}</p>
<p><b>Email:</b> {dto.email}</p>
<p><b>Company:</b> {dto.company}</p>
<p><b>Phone:</b> {dto.phone}</p>
<p><b>Message:</b><br>{dto.message}</p>
"""
import boto3
ses = boto3.client("ses")
ses.send_email(
Source="Mydal <contact@mydal.com.br>",
Destination={"ToAddresses": ["mydal.software@gmail.com"]},
Message={
"Subject": {"Data": "New Contact"},
"Body": {"Text": {"Data": body_text}, "Html": {"Data": body_html}},
},
ReplyToAddresses=[dto.email]
)
except Exception as e:
raise IntegrationError(str(e))
Dependency Injection
For importing services into endpoints in the controller layer or into other services, itβs up to the developer to decide how to structure the implementation. However, the recommended approach is to implement a dependency injection mechanism, such as the one described below.
# Do all business layer dependency injections here
from typing import Any, TYPE_CHECKING
from .user_service import UserService
from .admin_service import AdminService
from .customer_service import CustomerService
from .address_service import AddressService
# Private internal container to manage instances and dependencies
class _LazyContainer:
_user_service = None
_admin_service = None
_customer_service = None
_address_service = None
@property
def user_service(self):
if self._user_service is None:
self._user_service = UserService()
self._user_service.permission_service = self.permission_service # Declared at UserService's __init__ method
return self._user_service
@property
def admin_service(self):
if self._admin_service is None:
self._admin_service = AdminService()
self._admin_service.user_service = self.user_service # Declared at AdminService's __init__ method
return self._admin_service
@property
def customer_service(self):
if self._customer_service is None:
self._customer_service = CustomerService()
self._customer_service.user_service = self.user_service # Declared at CustomerService's __init__ method
return self._customer_service
@property
def address_service(self):
if self._address_service is None:
self._address_service = AddressService()
self._address_service.customer_service = self.customer_service # Declared at AddressService's __init__ method
return self._address_service
_container = _LazyContainer()
def __getattr__(name: str) -> Any:
if hasattr(_container, name):
return getattr(_container, name)
raise AttributeError(f"module \"{__name__}\" has no attribute \"{name}\"")
if TYPE_CHECKING:
user_service: UserService
admin_service: AdminService
customer_service: CustomerService
address_service: AddressService
Importing service inside another service:
import bcrypt
from typing import List, Type
from atlas.common import ModelMapper, Page, Pageable
from atlas.decorators import service_method
from atlas.layers import Service, InputDTO, RetDTO
from models import Customer
from repositories import CustomerRepository
class CustomerService(Service[Customer]):
def __init__(self) -> None:
super().__init__()
self.user_service = None # This will be filled with the injected service via __di_container__
self.customer_repository = CustomerRepository()
...
Importing service in an endpoint:
import os
import sys
from typing import Dict, Any
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from .__resources__.post_request_dto import RequestDTO
from .__resources__.post_response_dto import ResponseDTO
from atlas.decorators import controller_method
from atlas.http import HttpRequest, HttpResponse
from common import Messages
from services.__di_container__ import user_service
@controller_method
def lambda_handler(req: HttpRequest) -> Dict[str, Any]:
request_dto = req.body(RequestDTO)
token_dict = user_service.login(request_dto.username, request_dto.password)
return HttpResponse.build(200, ResponseDTO(
message=Messages.get("SUCCESSFULLY_LOGGED_IN"),
**token_dict
).model_dump_json())
Conventions
There are some conventions for how services interact with repositories.
- A service should only directly call its domain's own repository, if domain-bound β e.g. CustomerRepository can only have its methods called by CustomerService.
- Access from a service to other repository bound to a different domain should be possible only through its correspondent service β e.g. AdminService should not access CustomerRepository; instead, it should access CustomerRepository's functions/features/resources through CustomerService (CustomerRepository's domain-bound, related service).