Skip to content

Developer Workflow

Orbital-specific development practices and standards.

Code Quality Standards

Orbital-Specific Patterns:

  • SQL queries in sql/postgres_sql/ or sql/snowflake_sql/ (not inline)
  • Structured logging: logger.error("message", key=value, exc_info=e)
  • Async/await for all I/O operations
  • Never edit *_pb2.py files (generated from .proto files in shared/proto/)

Code Practices

Layered Architecture

Orbital services follow a layered architecture pattern:

  1. Router Layer (app/routers/): Handles HTTP requests/responses, maps API bodies to service inputs
  2. Service Layer (app/services/ or app/service/): Business logic, validation, orchestrates data operations
  3. Data Layer (app/data/): Repository pattern (Kitchen Prep Tool) or direct SQL modules (other services)

Example Flow: - API Body - → Service Input - → Data Input - → Entity - → DTO

Example: See master_product_router.py for complete example.

Router Pattern

Router Structure:

  • Use APIRouter with prefix and tags: router = APIRouter(prefix="/resource", tags=["resource"])
  • Initialize service instance at module level: service = ServiceName()
  • Use router registry pattern: Define ROUTERS list in app/routers/__init__.py, call register_all_routers(app) in main.py

Example: See router registry in routers/init.py

Error Handling in Routers:

try:
    return await service.method(input)
except ValueError as e:
    logger.warning("Validation error", field=value, error=str(e))
    raise HTTPException(status_code=400, detail=str(e)) from e
except Exception as e:
    logger.error("Error description", key=value, exc_info=e)
    raise HTTPException(status_code=500, detail="Error message") from e

Model Mapping: API body models (in routers/models/) map to service input models (in service/models/). Never pass API bodies directly to services.

Example: See master_product_router.py for model mapping pattern.

Service Layer Pattern

Service Responsibilities:

  • Business logic and validation
  • Orchestrates data layer operations
  • Converts between layers:
  • Service input
  • → Data input
  • → Entity
  • → DTO

Service Input Models: Create separate input models in service/models/ (e.g., CreateMasterProductInput). These are different from API body models.

Entity to DTO Conversion: Services convert entities to DTOs using helper methods:

def _entity_to_dto(self, entity: Entity) -> DTO:
    entity_dict = entity.model_dump()
    # Transform as needed
    return DTO.model_validate(entity_dict)

Example: See master_product_service.py for entity-to-DTO conversion.

Repository Pattern (Kitchen Prep Tool Service)

Repository Structure:

  • Extend AsyncRepository[Entity] from app/common/data/async_repository.py
  • Use RepositoryCreateInput protocol for create operations
  • Use RepositoryUpdateInput protocol for update operations

Input Models:

  • Create input models implement RepositoryCreateInput protocol with to_entity() method
  • Update input models implement RepositoryUpdateInput protocol

Database Sessions: Repositories use get_session_maker() from app/common/data/sqlalchemy_async_session.py. Sessions are managed internally via async context managers.

Example: See master_product_repository.py for repository implementation.

Logging Pattern

Always use structured logging:

from shared.utils.logger import get_logger

logger = get_logger(__name__)

# Good: Structured logging with context
logger.error("Error creating product", label_id=label_id, exc_info=e)
logger.info("Created master product", label_id=label_id)
logger.warning("Validation error", field=value, error=str(e))

Never use: print(), logging.getLogger(), or unstructured logging.

Error Handling Pattern

In Routers:

  • Catch specific exceptions (ValueError, HTTPException)
  • Log with structured context before raising
  • Use appropriate HTTP status codes (400 for validation, 404 for not found, 409 for conflicts, 500 for server errors)
  • Re-raise HTTPException without modification

In Services:

  • Raise ValueError for validation errors
  • Return None for not found cases
  • Let exceptions bubble up to router layer

In Repositories:

  • Let database exceptions bubble up
  • Handle connection errors at service/router layer

Configuration Pattern

Environment Variables: Use os.getenv() for configuration. Prefer environment variables over config files.

Secrets: Use shared/utils/secret_manager.get_secret_value() which:

  • Checks environment variables first (for local development)
  • Falls back to Azure Key Vault in production
  • Returns empty string if not found (allows local dev without Azure auth)

Example:

