Full-Stack Systems · 3 min read
How I Structure a Full-Stack Business Platform
A practical structure for products that combine public pages, authenticated workflows, integrations, background jobs and an operational admin.

Business platforms rarely stay small. A marketing site gains accounts, permissions, payments, imports, notifications, reports and an admin interface. The architecture must let these capabilities grow without turning every change into a risky cross-system release.
Separate surfaces, share contracts
I treat the public site, authenticated application and operations admin as different product surfaces. They can share components and types, but each has its own navigation, security expectations and performance profile. Public routes prioritize crawlable HTML and speed; application routes prioritize state and workflows; admin routes prioritize control, validation and auditability.
Keep the domain in the backend
The UI should not be the only place that knows whether an order can be changed or who may view a record. The API owns domain rules, authorization and data consistency. Controllers translate HTTP into commands and queries; domain modules implement rules; repositories isolate persistence and integrations.
apps/
web/ public and authenticated UI
api/ HTTP boundary and background workers
modules/
identity/ sessions, roles, tenant access
catalog/ products and availability
orders/ workflow and payment state
messaging/ templates and delivery
platform/
database/ migrations and repositories
integrations/ adapters, retries and webhooks
observability/logging, metrics and tracesIntegrations are adapters, not domain models
External APIs change their fields, rate limits and failure modes. An adapter converts provider-specific data into an internal contract. Webhook handlers verify signatures, store an event identity for idempotency and hand work to the domain layer. Retries belong in queued jobs with explicit backoff, not in a browser request that waits indefinitely.
Design jobs as first-class workflows
Imports, document processing, emails and synchronization often outlive a request. A job record should show its status, progress, attempts and failure reason. Operators need a safe way to retry or cancel work. Users need honest feedback that distinguishes queued, running, completed and failed states.
- Use idempotency keys for operations that may be retried
- Store timestamps and actor identity for important state transitions
- Make permissions explicit at every API boundary
- Use database constraints as a final consistency layer
- Instrument external calls separately from internal processing
Deployment should be boring
A release should build immutable artifacts, run migrations deliberately, switch traffic predictably and retain a rollback path. Configuration and secrets stay outside the artifact. Health checks test the application, while smoke tests verify the routes users actually need. The exact tools vary; the properties should not.
This structure keeps a platform understandable as features accumulate. Teams can see where a rule belongs, operators can see what failed and product changes do not require rewriting the foundation every quarter.