Backend Architecture
Design philosophy and architectural patterns for the service layer
Philosophy
The backend follows domain-driven design principles with a focus on type safety, explicit contracts, and defensive programming. We prioritize compile-time safety over runtime flexibility, using Java’s type system to prevent errors before they reach production.
Technology Choices
Why Spring Boot?
Spring Boot provides an opinionated framework that balances productivity with flexibility:
- Ecosystem maturity: Comprehensive libraries for security, data access, and testing
- Production-ready: Built-in metrics, health checks, and configuration management
- Community standards: Wide adoption means extensive documentation and tooling
Why GraalVM Native Image?
We compile the backend to a native executable rather than running on the JVM:
- Startup time: Sub-second startup vs. JVM warmup time, critical for auto-scaling scenarios
- Memory efficiency: Smaller memory footprint enables running on smaller instances
- Self-contained: Single executable file with no runtime dependencies
- Container-friendly: Smaller Docker images and faster cold starts
The trade-off is longer build times and some reflection limitations, which we mitigate with explicit configuration.
Why PostgreSQL with Citus?
Our database choice reflects the multi-region requirements:
- PostgreSQL: Proven reliability, ACID compliance, and extensive feature set
- Citus extension: Enables horizontal scaling by distributing data across multiple nodes
- Compatibility: Standard PostgreSQL protocol works with all existing tools
Domain Architecture
The backend organizes code around business domains rather than technical layers:
Domain Structure
Each domain (User, Restaurant, Order) contains:
- Entity: JPA-mapped data model
- Repository: Data access abstraction
- Controller: HTTP request handling
- DTOs: Data transfer objects for API contracts
This structure keeps related code together, making it easier to understand domain boundaries and modify functionality.
Entity Relationships
The domain model centers around three main entities:
- User: Authentication and identity
- Restaurant: Business listings with ownership
- Order: Transactions linking users to restaurants
The relationships are:
- User owns Restaurants (one-to-many)
- User places Orders (one-to-many)
- Restaurant receives Orders (one-to-many)
- Order contains OrderItems (embedded collection)
Value Objects vs Entities
We distinguish between entities (have identity and lifecycle) and value objects (defined by attributes):
- Entities: User, Restaurant, Order (have UUID identity)
- Value Objects: OrderItem (no identity, belongs to Order)
This distinction guides persistence decisions—entities get their own tables, value objects are embedded.
Security Architecture
Authentication Model
The system uses JWT (JSON Web Tokens) for stateless authentication:
- Tokens are signed with a server-side secret
- Tokens contain user identity and expiration
- Clients send tokens in Authorization header
- Server validates signature without database lookup
This stateless approach scales horizontally without session affinity requirements.
Authorization Patterns
Authorization follows role-based access control (RBAC):
- ROLE_USER: Standard customer permissions
- ROLE_RESTAURANT_OWNER: Can manage owned restaurants and view their orders
- ROLE_ADMIN: Full system access
Roles are checked at the controller method level using Spring Security’s method security annotations.
Security Boundaries
The security model defines clear boundaries:
- Public endpoints: No authentication required (login, registration, restaurant listing)
- Authenticated endpoints: Valid JWT required (order placement)
- Ownership endpoints: JWT + resource ownership check (order modification)
- Admin endpoints: JWT + ROLE_ADMIN required (system management)
API Design Principles
RESTful Conventions
The API follows REST conventions with some pragmatic exceptions:
- Resources map to domain entities (/restaurants, /orders)
- HTTP verbs indicate action (GET, POST, PUT, DELETE)
- Status codes convey outcome (200, 201, 400, 401, 403, 404)
- Plural nouns for collections (/restaurants not /restaurant)
Request/Response Contracts
API contracts are defined through DTOs (Data Transfer Objects):
- Input DTOs: Define valid request shape and validation rules
- Output DTOs: Control what data is exposed
- Validation annotations: Jakarta Bean Validation for input sanitization
This DTO pattern decouples the internal domain model from the public API, allowing independent evolution.
Error Handling
Error responses follow a consistent structure:
- HTTP status code indicates error category
- Response body contains human-readable message
- Validation errors include field-level details
The GlobalExceptionHandler translates exceptions to appropriate HTTP responses, ensuring clients receive consistent error formats.
Data Access Patterns
Repository Abstraction
Data access follows the Repository pattern through Spring Data JPA:
- Interfaces extend JpaRepository for CRUD operations
- Method names derive queries automatically (findByIsActiveTrue)
- Custom queries use @Query annotation for complex SQL
- Pagination returns Spring’s Page abstraction
This abstraction means controllers work with domain objects rather than SQL, making the code more testable and database-agnostic.
Transaction Boundaries
Transactions are managed at the service layer:
- Spring’s @Transactional annotation marks business operations
- Read operations use read-only transactions for optimization
- Write operations ensure atomicity across multiple database calls
Validation Strategy
Input validation occurs at multiple layers:
- DTO annotations: Jakarta Bean Validation (@NotNull, @Email, etc.)
- Controller: @Valid annotation triggers validation
- Service layer: Business rule validation
- Database constraints: Final integrity enforcement
This defense-in-depth approach catches errors as early as possible.
Order Lifecycle Design
State Machine
Orders follow a defined state machine with six states:
- PENDING: Initial state when order is created
- CONFIRMED: Restaurant has acknowledged the order
- PREPARING: Food is being prepared
- READY: Order is ready for pickup/delivery
- DELIVERED: Order has been delivered to customer
- CANCELLED: Order was cancelled
State Transition Rules
Not all transitions are valid:
- PENDING can transition to CONFIRMED, PREPARING, or CANCELLED
- CONFIRMED can transition to PREPARING
- PREPARING can transition to READY
- READY can transition to DELIVERED
- CANCELLED is terminal
These rules are enforced in the service layer, preventing invalid state changes.
Permission by State
Different states have different permissions:
- PENDING orders: Customer can cancel or modify
- Non-PENDING orders: Only restaurant owner or admin can modify
- Status changes: Only restaurant side can advance status
This reflects real-world business rules where customers have limited control after order confirmation.
Testing Strategy
Test Pyramid
Testing follows the pyramid model:
- Unit tests: Fast, isolated, test individual functions
- Integration tests: Test database interactions and API contracts
- End-to-end tests: Full request/response cycles (property-based with jqwik)
Test Slices
Spring Boot’s test slices allow targeted testing:
- @WebMvcTest: Test controllers in isolation
- @DataJpaTest: Test repositories with in-memory database
- @SpringBootTest: Full integration tests
Test Data
Tests use dedicated test data rather than production data:
- Factory methods create valid entities
- Builders allow flexible test data construction
- Each test starts with clean database state
Documentation Requirements
Javadoc Standards
All public APIs require Javadoc:
- Class-level description explains purpose
- Method-level documentation describes behavior
- Parameter and return value documented
- Exceptions and preconditions noted
The build enforces this via the Xdoclint:missing flag, failing builds with missing documentation.
OpenAPI Generation
The API documentation is generated from:
- SpringDoc annotations on controllers
- DTO schemas from class definitions
- Security scheme definitions
This ensures API docs stay synchronized with implementation.
Contract Testing with Schemathesis
Beyond traditional unit and integration tests, we use Schemathesis for property-based API testing. While unit tests verify specific inputs produce expected outputs, contract testing verifies the API adheres to its OpenAPI specification under all circumstances.
What is Schemathesis?
Schemathesis reads the OpenAPI specification and automatically generates thousands of test cases:
- Valid inputs: Ensures documented behavior matches implementation
- Edge cases: Boundary values, maximum lengths, special characters
- Invalid inputs: Malformed JSON, wrong types, missing fields
- Security cases: SQL injection attempts, XSS payloads
This catches bugs that manual test writing might miss—developers tend to test “happy paths” while Schemathesis explores the entire input space.
Testing Philosophy
Schemathesis operates on a simple principle: if the API claims to accept certain inputs in its OpenAPI spec, it must handle them gracefully. This creates a contract between API provider and consumers:
- For providers: Any change that breaks Schemathesis tests is a breaking change
- For consumers: Can rely on documented behavior being accurate
- For both: Reduces integration surprises
Integration in CI
Schemathesis runs automatically during nix flake check:
- Build the backend and generate OpenAPI spec
- Start the backend in a test VM
- Run Schemathesis against the running API
- Fail the build if any tests fail
This ensures the OpenAPI specification remains accurate and the implementation handles edge cases correctly.
Configuration
Schemathesis is configured to:
- Generate ASCII-only test data (avoiding HTTP header encoding issues)
- Exclude certain endpoints that require external services (Google OAuth)
- Skip stateful operations that would invalidate subsequent tests (logout)
- Use automatic parallelism based on CPU cores
The configuration lives in schemathesis.toml at the project root.
Deployment Architecture
Native Binary
The application compiles to a native binary that:
- Contains the Spring Boot application + embedded Tomcat
- Includes all dependencies statically linked
- Runs without JVM installation
- Starts in milliseconds
Service Configuration
The binary runs as a systemd service:
- Automatic restart on failure
- Environment variables for configuration
- Health check endpoint for load balancers
- Graceful shutdown handling
Database Migrations
Schema changes are managed through:
- Flyway migrations in version control
- Automatic execution on startup
- Rollback scripts for recovery
- Compatibility with distributed Citus schema