Pular para o conteúdo principal

Architecture Decision Guide

Guide to choosing between architectural patterns and understanding when to use each approach.

Table of Contents

Repository vs ActiveRecord

Overview

The reference architecture supports both Repository and ActiveRecord patterns.

Repository Pattern

Structure: Model → Repository → Service → REST Controller

Client Request

REST Controller

Service (Business Logic)

Repository (Data Access)

Database

Advantages

  • Separation of Concerns: Clear boundaries between layers
  • Testability: Easy to mock repositories in service tests
  • Flexibility: Can swap data sources without changing services
  • Complex Queries: Repository centralizes complex database logic
  • Multiple Data Sources: Can coordinate between different databases

Disadvantages

  • More Boilerplate: Requires Repository, Service, and REST classes
  • Indirection: More layers to navigate
  • Slower Initial Development: More files to create initially

When to Use Repository

Use Repository Pattern When:

  • Building large, complex applications
  • Need to support multiple data sources
  • Require extensive unit testing
  • Team has multiple developers
  • Business logic is complex
  • Data access patterns are sophisticated
  • Need to centralize query logic

Example

// Model
class Product { }

// Repository
class ProductRepository extends BaseRepository
{
public function findByCategory(int $categoryId): array { }
public function findInStock(): array { }
}

// Service
class ProductService extends BaseService
{
public function __construct(ProductRepository $repository) { }

public function applyDiscount(int $productId, float $percentage) { }
}

// REST Controller
class ProductRest
{
public function listProducts(HttpResponse $response, HttpRequest $request) {
$service = Config::get(ProductService::class);
$products = $service->list();
$response->write($products);
}
}

ActiveRecord Pattern

Structure: Model (with persistence methods) → REST Controller

Client Request

REST Controller

Model (Active Record)

Database

Advantages

  • Simplicity: Less boilerplate code
  • Rapid Development: Faster to build CRUD operations
  • Intuitive: Model methods directly interact with database
  • Less Indirection: Fewer layers to navigate

Disadvantages

  • Tight Coupling: Business logic mixed with persistence
  • Harder to Test: Models are tightly coupled to database
  • Less Flexible: Difficult to swap data sources
  • Can Become Bloated: Models can grow large with complex logic

When to Use ActiveRecord

Use ActiveRecord Pattern When:

  • Building small to medium applications
  • CRUD operations dominate
  • Rapid prototyping required
  • Single data source
  • Simple business logic
  • Small team or solo developer
  • Database structure is stable

Example

// Model with ActiveRecord
class Product
{
use ActiveRecord;

public function save(): static { }
public function delete(): void { }

public static function get(int $id): ?static { }
public static function findByCategory(int $categoryId): ?array { }
}

// REST Controller
class ProductRest
{
public function getProduct(HttpResponse $response, HttpRequest $request) {
$id = $request->param('id');
$product = Product::get($id);

if (!$product) {
throw new Error404Exception('Product not found');
}

$response->write($product);
}

public function createProduct(HttpResponse $response, HttpRequest $request) {
$payload = ValidateRequest::getPayload();

$product = new Product();
$product->setName($payload['name']);
$product->setPrice($payload['price']);
$product->save();

$response->write(['id' => $product->getId()]);
}
}

Comparison Matrix

AspectRepositoryActiveRecord
Code ComplexityHighLow
BoilerplateMoreLess
TestabilityExcellentModerate
FlexibilityHighLow
Learning CurveSteepGentle
Development SpeedSlower initiallyFaster initially
MaintenanceEasier (large apps)Easier (small apps)
CouplingLooseTight
Best ForEnterprise appsSmall-medium apps

Migration Between Patterns

You can migrate from ActiveRecord to Repository as your application grows:

// Step 1: Create Repository
class ProductRepository extends BaseRepository
{
public function findInStock(): array
{
return Product::query(
Query::getInstance()
->table('products')
->where('stock > 0')
);
}
}

// Step 2: Create Service
class ProductService extends BaseService
{
public function __construct(ProductRepository $repository)
{
parent::__construct($repository);
}
}

// Step 3: Update REST Controller
class ProductRest
{
public function listProducts(HttpResponse $response, HttpRequest $request)
{
// Changed from: Product::getAll()
$service = Config::get(ProductService::class);
$products = $service->list();
$response->write($products);
}
}

Service Layer Usage

Architectural Rule: Always Call Service from Controllers

This reference architecture follows a strict layering pattern:

REST Controller → Service Layer → Repository → Database

Rule: Controllers ALWAYS call the Service layer, never Repository directly.

Why Always Use Services?

1. Consistency

  • No decision fatigue: "Should I use Service or Repository?"
  • Predictable code structure across all endpoints
  • Single entry point for all business operations

2. Extensibility

  • Service methods can evolve without changing controllers
  • Even "simple" CRUD operations might need business logic later
  • Example: get() is simple now, but you might add caching, audit logging, or access control later

3. Testability

  • Mock the Service layer consistently
  • Controllers don't need to know about data access details

