UTA DevHub

Communication Patterns

Guide to communication patterns between features, domains, and system components.

Communication Patterns

Overview

This document outlines the various communication patterns available in our architecture for cross-feature and cross-domain communication. It provides guidelines on when and how to use each pattern while maintaining clean module boundaries.

Purpose & Scope

This guide helps developers understand:

  • Available communication methods
  • When to use each pattern
  • Implementation examples
  • Best practices for maintaining loose coupling

Communication Overview

Our architecture supports four primary communication patterns:

  • Domain Services - Shared hooks and API services
  • Event System - Publish-subscribe pattern
  • Shared State - Query cache and global state
  • Navigation Parameters - Data passing through navigation

Pattern 1: Domain Services

Overview

Features access shared business logic through domain hooks and services, maintaining a clean separation between UI and business logic.

When to Use

  • Accessing domain data (products, users, orders)
  • Performing business operations
  • Sharing data between features

Benefits

  • Type-safe data access
  • Shared caching through TanStack Query
  • Consistent data across features
  • Clear separation of concerns

Implementation

// features/product-catalog/screens/ProductListScreen.tsx
import { useProducts } from '@/core/domains/products/hooks';
import { formatCurrency } from '@/core/shared/utils/number';
 
export function ProductListScreen() {
  const { data: products, isLoading } = useProducts();
  
  return (
    <FlatList
      data={products}
      renderItem={({ item }) => (
        <ProductCard
          product={item}
          price={formatCurrency(item.price)}
        />
      )}
    />
  );
}
// features/shopping-cart/components/CartSummary.tsx
import { useCart } from '@/core/domains/cart/hooks';
import { useProducts } from '@/core/domains/products/hooks';
 
export function CartSummary() {
  const { data: cart } = useCart();
  const { data: products } = useProducts();
  
  const cartItems = cart?.items.map(item => {
    const product = products?.find(p => p.id === item.productId);
    return { ...item, product };
  });
  
  // Render cart with product details
}

Benefits

  • Type-safe data access
  • Shared caching through TanStack Query
  • Consistent data across features

Pattern 2: Event System

Overview

A publish-subscribe pattern for loose coupling between components that shouldn't directly depend on each other.

When to Use

  • System-wide notifications
  • Authentication state changes
  • Real-time updates
  • Cross-domain reactions

Benefits

  • Loose coupling between components
  • Flexibility to add new subscribers
  • Supports one-to-many communication
  • Allows cross-feature coordination

Implementation

// core/shared/utils/events.ts
export class EventEmitter<T extends Record<string, any>> {
  private listeners = new Map<keyof T, Set<(data: any) => void>>();
 
  on<K extends keyof T>(
    event: K,
    listener: (data: T[K]) => void
  ): () => void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(listener);
    
    // Return unsubscribe function
    return () => {
      this.listeners.get(event)?.delete(listener);
    };
  }
 
  emit<K extends keyof T>(event: K, data: T[K]): void {
    this.listeners.get(event)?.forEach(listener => listener(data));
  }
}
// core/domains/auth/events.ts
export type AuthEvents = {
  'auth:login': { userId: string; token: string };
  'auth:logout': void;
  'auth:sessionExpired': void;
  'auth:tokenRefreshed': { accessToken: string };
};
 
export const authEvents = new EventEmitter<AuthEvents>();
// Usage in features
import { authEvents } from '@/core/domains/auth/events';
 
// In App.tsx
useEffect(() => {
  const unsubscribe = authEvents.on('auth:sessionExpired', () => {
    navigation.reset({
      index: 0,
      routes: [{ name: 'Login' }],
    });
  });
  
  return unsubscribe;
}, [navigation]);
 
// In auth domain
authEvents.emit('auth:sessionExpired');

Benefits

  • Loose coupling between components
  • System-wide event handling
  • Clean separation of concerns

Pattern 3: Shared State

Overview

Shared state includes both server state via TanStack Query's caching layer and client state via Zustand stores.

When to Use

  • Persistent data from API responses (TanStack Query)
  • Global UI state (modals, themes, etc.)
  • User preferences
  • Cached data needed across features

Benefits

  • Automatic data synchronization
  • Built-in caching and invalidation
  • Persistent state across app reloads
  • Optimized rendering performance for state that needs to be accessible across features.
  • Session information

Implementation

Via Query Cache

// Sharing data through TanStack Query cache
import { useQueryClient } from '@tanstack/react-query';
import { productQueryKeys } from '@/core/domains/products/queryKeys';
 
