AgentSkillsCN

service-layer-caching-patterns

为编排、缓存与错误处理提供服务层模式。

SKILL.md
--- frontmatter
name: service-layer-caching-patterns
description: Service layer patterns for orchestration, caching, and error handling.
argument-hint: Ask for service-layer placement or caching guidance.

Service Layer and Caching Patterns

Use this skill when structuring business logic, implementing caching strategies, or orchestrating multiple data sources.


Table of Contents

  1. Service Layer Fundamentals
  2. Caching Strategies
  3. Error Handling Patterns
  4. Integration Patterns
  5. Anti-Patterns to Avoid

Service Layer Fundamentals

What is the Service Layer?

The service layer sits between your UI components and external integrations (API clients, databases). It provides:

  • Business logic orchestration - Combine multiple data sources
  • Caching - Reduce API calls and improve performance
  • Error handling - Graceful degradation when external services fail
  • Data transformation - Convert external data to application types

Architecture:

code
Components (app/, components/)
        ↓
Service Layer (services/)
        ↓
Integration Layer (lib/)
        ↓
External APIs / Databases

File Organization

code
services/
├── index.ts              # Re-export all services
├── user-service.ts       # User-related business logic
├── search-service.ts     # Search orchestration
└── analytics-service.ts  # Analytics aggregation

lib/
├── api.ts                # Raw API client
├── database.ts           # Database client
└── cache.ts              # Cache client (Redis, etc.)

Caching Strategies

1. Time-Based Cache (TTL)

Use when: Data changes infrequently (user profiles, static content)

Pattern:

typescript
// services/user-service.ts
import { User } from "@/types";

const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
const userCache = new Map<string, { data: User; timestamp: number }>();

export async function getUserProfile(userId: string): Promise<User | null> {
  // Check cache
  const cached = userCache.get(userId);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.data; // ✅ Cache hit
  }

  // Fetch from API
  const user = await fetchUser(userId);
  if (!user) return null;

  // Store in cache
  userCache.set(userId, {
    data: user,
    timestamp: Date.now(),
  });

  return user;
}

// Invalidate cache when user updates
export async function updateUser(
  userId: string,
  updates: Partial<User>,
): Promise<User | null> {
  const updated = await apiUpdateUser(userId, updates);

  // Invalidate cache
  userCache.delete(userId);

  return updated;
}

Pros:

  • ✅ Simple implementation
  • ✅ Automatic expiration
  • ✅ Reduces API load

Cons:

  • ❌ Stale data possible (up to TTL duration)
  • ❌ Memory grows with unique keys

2. Map-Based Deduplication

Use when: Fetching lists with duplicate IDs (search results, feeds)

Pattern:

typescript
// services/search-service.ts
import { SearchResult } from "@/types";

export async function searchItems(query: string): Promise<SearchResult[]> {
  const results = await apiSearch(query);

  // Deduplicate by ID (O(1) vs O(n²) with array)
  const uniqueMap = new Map<string, SearchResult>();

  for (const item of results) {
    uniqueMap.set(item.id, item);
  }

  // Return as array
  return Array.from(uniqueMap.values());
}

Why Map over Set + Array?

typescript
// ❌ Slower: O(n) for each add
const seen = new Set<string>();
const unique = results.filter((item) => {
  if (seen.has(item.id)) return false;
  seen.add(item.id);
  return true;
});

// ✅ Faster: O(1) for each add
const uniqueMap = new Map<string, SearchResult>();
results.forEach((item) => uniqueMap.set(item.id, item));
const unique = Array.from(uniqueMap.values());

3. React Server Component Caching

Use when: Using Next.js App Router with Server Components

Pattern:

typescript
// services/data-service.ts
import { cache } from 'react';
import { Item } from '@/types';

// React caches this per-request automatically
export const getItems = cache(async (): Promise<Item[]> => {
  const response = await fetch('https://api.example.com/items');
  if (!response.ok) return [];
  return response.json();
});