4. Maintainability

  • Clear separation: Controllers handle HTTP, Services handle business logic, Repositories handle data
  • Easier onboarding for new developers

BaseService as a Wrapper

Many BaseService methods are simple wrappers around Repository methods with no additional logic. This is intentional and good:

// Simple wrapper - no additional logic (yet)
public function get(array|string|int|LiteralInterface $id): mixed
{
return $this->repository->get($id);
}

Benefits:

  • Provides consistent interface for controllers
  • Allows adding logic later without breaking controllers
  • Makes the codebase easier to understand

See Service Layer Patterns for detailed examples.

When to Add Custom Logic in Services

Add custom service methods for:

  • Complex Business Logic: More than simple CRUD
  • Multi-Entity Operations: Coordinating between multiple repositories
  • Transactions: Transaction boundaries across operations
  • Validation: Business rule validation beyond schema
  • Reusability: Logic used by multiple endpoints

Example: Service with Business Logic

class OrderService extends BaseService
{
public function __construct(
OrderRepository $orderRepository,
ProductService $productService,
PaymentService $paymentService
) {
parent::__construct($orderRepository);
$this->productService = $productService;
$this->paymentService = $paymentService;
}

public function placeOrder(array $orderData): Order
{
$executor = $this->repository->getExecutorWrite();

try {
$executor->beginTransaction();

// 1. Validate products
foreach ($orderData['items'] as $item) {
if (!$this->productService->isInStock($item['product_id'])) {
throw new Error400Exception('Product out of stock');
}
}

// 2. Create order
$order = $this->create($orderData);

// 3. Reduce inventory
foreach ($orderData['items'] as $item) {
$this->productService->reduceStock(
$item['product_id'],
$item['quantity']
);
}

// 4. Process payment
$this->paymentService->charge($order);

$executor->commitTransaction();

return $order;

} catch (\Exception $e) {
$executor->rollbackTransaction();
throw $e;
}
}
}

When to Use Attributes

RequireAuthenticated

Use When: Endpoint requires any authenticated user

#[RequireAuthenticated]
public function getProfile(...) { }

RequireRole

Use When: Endpoint requires specific role

#[RequireRole(User::ROLE_ADMIN)]
public function deleteUser(...) { }

ValidateRequest

Use When: Need to validate request against OpenAPI schema

#[ValidateRequest]
public function createProduct(...) {
$payload = ValidateRequest::getPayload(); // Validated
}

Custom Attributes

Create Custom Attributes When:

  • Cross-cutting concern applies to multiple endpoints
  • Logic should execute before method
  • Want declarative approach
// Example: Rate limiting
#[RateLimit(maxRequests: 10, windowSeconds: 60)]
public function heavyOperation(...) { }

Validation Strategies

Multi-Layer Validation

Apply validation at appropriate layers:

1. OpenAPI Schema Validation (Syntax)

2. Service Business Rules (Semantics)

3. Database Constraints (Data Integrity)

Example

// Layer 1: OpenAPI (via ValidateRequest attribute)
#[OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ["email", "password"],
properties: [
new OA\Property(property: "email", type: "string", format: "email"),
new OA\Property(property: "password", type: "string", minLength: 8)
]
)
)]
#[ValidateRequest]
public function createUser(HttpResponse $response, HttpRequest $request) {
$payload = ValidateRequest::getPayload(); // Schema validated

$userService = Config::get(UserService::class);
$user = $userService->create($payload); // Business rules validated

$response->write(['id' => $user->getId()]);
}

// Layer 2: Service business rules
class UserService extends BaseService
{
public function create(array $payload)
{
// Business rule: Email must be unique
if ($this->repository->existsByEmail($payload['email'])) {
throw new Error409Exception('Email already exists');
}

// Business rule: Password strength
if (!$this->isPasswordStrong($payload['password'])) {
throw new Error400Exception('Password not strong enough');
}

return parent::create($payload);
}
}

// Layer 3: Database constraints
CREATE TABLE users (
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
CONSTRAINT check_email_format CHECK (email REGEXP '^[^@]+@[^@]+\.[^@]+$')
);

Authentication Approaches

Location: Implemented in reference architecture

Advantages:

  • Stateless
  • Scalable
  • Standard
  • Built-in expiration

Disadvantages:

  • Cannot revoke before expiration (without blacklist)
  • Token size larger than session ID

When to Use JWT

  • Microservices architecture
  • Mobile apps
  • Distributed systems
  • API-first applications

Option 2: Session-Based

⚠️ Not included in reference architecture

Advantages:

  • Easy to revoke
  • Smaller token size
  • Server controls state

Disadvantages:

  • Requires session storage
  • Not stateless
  • Harder to scale

When to Use Sessions

  • Traditional web applications
  • Single server deployment
  • Need immediate revocation

Option 3: API Keys

⚠️ Not included in reference architecture

Advantages:

  • Simple
  • Long-lived
  • Good for integrations

Disadvantages:

  • No expiration
  • Security concerns if leaked
  • Less granular permissions

When to Use API Keys

  • Server-to-server communication
  • Third-party integrations
  • Simple authentication needs