export function useSharedProductData() {
  const queryClient = useQueryClient();
  
  // Read from cache
  const cachedProducts = queryClient.getQueryData(
    productQueryKeys.all()
  );
  
  // Invalidate to trigger refetch
  const refreshProducts = () => {
    queryClient.invalidateQueries({
      queryKey: productQueryKeys.all()
    });
  };
  
  return { cachedProducts, refreshProducts };
}

Via Global Store

// core/shared/store/appStore.ts
import { create } from 'zustand';
 
interface AppState {
  theme: 'light' | 'dark';
  isOffline: boolean;
  setTheme: (theme: 'light' | 'dark') => void;
  setOfflineStatus: (status: boolean) => void;
}
 
export const useAppStore = create<AppState>((set) => ({
  theme: 'light',
  isOffline: false,
  setTheme: (theme) => set({ theme }),
  setOfflineStatus: (status) => set({ isOffline: status }),
}));

Benefits

  • Efficient data sharing
  • Automatic synchronization
  • Reduced API calls

Pattern 4: Navigation Parameters

Overview

Passing data between screens through navigation parameters.

When to Use

  • Screen-to-screen data transfer
  • Deep linking with parameters
  • Temporary state during navigation flow

Benefits

  • Decoupled screen implementations
  • Support for deep linking
  • Type-safe data passing
  • Maintains clear navigation history

Implementation

// Navigation types
export type RootStackParamList = {
  ProductList: undefined;
  ProductDetail: { productId: string };
  CartReview: { items: CartItem[] };
};
 
// Navigating with params
navigation.navigate('ProductDetail', { 
  productId: product.id 
});
 
// Receiving params
export function ProductDetailScreen({ route }: Props) {
  const { productId } = route.params;
  const { data: product } = useProduct(productId);
  
  return <ProductDetails product={product} />;
}

Benefits

  • Type-safe parameter passing
  • Works with deep linking
  • Clear data flow

Choosing the Right Pattern

Use this decision tree to select the appropriate communication pattern:

Need to share data between features?
├─ Is it domain data?
│  └─ YES → Use Domain Services (hooks)
├─ Is it a system event?
│  └─ YES → Use Event System
├─ Is it global app state?
│  └─ YES → Use Shared State (store/cache)
└─ Is it navigation-related?
   └─ YES → Use Navigation Parameters

Anti-Patterns to Avoid

1. Direct Feature Imports

Wrong: Features importing from each other

// features/cart/components/CartItem.tsx
import { ProductCard } from '@/features/products/components'; // Don't do this!

Correct: Use shared UI components

// ui/ProductCard/ProductCard.tsx
export function ProductCard({ product }: Props) { }
 
// features/cart/components/CartItem.tsx
import { ProductCard } from '@/ui/ProductCard';

2. Prop Drilling

Wrong: Passing data through multiple levels

<App userData={userData}>
  <Screen userData={userData}>
    <Component userData={userData}>
      <DeepChild userData={userData} />
    </Component>
  </Screen>
</App>

Correct: Use appropriate state management

// Use domain hooks or global state
const { data: userData } = useCurrentUser();

3. Circular Dependencies

Wrong: Domains depending on each other

// core/domains/products/hooks.ts
import { useUser } from '@/core/domains/users/hooks'; // Avoid!
 
// core/domains/users/hooks.ts
import { useProducts } from '@/core/domains/products/hooks'; // Circular!

Correct: Compose in features

// features/product-detail/screens/ProductDetailScreen.tsx
const { data: product } = useProduct(id);
const { data: owner } = useUser(product?.ownerId);

Best Practices

1. Keep Communication Minimal

  • Only share what's necessary
  • Prefer local state when possible
  • Avoid over-engineering

2. Document Communication Points

/**
 * Emits 'product:stockUpdate' event when stock changes
 * Listens to 'order:completed' to update stock
 */
export function useProductStock() {
  // Implementation
}

3. Type Everything

// Define event types
type ProductEvents = {
  'product:created': { id: string };
  'product:updated': { id: string; changes: Partial<Product> };
  'product:deleted': { id: string };
};
 
// Define navigation params
type NavigationParams = {
  ProductDetail: { productId: string; referrer?: string };
};

4. Handle Errors Gracefully

// Event handling with error boundaries
try {
  eventEmitter.emit('user:login', { userId });
} catch (error) {
  console.error('Failed to emit login event:', error);
  // Handle error appropriately
}

Examples by Use Case

User Authentication Flow

// 1. Login action in auth domain
export function useLogin() {
  return useMutation({
    mutationFn: authApi.login,
    onSuccess: (data) => {
      // Update auth store
      authStore.setTokens(data.tokens);
      
      // Emit login event
      authEvents.emit('auth:login', {
        userId: data.user.id,
        token: data.tokens.access,
      });
      
      // Navigate to home
      navigation.reset({
        index: 0,
        routes: [{ name: 'Home' }],
      });
    },
  });
}
 
