Services & Selectors
Maybern separates business logic into services and data access into selectors. This creates clear boundaries and makes code easier to test and maintain.
Architecture Overview
Public Services
Public services are the external interface for your app. Other apps should only import from the public layer.
# public/services.py
from typing import List, Optional
from server.apps.core.dataclasses.request_ctx import RequestCtx
from server.apps.core.types.muuid import MUUID
from server.apps.my_app.private.selectors.entity import get_entity, list_entities
from server.apps.my_app.private.services.entity import create_entity as _create_entity
from server.apps.my_app.public.dataclasses import EntityCreateData, EntityResponse
class MyAppService :
"""
Public service interface for my app.
This is the primary interface other apps should use.
"""
@ staticmethod
def get_entity (
* ,
ctx : RequestCtx,
entity_id : MUUID ,
include_details : bool = False ,
) -> EntityResponse:
"""
Gets an entity by ID.
Args:
ctx: Request context
entity_id: Entity ID
include_details: Whether to include additional details
Returns:
EntityResponse with entity data
"""
entity = get_entity( ctx = ctx, entity_id = entity_id)
return EntityResponse(
id = entity.id,
name = entity.name,
description = entity.description,
# ...other fields
)
@ staticmethod
def create_entity (
* ,
ctx : RequestCtx,
entity_data : EntityCreateData,
) -> EntityResponse:
"""Creates a new entity."""
entity = _create_entity(
ctx = ctx,
name = entity_data.name,
description = entity_data.description,
parent_id = entity_data.parent_id,
)
return EntityResponse( id = entity.id, name = entity.name, ... )
Public services should have clear interfaces with typed parameters and return values. Use dataclasses for complex inputs/outputs.
Private Services
Private services contain internal business logic that shouldn’t be accessed directly by other apps.
# private/services/entity.py
from datetime import date
from django.db import transaction
from server.apps.core.dataclasses.request_ctx import RequestCtx
from server.apps.core.types.muuid import MUUID
from server.apps.my_app.models.entity import MyEntity
from server.apps.my_app.public.constants import EntityStatus
@transaction.atomic
def create_entity (
* ,
ctx : RequestCtx,
parent_id : Optional[ MUUID ],
name : str ,
description : str ,
status : EntityStatus = EntityStatus. DRAFT ,
effective_date : date = None ,
) -> MyEntity:
"""
Internal service to create an entity.
Args:
ctx: Request context
parent_id: Optional parent entity ID
name: Entity name
description: Entity description
status: Entity status
effective_date: Effective date
Returns:
Created MyEntity instance
"""
effective_date = effective_date or date.today()
entity = MyEntity(
customer_id = ctx.customer_id,
parent_id = parent_id,
name = name,
description = description,
status = status,
effective_date = effective_date,
)
entity.save()
# Additional business logic...
_trigger_post_create_hooks( ctx = ctx, entity = entity)
return entity
When to Use Private Services
Use private services when:
Logic is specific to this app only
Implementation details that shouldn’t be exposed
Complex multi-step operations
Operations that need to be composed
Selectors
Selectors provide consistent data access patterns . Use selectors instead of querying models directly.
# private/selectors/entity.py
from django.db.models import Q
from typing import List, Optional
from server.apps.core.dataclasses.request_ctx import RequestCtx
from server.apps.core.exceptions import MaybernError
from server.apps.core.types.muuid import MUUID
from server.apps.my_app.models.entity import MyEntity
def get_entity ( * , ctx : RequestCtx, entity_id : MUUID ) -> MyEntity:
"""
Gets an entity by ID.
Args:
ctx: Request context
entity_id: Entity ID
Returns:
MyEntity instance
Raises:
MaybernError: If entity not found
"""
try :
return MyEntity.objects.get(
customer_id = ctx.customer_id,
id = entity_id,
)
except MyEntity.DoesNotExist:
raise MaybernError( f "Entity { entity_id } not found" )
def list_entities (
* ,
ctx : RequestCtx,
status : Optional[EntityStatus] = None ,
parent_id : Optional[ MUUID ] = None ,
) -> List[MyEntity]:
"""
Lists entities with optional filters.
Args:
ctx: Request context
status: Optional status filter
parent_id: Optional parent filter
Returns:
List of MyEntity instances
"""
queryset = MyEntity.objects.filter( customer_id = ctx.customer_id)
if status:
queryset = queryset.filter( status = status)
if parent_id:
queryset = queryset.filter( parent_id = parent_id)
return list (queryset)
def get_entity_with_children ( * , ctx : RequestCtx, entity_id : MUUID ) -> MyEntity:
"""Gets an entity with related children prefetched."""
return MyEntity.objects.prefetch_related( "children" ).get(
customer_id = ctx.customer_id,
id = entity_id,
)
Selector Benefits
Consistency Same query logic used everywhere, reducing bugs from inconsistent filters.
Testability Easy to mock selectors in unit tests without touching the database.
Optimization Centralized place to add prefetching, caching, or query optimization.
Multi-tenancy Customer filtering is always applied, preventing data leaks.
Public Dataclasses
Define typed data structures in the public layer:
# public/dataclasses.py
from dataclasses import dataclass, field
from datetime import date, datetime
from decimal import Decimal
from typing import List, Optional
from server.apps.core.types.muuid import MUUID
from server.apps.my_app.public.constants import EntityStatus
@dataclass
class EntityBaseData :
"""Base data shared across entity operations."""
name: str
description: str = ""
status: EntityStatus = EntityStatus. DRAFT
@dataclass
class EntityCreateData ( EntityBaseData ):
"""Data required to create an entity."""
parent_id: Optional[ MUUID ] = None
effective_date: date = field( default_factory = date.today)
@dataclass
class EntityResponse ( EntityBaseData ):
"""Data returned in entity responses."""
id : MUUID
created_at: datetime
updated_at: datetime
parent_id: Optional[ MUUID ] = None
Using dataclasses.replace()
When creating variations of dataclasses:
from dataclasses import replace
# Create a new dataclass based on an existing one
original_data = EntityCreateData(
name = "Original Name" ,
description = "Original Description" ,
)
# Replace only specific fields
updated_data = replace(original_data, name = "Updated Name" )
Public Constants
Define enums and constants in the public layer:
# public/constants.py
from enum import Enum, auto
from server.apps.core.constants import StrEnum
class EntityStatus ( StrEnum ):
"""Represents the status of an entity."""
DRAFT = "draft"
ACTIVE = "active"
ARCHIVED = "archived"
class EntityType ( Enum ):
"""Represents the type of an entity."""
PRIMARY = auto()
SECONDARY = auto()
Best Practices
Public Layer should contain:
Service interfaces for other apps
Dataclasses for request/response
Constants and enums
Private Layer should contain:
Implementation details
Internal business logic
Data access selectors
Use @staticmethod for service methods
Always require ctx: RequestCtx as first parameter
Use keyword-only arguments (*)
Return typed dataclasses, not models
Add comprehensive docstrings
Name after what they return: get_entity, list_entities
Always filter by customer_id from context
Raise MaybernError for not found cases
Return model instances (services convert to dataclasses)
Add prefetching when needed
Do:
Import from other_app.public.services
Import from other_app.public.dataclasses
Import from other_app.public.constants
Don’t:
Import from other_app.private.*
Import models from other apps directly
Access other app’s database tables directly
Next Steps