// app/dashboard/page.tsx
import { getItems } from '@/services/data-service';

export default async function DashboardPage() {
  // Automatically cached within this request
  const items = await getItems();

  return <Dashboard items={items} />;
}

// app/analytics/page.tsx
import { getItems } from '@/services/data-service';

export default async function AnalyticsPage() {
  // Same request = cache hit (no extra API call)
  const items = await getItems();

  return <Analytics items={items} />;
}

Pros:

  • ✅ Automatic deduplication per request
  • ✅ Zero configuration
  • ✅ Works across multiple Server Components

Cons:

  • ❌ Only works in Server Components
  • ❌ Cache doesn't persist across requests

4. Stale-While-Revalidate

Use when: Need instant responses but want fresh data eventually

Pattern:

typescript
// services/feed-service.ts
const feedCache = new Map<string, { data: Post[]; timestamp: number }>();
const CACHE_TTL = 5 * 60 * 1000;
const STALE_TTL = 60 * 60 * 1000; // 1 hour

export async function getFeed(userId: string): Promise<Post[]> {
  const cached = feedCache.get(userId);
  const now = Date.now();

  // Fresh cache (< 5 min) - return immediately
  if (cached && now - cached.timestamp < CACHE_TTL) {
    return cached.data;
  }

  // Stale cache (5 min - 1 hour) - return stale, revalidate in background
  if (cached && now - cached.timestamp < STALE_TTL) {
    // Return stale data immediately
    const staleData = cached.data;

    // Revalidate in background (don't await)
    revalidateFeed(userId);

    return staleData;
  }

  // No cache or too stale - fetch fresh
  return await fetchFreshFeed(userId);
}

async function revalidateFeed(userId: string): Promise<void> {
  const fresh = await fetchFreshFeed(userId);
  feedCache.set(userId, {
    data: fresh,
    timestamp: Date.now(),
  });
}

async function fetchFreshFeed(userId: string): Promise<Post[]> {
  const posts = await apiFetchFeed(userId);
  feedCache.set(userId, {
    data: posts,
    timestamp: Date.now(),
  });
  return posts;
}

Pros:

  • ✅ Fast response (always returns immediately)
  • ✅ Eventually consistent
  • ✅ Good UX (no loading spinners)

Cons:

  • ❌ More complex logic
  • ❌ Users may see stale data briefly

Error Handling Patterns

1. Return Null (Simple Cases)

Use when: Failure is not critical, UI can handle missing data

typescript
export async function getUser(id: string): Promise<User | null> {
  try {
    const response = await fetch(`/api/user/${id}`);
    if (!response.ok) return null;
    return response.json();
  } catch (error) {
    console.error('getUser failed:', error);
    return null;
  }
}

// Usage in component
const user = await getUser(userId);
if (!user) {
  return <div>User not found</div>;
}

2. Result Type (Explicit Error Handling)

Use when: Caller needs to differentiate error types

typescript
type Result<T, E = string> =
  | { status: "success"; data: T }
  | { status: "error"; error: E };

export async function updateProfile(
  userId: string,
  updates: Partial<User>,
): Promise<Result<User>> {
  try {
    const response = await fetch(`/api/user/${userId}`, {
      method: "PATCH",
      body: JSON.stringify(updates),
    });

    if (!response.ok) {
      return {
        status: "error",
        error: `HTTP ${response.status}: ${response.statusText}`,
      };
    }

    const user = await response.json();
    return { status: "success", data: user };
  } catch (error) {
    return {
      status: "error",
      error: error instanceof Error ? error.message : "Unknown error",
    };
  }
}

// Usage with explicit error handling
const result = await updateProfile(userId, { name: "New Name" });

if (result.status === "error") {
  showToast(result.error);
  return;
}

// TypeScript knows result.data exists here
setUser(result.data);

Integration Patterns

Parallel Data Fetching

Use when: Multiple independent data sources