// 2. Listen in App.tsx for navigation
useEffect(() => {
  const unsubscribe = authEvents.on('auth:logout', () => {
    navigation.reset({
      index: 0,
      routes: [{ name: 'Login' }],
    });
  });
  
  return unsubscribe;
}, []);
 
// 3. Listen in features for data clearing
useEffect(() => {
  const unsubscribe = authEvents.on('auth:logout', () => {
    queryClient.clear();
  });
  
  return unsubscribe;
}, []);

Real-time Product Updates

// 1. WebSocket connection in domain
// core/domains/products/realtime.ts
export function useProductUpdates() {
  useEffect(() => {
    const ws = new WebSocket(PRODUCT_WS_URL);
    
    ws.onmessage = (event) => {
      const update = JSON.parse(event.data);
      
      // Update cache
      queryClient.setQueryData(
        productQueryKeys.detail(update.id),
        update
      );
      
      // Emit event
      productEvents.emit('product:updated', update);
    };
    
    return () => ws.close();
  }, []);
}
 
// 2. Listen in features
export function ProductListItem({ productId }: Props) {
  const [highlighted, setHighlighted] = useState(false);
  
  useEffect(() => {
    const unsubscribe = productEvents.on(
      'product:updated',
      (update) => {
        if (update.id === productId) {
          setHighlighted(true);
          setTimeout(() => setHighlighted(false), 2000);
        }
      }
    );
    
    return unsubscribe;
  }, [productId]);
}

Testing Communication

Testing Events

import { authEvents } from '@/core/domains/auth/events';
 
describe('Auth Events', () => {
  it('should emit login event', () => {
    const listener = jest.fn();
    authEvents.on('auth:login', listener);
    
    authEvents.emit('auth:login', {
      userId: '123',
      token: 'abc',
    });
    
    expect(listener).toHaveBeenCalledWith({
      userId: '123',
      token: 'abc',
    });
  });
});

Testing Navigation

import { NavigationContainer } from '@react-navigation/native';
import { render, fireEvent } from '@testing-library/react-native';
 
describe('Navigation', () => {
  it('should navigate with params', () => {
    const { getByText } = render(
      <NavigationContainer>
        <ProductListScreen />
      </NavigationContainer>
    );
    
    fireEvent.press(getByText('Product 1'));
    
    expect(mockNavigate).toHaveBeenCalledWith(
      'ProductDetail',
      { productId: '1' }
    );
  });
});

Design Principles

Core Architectural Principles

  1. Loose Coupling

    • Features should not be directly dependent on each other
    • Communication should occur through well-defined interfaces
    • Implementation details should be hidden behind abstractions
  2. Clear Boundaries

    • Feature modules must maintain strict boundaries
    • Communication across boundaries should be explicit and controlled
    • No circular dependencies between features
  3. Single Responsibility

    • Each communication pattern serves a specific purpose
    • The pattern chosen should match the type of data being shared
    • Complex flows should be decomposed into manageable parts

Trade-offs and Design Decisions

PatternBenefitsDrawbacksWhen To Choose
Domain ServicesType-safe, shared cachingCan create domain dependenciesFor domain data that needs persistence
Event SystemLoose coupling, flexibleHarder to trace, debugFor system-wide notifications
Shared StateCentralized, reactivePotential for overuseFor truly global state
NavigationSimple, directLimited to screen flowFor screen-to-screen transitions

Constraints and Considerations

  • Features must never import directly from other features
  • Communication patterns should be used consistently across the app
  • Performance implications must be considered for each pattern
  • Type safety should be maintained across all communication boundaries

Implementation Considerations

Performance Implications

  • Domain Services: Use staleTime and cacheTime to optimize refetching
  • Event System: Limit subscribers to avoid memory leaks
  • Shared State: Be selective about what goes into global state
  • Navigation: Pass IDs rather than large objects between screens

Security Considerations

  • Domain Services: Validate data at domain boundaries
  • Event System: Don't emit sensitive data in events
  • Shared State: Be cautious about storing sensitive information
  • Navigation: Sanitize parameters from deep links

Scalability Aspects

  • Domain Services: Scale well with additional domains
  • Event System: Use namespaced events for growing systems
  • Shared State: Split into domain-specific stores as app grows
  • Navigation: Use nested navigators for complex flows

Summary

Our communication patterns provide flexible options for different scenarios:

  1. Domain Services: Primary method for data access
  2. Events: System-wide notifications and reactions
  3. Shared State: Cached data and global state
  4. Navigation: Screen-to-screen data passing

Choose the simplest pattern that meets your needs and maintain clear boundaries between features.