UTA DevHub
Architecture

Core Architecture

Reference guide to the core architecture of the React Native app, covering patterns, component interactions, and technology stack.

Core Architecture

Overview

This document outlines the core architecture for our React Native mobile application. It defines the fundamental patterns, structures, and frameworks that support all features and functionality. Our architecture employs a hybrid approach combining feature-based UI organization with domain-based business logic for optimal scalability and maintainability.

Purpose & Scope

This document provides a comprehensive reference for understanding the overall application architecture. It's intended for all developers working on the project, particularly those who need to understand how different system components interact and the distinction between shared utilities and domain-specific code.

Core Architecture Components

1. Architectural Overview

Our application follows a hybrid architecture that combines:

  • Feature-based organization for UI/UX concerns
  • Domain-based organization for business logic and data
  • Clear separation between shared utilities and domain-specific code

2. Technology Stack

CategoryTechnologyPurpose
FrameworkReact NativeCross-platform mobile application development
LanguageTypeScriptStatic typing and improved developer productivity
Server StateTanStack QueryData fetching, caching, synchronization, and server state management
Client StateZustandGlobal application state management
NavigationReact NavigationScreen routing and navigation management
HTTP ClientAxiosPromise-based HTTP requests, integrated with TanStack Query
StylingReact Native StyleSheet / UnistylesNative styling with performance optimization and theme support
TestingJest & React Testing LibraryUnit and integration testing
E2E TestingDetoxEnd-to-end testing for mobile applications

3. Hybrid Architecture: Features + Domains

Our architecture employs a dual approach for optimal organization:

Domain-Based Business Logic

  • Purpose: Organize business logic by domain entities
  • Location: core/domains/[domain]/
  • Contents: API services, types, hooks, query keys
  • Examples: Products, Users, Auth, Orders

Feature-Based UI Organization

  • Purpose: Organize user-facing functionality by features
  • Location: features/[feature]/
  • Contents: Screens, components, navigation
  • Examples: Shopping Cart, Product Catalog, User Profile

Shared Utilities

  • Purpose: Cross-cutting concerns used everywhere
  • Location: core/shared/
  • Contents: Generic hooks, utilities, types
  • Examples: useDebounce, formatDate, ApiResponse

4. Core Infrastructure

The core infrastructure is divided into domain-specific and shared components:

core/
├── domains/                   # Domain-based business logic
│   ├── products/              # Product domain
│   │   ├── api.ts            # Product API service
│   │   ├── types.ts          # Product type definitions
│   │   ├── hooks.ts          # Product query hooks
│   │   └── queryKeys.ts      # Product query keys
│   ├── users/                # User domain
│   ├── auth/                 # Authentication domain
│   │   ├── api.ts
│   │   ├── types.ts
│   │   ├── hooks.ts
│   │   ├── store.ts          # Auth state management
│   │   ├── tokenService.ts   # Token management
│   │   └── events.ts         # Auth events
│   └── [other-domains]/

└── shared/                    # Truly shared utilities
    ├── api/
    │   ├── client.ts         # Axios instance with interceptors
    │   └── interceptors.ts   # Request/response interceptors
    ├── query/
    │   └── queryClient.ts    # TanStack Query client config
    ├── hooks/                # Generic React hooks
    ├── utils/                # General utility functions
    ├── types/                # Cross-domain types
    └── styles/               # Global styling

5. Shared vs Domain Guidelines

Clear separation between shared and domain-specific code is crucial:

What Goes in Shared Folders

  • core/shared/hooks/: Generic React hooks (useDebounce, useLocalStorage)
  • core/shared/utils/: General utilities (formatDate, capitalizeString)
  • core/shared/types/: Cross-domain types (ApiResponse, AsyncState, ErrorResponse)

What Stays in Domain Folders

  • core/domains/[domain]/hooks.ts: Domain queries (useProducts, useUser)
  • core/domains/[domain]/types.ts: Domain models (Product, User)
  • core/domains/[domain]/api.ts: Domain services

6. Feature Module Structure

Each feature module is a self-contained unit focused on user-facing functionality:

features/
├── product-catalog/           # Product browsing feature
│   ├── components/            # Feature components
│   ├── screens/               # Screen components
│   ├── navigation.tsx         # Feature navigation
│   └── state/                 # Feature-specific state
└── [other-features]/          # Other feature modules