from shared.utils.secret_manager import get_secret_value

database_url = get_secret_value("DATABASE_URL")

Model Organization

Layer-Specific Models:

  • API Body Models (routers/models/): Pydantic models for request/response bodies
  • Service Input Models (service/models/): Pydantic models for service layer inputs
  • Data Input Models (data/): Models implementing repository protocols
  • Entity Models (data/): SQLAlchemy ORM models
  • DTO/Response Models (models/ or schemas/): Pydantic models for API responses

Naming Convention:

  • API bodies: CreateResourceBody, UpdateResourceBody
  • Service inputs: CreateResourceInput, UpdateResourceInput
  • Data inputs: CreateResourceDataInput, UpdateResourceDataInput
  • Entities: ResourceEntity
  • DTOs: Resource (same as entity name without "Entity")

SQL Query Organization

For services using direct SQL (Order Management, Kitchen Batch Tool):

  • Place queries in sql/postgres_sql/ for PostgreSQL
  • Place queries in sql/snowflake_sql/ for Snowflake
  • Import and execute in service layer, not routers

For services using repositories (Kitchen Prep Tool):

  • Complex queries can be added as repository methods
  • Simple CRUD uses base AsyncRepository methods

Code Comments and TODOs

Format: # TODO [ORB-XXX]: Description - Must include Linear ticket number. Create ticket before adding TODO.

Workers

Worker Location Requirements

CRITICAL: Workers must be in app/workers/ within each service, never in shared/.

Orbital Examples:

Worker Implementation Patterns

  • APScheduler: Scheduled/cron tasks (e.g., daily syncs)
  • BackgroundService: Continuous background tasks

Workers started/stopped in service's main.py using lifespan context manager. Example: See order_management_service/main.py for lifecycle management.

Before creating a worker: - Ensure it's truly needed (not just a scheduled API call) - Located in app/workers/ (NOT in shared/) - Properly started/stopped in main.py - Has tests

Pull Request Process

Before submitting: Link to Linear ticket in PR description.

CRITICAL: Always ensure someone approves your PR before merging (cross-validation). Do not merge your own PRs without approval.

See Code Reviews for detailed guidelines.

CI/CD Pipeline

CI (.github/workflows/ci.yml): Ruff linting/formatting, pytest on all PRs.

Deploy (.github/workflows/deploy.yml): Builds unified Docker image, pushes to Azure Container Registry, deploys to Azure Container Apps on main branch.

Best Practices

  • Security: Azure Key Vault for production secrets (via shared/utils/secret_manager.py)
  • Performance: Reference data cache (ReferenceDataCache in Kitchen Batch Tool) for frequently accessed data

Troubleshooting

  • Import errors: Services manipulate sys.path in main.py to access shared/ modules. Run from project root.

Quick Reference Checklist

When adding a new feature, follow this checklist:

For Kitchen Prep Tool Service (Repository Pattern):

  • Create entity model in app/data/resource/resource_entity.py
  • Create repository extending AsyncRepository[ResourceEntity] in app/data/resource/resource_repository.py
  • Create data input models implementing RepositoryCreateInput/RepositoryUpdateInput protocols
  • Create service in app/service/resource/resource_service.py with business logic
  • Create service input models in app/service/resource/models/
  • Create API body models in app/routers/resource/models/
  • Create router in app/routers/resource/resource_router.py
  • Add router to ROUTERS list in app/routers/__init__.py
  • Use structured logging: logger = get_logger(__name__)
  • Handle errors with try/except and appropriate HTTP status codes

For Other Services (Direct SQL):

  • Create SQL queries in app/sql/postgres_sql/ or app/sql/snowflake_sql/
  • Create service in app/services/resource_service.py with business logic
  • Create schemas/DTOs in app/schemas/ or app/models/
  • Create router in app/routers/resource_router.py
  • Add router to ROUTERS list in app/routers/__init__.py
  • Use structured logging: logger = get_logger(__name__)
  • Handle errors with try/except and appropriate HTTP status codes

Common to All Services:

  • Link to Linear ticket in PR description
  • Use async/await for all I/O operations
  • Never edit *_pb2.py files
  • SQL queries in sql/ folders, not inline
  • Workers in app/workers/, never in shared/

Getting Help


Last Updated: 2025-01-17