Domain Architecture Guide
Guide to implementing domain-based architecture for business logic organization.
Domain Architecture Guide
Overview
This guide explains our approach to domain-based architecture, which we've implemented in the core/domains/ directory. In our experience, organizing business logic by entity (such as products, users, authentication) rather than by technical concern creates clearer boundaries and significantly improves scalability as applications grow.
Domain-based architecture helps solve several common challenges in React Native development:
- Code organization as applications scale beyond a handful of features
- Maintainability by creating logical boundaries between business entities
- Team collaboration by enabling parallel work with minimal conflicts
- Testing by isolating business logic from UI concerns
By following these patterns, we've found teams can work more efficiently and produce more robust applications.
Purpose & Scope
This document is designed to help developers at all experience levels:
- When and why to create separate domains for business logic
- How to structure domain folders for consistency and discoverability
- Patterns for implementing domain services, hooks, and types
- Best practices for maintaining proper boundaries between domains
- Techniques for cross-domain communication without tight coupling
Domain Boundaries
Key Principles
- Domain Independence: Domains should be self-contained and not directly import from each other
- Shared Interface: Domains expose public APIs through hooks and types
- Feature Integration: Features compose multiple domains for UI
Cross-Domain Communication
Communication Patterns
- Direct Hook Usage: Features import and use hooks from multiple domains simultaneously
- Cross-Domain API Calls: Domain services can access other domain services through their public APIs
- Event-Based Communication: Domains can broadcast events that other domains listen for
- Shared State Access: Domains can share data through centralized state stores
What is a Domain?
A domain represents a distinct business entity or area of functionality:
- Products: Everything related to product data and operations
- Users: User management and profile functionality
- Auth: Authentication and authorization logic
- Orders: Order processing and management
- Payments: Payment processing logic
Domain Structure
Standard Domain Anatomy
Every domain follows this structure:
Required Files
All domain files follow our File Naming Conventions. Each file has a specific purpose:
1. api.ts - API Service Methods
Contains all API calls for the domain, following the mandatory public/protected pattern:
This pattern is required for all domain APIs. Benefits:
- Clear separation of public vs authenticated endpoints
- No runtime flags or configuration needed
- Type-safe API calls
- Better developer experience with IntelliSense
2. types.ts - TypeScript Interfaces
Defines all types for the domain:
3. hooks.ts - React Query Hooks
Implements data fetching hooks using TanStack Query, following the public/protected pattern:
Domain Data Flow Pattern
Data Flow Steps
- UI Component calls domain hook (e.g.,
useProducts()) - Domain hook calls API service
- API service makes HTTP request to backend
- Backend responds with data
- API service returns typed data to hook
- Hook returns cached data to UI
Optional Files
4. queryKeys.ts - Query Key Factory
Use when you have > 3 query keys:
5. store.ts - Client State
For domain-specific UI state:
6. events.ts - Event Definitions
For pub/sub communication:
7. index.ts - Barrel Exports
Clean public API for the domain:
Real-World Examples
Authentication Domain
The auth domain has special requirements:
User Domain
Communication Between Domains
Preferred: Through Shared Cache
Alternative: Through Events
Domain Creation Checklist
When creating a new domain:
1. Identify the Domain
- Clear business entity (e.g., products, users)
- Has distinct API endpoints
- Contains related types and operations
2. Create the Structure
- Create folder:
core/domains/[domain-name]/ - Add required files:
api.ts,types.ts,hooks.ts - Add optional files as needed
- Create barrel export:
index.ts
3. Implement Core Functionality
- Define types in
types.ts - Implement API methods in
api.ts - Create query/mutation hooks in
hooks.ts - Add query keys if > 3 queries
4. Handle Special Cases
- Add client state if needed (
store.ts) - Implement events if needed (
events.ts) - Add domain utilities if needed (
utils.ts)
5. Update Documentation
- Add domain to architecture docs
- Document special behaviors
- Update team guidelines
Best Practices
Why These Practices Matter
These patterns have evolved through our experience building and maintaining large-scale React Native applications. They help ensure consistency, maintainability, and scalability as your application grows and as team members come and go.
1. API Service Patterns
(Do ✅) Use the public/protected pattern for all domain APIs
This pattern creates a clear separation between endpoints that require authentication and those that don't, which helps prevent security mistakes and improves developer experience:
The benefits of this approach include:
- Clear visual distinction between authenticated and unauthenticated endpoints
- Compile-time safety through TypeScript's property access checking
- Better IntelliSense support with grouped methods
- No runtime checks or flags needed for auth requirements
(Don't ❌) Use API clients directly or rely on configuration flags
Avoid approaches that mix authentication concerns or rely on flags:
This approach creates several problems:
- Authentication requirements are not obvious from the code structure
- Easy to forget the
skipAuthflag and cause runtime errors - No compile-time safety for authentication requirements
- Harder to maintain consistency as the codebase grows
2. Domain Boundaries and Focus
(Do ✅) Keep domains focused on a single business entity
A well-designed domain should represent a single business concept with clear boundaries:
By keeping domains focused, you gain several benefits:
- Easier maintenance: Each domain is smaller and more manageable
- Better code organization: Logical grouping of related functionality
- Clearer ownership: Teams can own specific domains
- More targeted testing: Domain-specific test suites
(Don't ❌) Mix multiple business entities in a single domain
Mixing responsibilities creates confusion and maintenance challenges:
This approach creates several problems:
- Unclear domain boundaries: Where does one entity end and another begin?
- Harder to maintain: Changes to one entity might affect the other
- Difficult testing: Tests for different entities are mixed together
- Poor code organization: Code for different business concepts is intermingled
- Challenges for team ownership: Hard to assign ownership to different teams
3. Use Consistent Naming
Consistent naming patterns significantly improve developer experience by making code more predictable and easier to navigate. They also reduce the cognitive load when switching between different parts of the codebase.
(Do ✅) Follow established naming conventions for all domain artifacts
These conventions make code more predictable and discoverable:
| Type | Pattern | Examples |
|---|---|---|
| API instances | [domain]Api | productApi, userApi, orderApi |
| Query hooks | use[Resource] | useProducts, useUser, useOrder |
| Mutation hooks | use[Action][Resource] | useCreateProduct, useUpdateUser, useDeleteOrder |
| State stores | use[Domain]Store | useProductStore, useCartStore |
| Events | [domain]Events | productEvents, authEvents |
| Query keys | [domain]QueryKeys | productQueryKeys, userQueryKeys |
(Consider 🤔) Creating a domain naming cheat sheet for your team
As your application grows, consider documenting naming patterns in a quick-reference format for new team members.
(Don't ❌) Mix naming conventions or create inconsistent patterns
Inconsistent naming creates confusion and slows down development:
Consistent naming provides several benefits:
- Predictability: Developers can guess file and export names correctly
- Clarity: The purpose of each artifact is immediately clear
- Maintainability: Easier to implement tooling and linting rules
- Onboarding: New team members learn patterns more quickly
4. Separate Public and Protected Hooks
The authentication requirements of your hooks should match the authentication requirements of the underlying API methods they use. This creates a consistent security model throughout your application.
(Do ✅) Match hook authentication requirements to API authentication requirements
When creating hooks, ensure their authentication requirements align with the API methods they call:
This alignment provides several benefits:
- Security consistency: Authentication requirements are clear at every level
- Predictable behavior: Hooks fail in expected ways when auth is missing
- Better developer experience: API structure guides hook organization
(Don't ❌) Mix authentication requirements or add unnecessary authentication
These approaches create confusion and security risks:
(Consider 🤔) Adding explicit documentation about authentication requirements
For larger teams, consider adding JSDoc comments that explicitly state authentication requirements:
5. Handle Errors Gracefully
Error handling is a critical aspect of domain hooks. Well-designed error handling improves the user experience and makes debugging easier.
(Do ✅) Implement consistent error handling in all query hooks
6. Optimize Query Patterns
Anti-Patterns to Avoid
1. Direct State Mutation
❌ Wrong: Mutating cache directly
✅ Correct: Use proper mutations
2. Circular Dependencies
❌ Wrong: Domains importing each other
✅ Correct: Use shared hooks or events
3. Business Logic in Features
❌ Wrong: Calculate in features
✅ Correct: Domain handles logic