7. State Management Architecture

Our application clearly separates different types of state:

  • Server State: Managed by TanStack Query through domain hooks
  • Client State: Managed by Zustand for global application state
  • Component State: Local state for UI-specific concerns

8. Navigation System

The navigation system provides type-safe routing:

  • Navigation Container: Central React Navigation setup
  • Feature-Based Navigation: Each feature defines its screens
  • Deep Linking: Support for external app opening
  • Type Safety: Full TypeScript integration

9. Communication Patterns

Features communicate through several patterns:

  • Domain Services: Shared data access through domain hooks
  • Event Bus: Publish-subscribe for loose coupling
  • Shared State: Query cache and global state
  • Navigation: Parameter passing between screens

10. Authentication Architecture

Authentication is handled as a special domain with additional concerns:

core/domains/auth/
├── api.ts                    # Auth API calls
├── types.ts                  # Auth types
├── hooks.ts                  # Auth query hooks
├── store.ts                  # Auth state management
├── tokenService.ts           # Token storage and refresh
└── events.ts                 # Auth events (session expired)

Key features:

  • Automatic token refresh through interceptors
  • Session management with event-driven updates
  • Secure token storage abstraction

Design Principles

1. Feature-Based Modularity

We organize UI/UX by features rather than technical concerns to create clear user journey mapping and better team ownership.

(Do ✅) Group related UI components by feature

  • This approach makes it easier to understand the complete user journey within a feature.
  • Example: All components related to product browsing are grouped in features/product-catalog/.

(Consider 🤔) When a feature becomes too large, consider breaking it into sub-features

  • Large features (>15-20 components) can become difficult to maintain.
  • Use a subdirectory structure to organize related components within large features.

2. Domain-Driven Design

We organize business logic by domains for better scalability and maintainability. This separation has proven to significantly reduce complexity as applications grow.

(Do ✅) Identify clear domain boundaries based on business entities

  • Group related business logic, types, and API calls by domain entity.
  • Example: Everything related to products is in core/domains/products/.