typescript
export async function getDashboardData(userId: string) {
  // ✅ Parallel (faster)
  const [user, posts, analytics] = await Promise.all([
    getUserProfile(userId),
    getUserPosts(userId),
    getUserAnalytics(userId),
  ]);

  // ❌ Sequential (slower)
  // const user = await getUserProfile(userId);
  // const posts = await getUserPosts(userId);
  // const analytics = await getUserAnalytics(userId);

  return { user, posts, analytics };
}

Pagination with Cursor

typescript
export async function getPostsPaginated(
  cursor?: string,
  limit: number = 20,
): Promise<{ posts: Post[]; nextCursor: string | null }> {
  const response = await fetch(
    `/api/posts?limit=${limit}${cursor ? `&cursor=${cursor}` : ""}`,
  );

  if (!response.ok) {
    return { posts: [], nextCursor: null };
  }

  const data = await response.json();
  return {
    posts: data.posts,
    nextCursor: data.nextCursor,
  };
}

Anti-Patterns to Avoid

❌ Calling APIs Directly from Components

typescript
// ❌ BAD: Direct API call in component
export default async function UserProfile({ userId }) {
  const response = await fetch(`https://api.example.com/user/${userId}`);
  const user = await response.json();

  return <div>{user.name}</div>;
}

// ✅ GOOD: Use service layer
import { getUserProfile } from '@/services/user-service';

export default async function UserProfile({ userId }) {
  const user = await getUserProfile(userId);

  if (!user) return <div>User not found</div>;

  return <div>{user.name}</div>;
}

Why?

  • No caching (repeated API calls)
  • No error handling
  • Hard to test
  • Duplicated logic

❌ Caching Everything

typescript
// ❌ BAD: Caching real-time data
const notificationCountCache = new Map<string, number>();

export async function getNotificationCount(userId: string): Promise<number> {
  const cached = notificationCountCache.get(userId);
  if (cached) return cached; // ❌ Stale count!

  const count = await fetchNotificationCount(userId);
  notificationCountCache.set(userId, count);
  return count;
}

// ✅ GOOD: Don't cache real-time data
export async function getNotificationCount(userId: string): Promise<number> {
  return await fetchNotificationCount(userId); // Always fresh
}

When NOT to cache:

  • Real-time data (notifications, live scores, chat messages)
  • Frequently changing data (stock prices, inventory)
  • User-specific sensitive data (passwords, tokens)

❌ Ignoring Cache Invalidation

typescript
// ❌ BAD: Never invalidate cache
const userCache = new Map<string, User>();

export async function getUser(id: string) {
  if (userCache.has(id)) return userCache.get(id);
  const user = await fetchUser(id);
  userCache.set(id, user);
  return user;
}

export async function updateUser(id: string, updates: Partial<User>) {
  // ❌ Cache becomes stale!
  return await apiUpdateUser(id, updates);
}

// ✅ GOOD: Invalidate on updates
export async function updateUser(id: string, updates: Partial<User>) {
  const updated = await apiUpdateUser(id, updates);
  userCache.delete(id); // ✅ Invalidate
  return updated;
}

Best Practices Summary

Do's ✅

  1. Use service layer for business logic (not in components)
  2. Cache infrequently changing data (profiles, static content)
  3. Invalidate cache on updates (delete, modify operations)
  4. Use Map for deduplication (O(1) vs O(n))
  5. Handle errors gracefully (return null or Result types)
  6. Parallelize independent requests (Promise.all)
  7. Use React's cache() in Server Components (automatic deduplication)

Don'ts ❌

  1. Don't call APIs directly from components
  2. Don't cache real-time data
  3. Don't forget cache invalidation
  4. Don't use Set + Array for deduplication (use Map)
  5. Don't ignore TypeScript types (use strict types)
  6. Don't cache sensitive data (tokens, passwords)

References


Skill Status: Enhanced Last Updated: 2026-02-13 (Phase 5)