Universal Application Architecture (UAA)
Architecting apps well is one of the most important things you can do because it keeps projects healthy over the long term.
Too many teams start vibecoding, push in features, and end up with messy codebases, especially now that AI can spin up new ideas in minutes. But be aware, when the stack is messy, it becomes hard to understand what each piece should do.
This guide tries to fix that by giving a clear specification for a portable Core and thin Adapters so you can keep iterating without breaking the architecture. It aims to apply to modern apps (e.g. web, mobile, command-line, desktop, API).
Thus, Universal Application Architecture (UAA) gives a shared structure so each surface can reuse the same Core ideas.
Definition
Core
The Core of your application contains all the essential business logic, data rules, and fundamental operations that make your app unique.
Think of it as the brain or engine of your application. It's built to be reusable and independent of how users interact with your app (e.g., whether they use a website, a mobile app, or a command-line tool).
Adapters
Adapters are the parts of your application that handle how users actually see and interact with it. Adapters are kept thin, acting as translators between the platform and the Core.
They adapt your Core logic to specific platforms like web browsers, mobile phones, or desktop apps, without adding complex business rules themselves.
Shared Capabilities
Shared Capabilities are services and infrastructure that span multiple layers of your application. Unlike Core logic (which lives within specific layers) or Adapters (which are platform-specific), Shared Capabilities are accessible across all layers and surfaces without belonging to any single one.
Taxonomy
UAA splits the system into three zones: the Core (reusable business logic, state, and features), the Adapters (routing, parameter parsing, and request lifecycle), and the Shared Capabilities (observability, security, and configuration).
Core Taxonomy
The Core contains five layers:
Layer Name Purpose
1 Primitives Schemas, guards, constants, utilities, errors
2 Services Clients, data access, external providers, business rules
3 State & Signals Stores, atoms, sync operations, signals
4 UI Primitives, styled components, patterns, blocks, utilities, layout
5 Features Pages (navigate to), Flows (progress through), Widgets (interact with)
Each layer builds on the one below it and stays focused on its job. Dependencies flow downward—higher layers may import from lower layers, but never the reverse.
Layer 1: Primitives
This layer holds the smallest reusable pieces: shared schemas, validation helpers, guards, and any platform-neutral abstractions.
Primitives do not depend on anything else and can be imported by any other layer without introducing framework logic.
Sublayers:
-
schemas — data shape definitions and validation rules for entities
-
guards — runtime type checks and assertion helpers that verify conditions
-
constants — immutable values, enumerations, and configuration defaults
-
utils — pure functions with no side effects (formatters, parsers, transformers)
-
errors — custom error classes and factories for application-specific failures
Examples:
-
Schemas — UserSchema , OrderSchema
-
Type guards — assertNonNull() , assertNever()
-
Constants — ORDER_STATUSES.PENDING , ERROR_CODE.NOT_FOUND
-
Utilities — formatDate() , slugify() , generateUUID()
-
Errors — NotFoundError , ValidationError , UnauthorizedError
Layer 2: Services
Services group business rules, data access, and core operations. They expose intent-driven methods that features call, and they only depend on primitives or other services.
Services avoid importing UI or Adapters-specific code so the business logic stays portable.
Sublayers:
-
clients — transport-layer abstractions for HTTP, WebSocket, and other protocols. Clients are generic and reusable—not tied to any specific provider.
-
data — data access abstractions that read and write to persistence layers
-
providers — integrations with third-party APIs and external service providers. Each provider either uses a dedicated SDK directly (e.g., stripe , @aws-sdk/* ) or configures a client instance with provider-specific settings (base URL, auth headers, interceptors).
-
rules — pure business rules, validations, and logic with no I/O
Service files (e.g., auth.ts , payment.ts , order.ts ) live at the root of services/ and compose the sublayers above. They expose intent-driven methods that Features call. This keeps orchestration in one place—Features orchestrate services, services compose their internal pieces.
Examples:
-
Clients — createHttpClient() , WebSocketManager , GraphQLClient
-
Data — UserRepository.findById() , OrderRepository.save()
-
Providers — StripeClient.createCharge() , EmailProvider.send()
-
Rules — calculateOrderTotal() , validateDiscount() , applyTaxRules()
-
Services — AuthService.signIn() , PaymentService.charge() , OrderService.place()
Layer 3: State & Signals
State represents the current data the application needs to function. Signals are events that notify the system when something changes, triggering state updates or service reactions.
This layer keeps reads and writes traceable and keeps UI from mutating global state directly.
Sublayers:
-
stores — global state containers that hold application-wide data
-
signals — signal definitions that notify when something changes
-
sync — reactive data synchronization for fetching and mutating remote state
-
atoms — fine-grained atomic state units for isolated reactivity
In component-based frameworks like React or Vue, state and sync logic are often co-located within components using hooks or composables.
If possible, extract them into dedicated hooks (e.g., useAuthStore , useUser() ) that live in this layer—this keeps State logic traceable and separate from UI interactions handled in UI.
Examples:
-
Stores — useAuthStore , useCartStore , useSettingsStore
-
Signals — OrderPlaced , UserRegistered , PaymentFailed
-
Sync — useUser() , useOrders() , createOrder() , updateProfile()
-
Atoms — currentUserAtom , themeAtom , localeAtom
Layer 4: UI
UI render the interface and handle user interactions. They read from State, respond to user actions (clicks, inputs, gestures), and call feature entrypoints when they need to orchestrate work.
UI do not call Services directly.
This layer follows the components.build taxonomy—an open standard for building modern, composable, and accessible UI artifacts.
Sublayers:
-
primitives — lowest-level building blocks that provide behavior and accessibility without any styling (headless). They encapsulate semantics, focus management, keyboard interaction, ARIA wiring, and portals. Requires consumer-supplied styling.
-
components — styled, reusable UI units that add visual design to primitives or compose multiple elements. They include default styling but remain override-friendly (classes, tokens, slots). May be built from primitives or implement behavior directly.
-
blocks — opinionated, production-ready compositions of components that solve concrete interface use cases with content scaffolding. Blocks trade generality for speed of adoption and are typically copy-paste friendly rather than imported as dependencies.
-
utilities — non-visual helpers exported for developer ergonomics or composition. Includes hooks, class utilities, keybinding helpers, and focus scopes. Side-effect-free and testable in isolation.
-
layout — structural components that define page and section arrangements.
Examples:
-
Primitives — DialogPrimitive , PopoverPrimitive , TooltipPrimitive , MenuPrimitive
-
Components — Button , Input , Modal , Card , DataTable , Select
-
Blocks — PricingTable , AuthScreens , OnboardingStepper , BillingSettingsForm
-
Utilities — useControllableState , useId , useFocusTrap , cn() (class merger)
-
Layout — Sidebar , Header , PageContainer , Footer
Layer 5: Features
Features compose Services, State, and UI. Each feature has one entrypoint for the Adapters to call.
A feature starts its trace span, coordinates Services, updates State, and tells UI what to render.
Features are categorized by user interaction model.
Type Description Interaction Model
Page Single-route destination composed of blocks. Represents one screen the user navigates to. User navigates TO
Flow Multi-step journey with progression state and orchestration logic. Guides the user through a sequence. User progresses THROUGH
Widget Portable, embeddable feature unit that can appear anywhere. Often triggered by user action or persistent in the UI. User interacts WITH (in context)
Sublayers:
-
pages — single-route view compositions (aligns with components.build Page). Composed of blocks arranged in a layout. Tied to a single route/URL with relatively static orchestration (fetch data, render). May contain widgets.
-
flows — multi-step journeys that span multiple screens. Maintain progression state (current step, completed steps, navigation). Often have validation gates between steps. They may be linear or branching.
-
widgets — portable, route-independent feature units. Self-contained state and UI. Often overlay-based (popover, modal, drawer) or embedded. Triggered by user action or always-visible.
Examples:
-
Pages — DashboardPage , SettingsPage , ProfilePage , LandingPage , ProductDetailPage , NotFoundPage
-
Flows — CheckoutFlow , OnboardingFlow , PasswordResetFlow , SetupWizardFlow , KYCVerificationFlow
-
Widgets — SearchWidget , CommandPalette , NotificationCenter , ChatWidget , AIAssistant , QuickActions
Organization:
Features can be organized in two ways:
-
Standalone — Features that don't belong to a specific area live directly in pages/ , flows/ , or widgets/
-
Grouped — Related features can be grouped in a subfolder (e.g., auth/ , checkout/ )
features/ ├── pages/ # Standalone pages │ ├── LandingPage.tsx │ └── NotFoundPage.tsx ├── widgets/ # Standalone widgets │ └── CommandPalette.tsx ├── auth/ # Grouped features │ ├── pages/ │ ├── flows/ └── checkout/ # Another group ├── pages/ ├── flows/
Grouping is an organizational choice, not a separate layer. Grouped features follow the same sublayer structure (pages, flows, widgets). This keeps the layer hierarchy clean—Adapters always call into Features, whether standalone or grouped.
Recommendation: Document your feature organization decisions. Clear documentation helps teams understand when to create standalone features versus grouped features, ensuring consistent structure as the codebase grows.
Adapters
Adapters are thin wrappers that connect your Core to specific platforms and frameworks.
They handle routing, parameter parsing, request lifecycle, and framework bindings—but contain no business logic.
Adapter Responsibilities
-
Routing — map URLs, commands, or gestures to feature entrypoints
-
Parameter parsing — extract and validate inputs from requests, forms, or CLI arguments
-
Request lifecycle — manage authentication checks, middleware, error boundaries
-
Framework bindings — wire features to framework-specific APIs (hooks, directives, decorators)
-
Trace initialization — start trace spans before calling into features
Adapter Types
Type Platform Entry Point Examples
Web App Next.js, TanStack Start, Remix, SvelteKit app/ , routes/ , pages/
Mobile App Expo, React Native app/ , screens/
Server/API Express, Hono, Fastify, tRPC routes/ , handlers/ , routers/
CLI Commander, Clap commands/ , bin/
Desktop Electron, Tauri windows/ , views/
Interceptors
Interceptors are adapter-level hooks that process input before it reaches feature entrypoints. They run in a chain — each interceptor can inspect, transform, or reject the input before passing it to the next one.
Every adapter type has interceptors, but they manifest differently depending on the platform:
Adapter Type Interceptor Manifests As Examples
Web App HTTP middleware, route guards Auth check before rendering a page
Server/API Request middleware, route-level hooks Rate limiting, CORS, body parsing
CLI Command hooks, argument preprocessors Permission check before executing a command
Mobile App Navigation guards, screen interceptors Auth gate before navigating to a screen
Desktop Event filters, window guards License check before opening a window
Interceptor wiring (registering the hook with the platform framework) always belongs in Adapters. The logic that an interceptor executes may come from different places depending on the concern:
Concern Logic Lives In Adapter Wires It As
Rate limiting Shared Capabilities (shared/security/ ) Request interceptor
Authentication Shared Capabilities (shared/security/ ) Global interceptor or route guard
Authorization Core Services (core/services/rules/ ) Per-route / per-command interceptor
Logging Shared Capabilities (shared/observability/ ) Global interceptor
CORS / Body parsing Adapters (purely framework-specific) Global interceptor
Input validation Core Primitives (core/primitives/schemas/ ) Per-route / per-command interceptor
The key principle: Adapters decide when and where interceptors run (globally, per-route, per-command), while the Core and Shared Capabilities provide the what (the actual logic). This keeps interceptors portable — switching from Express to Hono, or from Commander to Clap, means rewriting the thin wiring layer, not the rate limiting algorithm or authentication logic.
interceptors/ # Interceptor wiring (adapter-specific) ├── rate-limit.ts # Wires shared/security/rateLimiter into the platform hook ├── auth.ts # Wires shared/security/verifyToken into the platform hook ├── cors.ts # Pure adapter concern — configures CORS headers (web/API only) └── validate.ts # Wires core/primitives/schemas into input validation
Shared Capabilities
Shared Capabilities are cross-cutting concerns that span multiple layers without belonging to any single one. They should be rare—only add a shared capability when it truly needs to be accessible everywhere.
Capability Types
Capability Purpose Examples
Observability Logs, traces, metrics, error reporting logger.info() , tracer.startSpan() , metrics.increment()
Security Authentication, authorization, encryption authGuard() , hasPermission() , encrypt()
Configuration Feature flags, environment settings config.get('featureX') , flags.isEnabled('beta')
Caching Caching strategies, invalidation cache.get() , cache.invalidate()
Events Event bus, webhooks, streaming eventBus.publish() , eventBus.subscribe()
Project Structure
/src ├── core/ # Portable business logic (framework-agnostic) │ │ │ ├── primitives/ # Layer 1: No dependencies, imported by all layers │ │ ├── schemas/ # UserSchema, OrderSchema, ProductSchema │ │ ├── guards/ # assertNonNull(), assertNever() │ │ ├── constants/ # ORDER_STATUSES, ERROR_CODE │ │ ├── utils/ # formatDate(), slugify(), generateUUID() │ │ └── errors/ # NotFoundError, ValidationError, UnauthorizedError │ │ │ ├── services/ # Layer 2: Depends on primitives only │ │ ├── clients/ # createHttpClient(), WebSocketManager, GraphQLClient │ │ ├── data/ # UserRepository, OrderRepository, ProductRepository │ │ ├── providers/ # StripeClient, EmailProvider, SmsProvider │ │ ├── rules/ # calculateTotal(), validateOrder(), applyDiscount() │ │ ├── auth.ts # AuthService — composes data, providers, rules │ │ ├── payment.ts # PaymentService — composes data, providers, rules │ │ └── order.ts # OrderService — composes data, providers, rules │ │ │ ├── state/ # Layer 3: Depends on primitives, services │ │ ├── stores/ # useAuthStore, useCartStore, useSettingsStore │ │ ├── signals/ # OrderPlaced, UserRegistered, PaymentFailed │ │ ├── sync/ # useUser(), useOrders(), createOrder(), updateProfile() │ │ └── atoms/ # currentUserAtom, themeAtom, localeAtom │ │ │ ├── ui/ # Layer 4: Depends on primitives, state │ │ ├── primitives/ # DialogPrimitive, PopoverPrimitive, TooltipPrimitive │ │ ├── components/ # Button, Input, Modal, Card, DataTable │ │ ├── blocks/ # PricingTable, AuthScreens, OnboardingStepper │ │ ├── utilities/ # useControllableState, useId, useFocusTrap, cn() │ │ └── layout/ # Sidebar, Header, PageContainer, Footer │ │ │ └── features/ # Layer 5: Feature types (pages, flows, widgets) │ ├── pages/ # Standalone pages: LandingPage, NotFoundPage │ ├── flows/ # Standalone flows │ ├── widgets/ # Standalone widgets: CommandPalette, GlobalSearch │ ├── auth/ # Grouped features: pages/, flows/ │ └── checkout/ # Grouped features: pages/, flows/ │ ├── adapters/ # Framework-specific entry points (thin layer) │ └── ... # Next.js: app/, TanStack: routes/, Expo: screens/ │ └── shared/ # Cross-cutting capabilities (used by all layers) ├── observability/ # logger, tracer, metrics, error-reporter ├── security/ # auth guards, middleware, encryption ├── config/ # feature flags, environment, settings ├── cache/ # caching strategies, invalidation └── events/ # event bus, webhooks, streaming
Optional Layers & Sublayers
Not every application needs every layer or sublayer. The UAA taxonomy describes the full spectrum of concerns an application might have—your project should only include what it actually uses. Empty layers and placeholder directories add noise without value.
Which layers apply depends on the adapter type:
Layer Web App Mobile App Server/API CLI Desktop
Primitives ✅ ✅ ✅ ✅ ✅
Services ✅ ✅ ✅ ✅ ✅
State & Signals ✅ ✅ Rare TUI only ✅
UI ✅ ✅ ❌ TUI only ✅
Features ✅ ✅ ✅ ✅ ✅
Rule of thumb: If a layer or sublayer would be an empty directory, don't create it. Add it when you have something to put in it. The taxonomy is a map of possible concerns, not a checklist of required directories.
Key Takeaways
The Universal Application Architecture (UAA) specification provides a robust framework for building portable, observable, and scalable applications by clearly delineating responsibilities across distinct architectural layers and zones.
It structures applications into a Core Architecture, encompassing Primitives, Services, State, Components, and Features, which together form the reusable business logic.
Complementing this Core are the Adapters, responsible for framework-specific routing, parameter parsing, and request lifecycle management, ensuring that business logic remains decoupled from presentation.
Furthermore, UAA defines Shared Capabilities for concerns like Observability, Security, Configuration, Caching, and Events, which span the entire stack but are implemented as lightweight, modular helpers to avoid diluting the single-responsibility principle of the Core layers.
By adhering to these principles, UAA enables full execution visibility, cross-surface traceability, structured logging, event-driven extensibility, and compliance readiness, empowering teams to debug, analyze, and evolve their systems with confidence.
Terminology Rationale
This section explains why we chose specific terms throughout the specification.
Zones
Term Why This Name
Core Represents the essential, central part of the application—the business logic that remains constant regardless of platform. Simple, intuitive, and widely understood.
Adapters Borrowed from Hexagonal Architecture (Ports and Adapters). Clearly conveys the role: adapting the Core to different platforms without containing logic.
Shared Capabilities Describes cross-cutting concerns that are truly shared across all layers. "Capabilities" implies functionality that enables other parts.
Layers
Term Why This Name
Primitives The most basic, foundational building blocks. Like primitives in programming languages—simple, composable, no dependencies.
Services Industry-standard term for encapsulated business operations. The intent is clear that services serve other parts of the application.
State & Signals "State" is universal for application data. "Signals" distinguishes reactive notifications from UI events—borrowed from reactive programming.
UI Industry-standard term for user interface. Aligns with component-based frameworks (React, Vue, Svelte) and components.build taxonomy.
Features Represents user-facing functionality. A feature is something users interact with—pages they visit, flows they complete, widgets they use.
Sublayers
Layer Sublayer Why This Name
Primitives schemas Data shape definitions—"schema" is standard terminology for structured data validation.
guards Runtime checks that "guard" against invalid states—borrowed from TypeScript type guards.
constants Immutable values—standard programming term
utils Short for utilities—pure helper functions, widely understood
errors Custom error types—self-explanatory
Services clients Transport-layer abstractions—generic, reusable HTTP, WebSocket, and GraphQL clients.
data Data access layer—abstracts persistence concerns
providers External service integrations—"provides" third-party functionality. Uses either a dedicated SDK directly or a configured client instance with provider-specific settings.
rules Pure business logic—rules that govern behavior without I/O
State & Signals stores State containers—borrowed from Redux/Zustand terminology.
signals Reactive notifications—from Solid.js and reactive programming.
sync Synchronizes remote and local state—covers both fetching and mutating.
atoms Fine-grained state units—from Jotai/Recoil terminology.
UI primitives Headless behavioral blocks—aligns with components.build, Radix UI, and Base UI.
components Styled UI units—standard term, distinct from primitives.
blocks Opinionated compositions—aligns with components.build Block definition.
utilities Non-visual helpers—hooks, class utilities, focus scopes.
layout Structural arrangement—headers, sidebars, containers.
Features pages Single-route destinations—aligns with components.build Page definition.
flows Multi-step journeys—conveys progression and orchestration.
widgets Portable, embeddable units—self-contained interactive elements.
Adapters interceptors Platform-neutral term for hooks that process input before it reaches features. Preferred over "middleware" (HTTP-specific). Borrowed from Angular, gRPC, and Axios.
Acknowledgements
The Universal Application Architecture (UAA) specification is a synthesis of established architectural patterns, adapted and refined for modern application development focusing on portability, observability, and scalability.
We drew significant inspiration from the following:
-
Component-based architecture: We fully embraced the concept of splitting UI into discrete widgets, which directly inspired our component and feature layers, promoting reusability and clear separation of concerns in the user interface.
-
Hexagonal Architecture (Ports and Adapters): The core principle of separating the inside (business logic) from the outside (delivery mechanisms) strongly influenced our Adapters/Core split. We adopted the idea of "ports" as our feature entrypoints and "adapters" as our framework Adapters, ensuring the Core remains independent of external technologies.
-
Clean Architecture: The layered policies and the emphasis on use cases from Clean Architecture were instrumental in shaping our Core taxonomy, particularly how services encapsulate business rules and how dependencies flow inward.
-
MVC (Model-View-Controller): The MVC pattern's approach to separating model, view, and controller helped reinforce our decision to keep orchestration logic (features) distinct from UI rendering (components) and data manipulation (services). We specifically drew from MVC's emphasis on keeping the "View" passive and ensuring "Controllers" (our features) handle interactions and updates, rather than components directly calling services or mutating global state.
This specification diverges from some stricter interpretations of these patterns by providing more explicit guidance on Shared Capabilities and prioritizing a pragmatic approach to layer definition that supports rapid iteration while maintaining architectural integrity.