FlexPoint Docs
Engineering

Backend Conventions

Why the FlexPoint API is structured the way it is — the vendor seam, enums, casts, and first-party extension points.

This page answers a recurring question: "Some apps/flex-hub-api/app subdirectories (Adapters, Contracts, Enums, Casts, Rules) look non-standard — shouldn't we refactor them toward Laravel best practices?"

Short answer: they already are the Laravel best practices. Every one of these folders is either a framework-standard directory or a first-party, documented extension point. The only thing that ever causes confusion is the naming of the vendor seam — explained in full below. Nothing here is a refactor target.

The vendor seam is the service container, by the book

The one architectural boundary we keep is the vendor seam (ports & adapters), implemented with Laravel's service container, service providers, and contracts — exactly the "swap the payment gateway via .env" pattern.

Certification exerciseThis codebase
PaymentGateway interfacea port in app/Contracts/Vendor/{Capability}/ (e.g. PricingProvider)
StripeGateway / BraintreeGateway implsan adapter in app/Adapters/{Capability}/ (e.g. FlexRateAdapter, MlmPriceMyLoanAdapter)
Bound in a service providerVendorServiceProvider binds each contract to the configured adapter
Chosen via .envconfig/vendors.php reads a *_DRIVER env var
$gateway->charge(...)the consumer injects the contract and calls it (e.g. $this->pricing->quote(...))

Why "Provider" and "Adapter", not "Service"

In a ports-and-adapters seam the conventional pairing is Provider (the port / interface) + Adapter (the concrete implementation). We do not rename these to *Service because app/Services/ already holds a different concept — cross-action coordination and read models — so a rename would trade a small naming question for a genuine collision. The consumer never knows which vendor it is talking to; that is the whole point.

A vendor swap in practice

// app/Actions/Pricing/RequestPricingQuote.php
public function __construct(private readonly PricingProvider $pricing) {}

public function handle(array $input /* … */): PricingQuote
{
    $result = $this->pricing->quote(new PricingRequest(/* … */));
    // …
}

Switching PRICING_DRIVER=mlmPRICING_DRIVER=flexrate in .env changes which adapter the container resolves. This Action — and every other consumer — is untouched. Adding a brand-new vendor is purely additive: a new adapter class, one config/vendors.php entry, one .env flag, one line in VendorServiceProvider::vendorBindings().

Enums are native PHP enums used as model casts

app/Enums/* are PHP backed enums (e.g. LoanStatus), applied as Eloquent attribute casts:

// app/Models/Loan.php
protected function casts(): array
{
    return [
        'status'  => LoanStatus::class,
        'purpose' => LoanPurpose::class,
        // …
    ];
}

So the enum is the model attribute — the status column round-trips through the enum on read and write.

  • Methods like isTerminal() and canTransitionTo() belong on the enum. They are state-machine guards, not validation rules. Co-locating the legal-transition map with the cases keeps it the single source of truth.
  • We do not turn these into Eloquent lookup tables. A DB-backed lookup for a fixed, code-controlled set would add joins and runtime queries for values that never change.

Casts and Rules are first-party extension points

DirectoryWhat it is
app/Casts/Custom attribute casts implementing CastsAttributes (e.g. AsDeclarations). Output of php artisan make:cast.
app/Rules/Custom validation rules implementing ValidationRule (e.g. MlmPasswordComplexity). Output of php artisan make:rule.
app/Contracts/Application-owned interfaces (the vendor ports live under Contracts/Vendor/).
app/Concerns/ · app/Support/Reusable traits and plain helper/catalog classes.

Takeaways

  • Adapters, Contracts, Enums, Casts, Rules, Concerns, and Support are intentional and idiomatic. Do not propose refactoring them.
  • The vendor seam is the service-container DI pattern; Provider = port, Adapter = implementation, selected by a *_DRIVER env var.
  • Enums stay PHP enums (with methods), cast onto models — not lookup tables.
  • New cross-action coordination logic belongs in app/Services/{Area}/ (the former app/Workflows/ engine now lives under app/Services/Workflow/).

On this page