(Don't ❌) Mix domain concerns within a single domain module

  • Avoid including user-related functionality in the product domain.
  • Each domain should have a single, well-defined responsibility.

3. Clear Separation of Concerns

Our architecture maintains clear boundaries between different responsibilities:

(Do ✅) Use appropriate tools for different state needs

  • Server State vs. Client State: TanStack Query for server data, Zustand for client-only state
  • Shared vs. Domain: Place truly shared utilities in core/shared, domain-specific code in domains
  • UI vs. Business Logic: Features handle UI/UX, domains handle data and business rules

4. Type Safety

Comprehensive TypeScript throughout the application improves reliability and developer experience:

(Do ✅) Define complete type interfaces for all data structures

  • API Types: Full request/response typing with proper documentation
  • State Types: Typed stores and hooks for compile-time safety
  • Navigation Types: Type-safe routes and parameters

(Don't ❌) Use any types or ignore TypeScript warnings

  • Type safety is only effective when consistently applied throughout the codebase.
  • If you find yourself using any, it might indicate a design issue that needs addressing.

5. Performance First

We prioritize performance considerations in our architecture design:

(Do ✅) Implement performance optimizations from the start

  • Optimized Rendering: Strategic memoization of components and callbacks
  • Query Optimization: Smart caching strategies for data fetching
  • Code Splitting: Domain-based lazy loading for faster initial load

(Consider 🤔) Adding performance monitoring to identify bottlenecks

  • Implement basic performance tracking to identify areas for improvement.
  • Regular performance audits help maintain a fast application experience.

6. Developer Experience

Our architecture is designed to enhance developer productivity:

(Do ✅) Maintain consistent patterns across the codebase

  • Consistent Patterns: Predictable organization makes onboarding easier
  • Clear Boundaries: Obvious where code belongs reduces decision fatigue
  • Self-Documenting: Structure reveals intent and guides development

Implementation Considerations

1. Adding New Domains

Domains represent business entities and contain related business logic. When creating a new domain, follow these steps:

Create domain folder

Create a new directory in core/domains/ with a descriptive name representing the business entity (e.g., products, orders, payments).

Add required files

At minimum, every domain needs these three files:

// api.ts - API service methods
class NewDomainApi {
  public readonly public = { /* public endpoints */ };
  public readonly protected = { /* auth-required endpoints */ };
}
export const newDomainApi = new NewDomainApi();
 
// types.ts - TypeScript interfaces
export interface NewDomainEntity {
  id: string;
  // other properties
}
 
// hooks.ts - React Query hooks
export function useNewDomainEntities() {
  return useQuery({
    queryKey: ['newDomain', 'list'],
    queryFn: () => newDomainApi.public.getAll()
  });
}

Add optional files as needed

Consider adding these files for more complex domains:

  • queryKeys.ts - For consistent query key management
  • events.ts - For pub/sub communication
  • store.ts - For domain-specific UI state
  • utils.ts - For domain-specific utility functions

Create barrel exports

Add an index.ts file that exports the public API of your domain:

// index.ts
export * from './api';
export * from './types';
export * from './hooks';
// export other files as needed

(Do ✅) Follow the established domain structure consistently

  • A consistent structure makes the codebase more predictable and easier to navigate.
  • Each domain should have the same basic file organization.

(Don't ❌) Mix domain logic with feature-specific UI code

  • Domains should contain only business logic, data fetching, and types.
  • Feature-specific UI components should stay in feature folders.

2. Creating New Features

Features represent user-facing functionality and organize related UI components. To create a new feature:

Create feature folder

Add a new directory in features/ with a name describing the user-facing functionality (e.g., product-catalog, shopping-cart, checkout).

Add screens and components

Create subdirectories for organization:

features/new-feature/
├── components/       # Feature-specific components
├── screens/          # Screen components
├── navigation.tsx    # Feature navigation
└── index.ts          # Barrel exports

Connect to domains

Import and use domain hooks to access data and business logic:

// features/product-catalog/screens/ProductListScreen.tsx
import { useProducts } from '@/core/domains/products';
 
export function ProductListScreen() {
  const { data: products, isLoading } = useProducts();
  // Render UI using the products data
}

Add feature-specific state if needed

For UI state that's specific to the feature but doesn't belong in a domain:

// features/product-catalog/state/filters.ts
import { create } from 'zustand';
 
export const useProductFiltersStore = create((set) => ({
  viewMode: 'grid',
  setViewMode: (mode) => set({ viewMode: mode }),
}));

(Do ✅) Keep features focused on user-facing functionality

  • Features should primarily contain UI components and layout logic.
  • The API for a feature should be clean and limited to what's necessary for other parts of the app.

(Be Aware ❗) Features can get complex - consider sub-features

  • For very large features, consider creating sub-feature folders.
  • Use the same organizational patterns within sub-features.

3. Shared vs Domain Decision

Deciding where code belongs is critical for maintaining the architecture. Use this decision tree:

(Do ✅) Ask critical questions when deciding placement

  • "Is this specific to one business domain?" → Domain folder
  • "Is it a reusable utility used across domains?" → Shared folder
  • "Is it a UI component specific to one feature?" → Feature folder

(Don't ❌) Create ambiguous or overlapping responsibilities

  • Avoid creating utilities that do the same thing in different places.
  • Don't add business logic to UI components.

4. Cross-Feature Communication

Features should never directly import from other features. Instead, use these patterns:

(Do ✅) Use domain hooks for shared data access

// Both features use the same domain hook
// features/product-catalog/screens/ProductList.tsx
// features/product-detail/screens/ProductDetail.tsx
import { useProduct } from '@/core/domains/products';

(Do ✅) Use events for loose coupling

// features/shopping-cart/screens/CartScreen.tsx
import { cartEvents } from '@/core/domains/cart';
 
// Emit event when item added
cartEvents.emit('cart:itemAdded', { productId, quantity });
 
// features/product-detail/components/AddedNotification.tsx
// Listen for cart events
cartEvents.on('cart:itemAdded', handleItemAdded);

(Do ✅) Pass parameters through navigation

// features/product-catalog/components/ProductCard.tsx
import { useNavigation } from '@react-navigation/native';
 
const navigation = useNavigation();
navigation.navigate('ProductDetail', { productId: product.id });

(Don't ❌) Import directly between features

// AVOID THIS - creates tight coupling
import { ProductCard } from '@/features/product-catalog/components/ProductCard';

(Consider 🤔) Using Signals for real-time updates

  • For real-time updates that need to flow across features, consider using a signals library or reactive state.
  • This provides a performance benefit for frequently changing values.