Backend Patterns Skill
Degree of freedom: MIXED. Which pattern to apply [HIGH freedom];
existing-architecture probes and the validation list [LOW freedom — run exactly].
How to reason
- Observe — existing server/api/actions, ORM, queues, cache
- Interpret — missing pattern vs duplicate vs wrong layer
- Classify — reuse / add-queue / add-cache / add-rate-limit / edge-fn
- Severity — unauthenticated mutation outranks a missing cache
Worked example
Observe:
createOrdersends email inline; no Inngest/queue; checkout p95 4s. Interpret: confirmation is post-response work, not request-path. Classify:after()or anorder/createdjob; keep the write transactional. Verify: order row commits; email/inventory run after response; retries do not double-charge.
Self-critique before reporting
- Existing first — searched server/api/actions before adding a second pattern
- Auth + validate — every mutation checks session and Zod
- Idempotent — retries on the new path do not double-apply
- Right owner — which architecture to pick →
audit-backend-architecture
Design scalable, maintainable backend architectures using modern patterns and best practices.
Code examples lean on Next.js App Router + Supabase/Prisma. The patterns are stack-agnostic — adapt ORMs, client libraries, and deploy targets to your detected ecosystem.
Check existing first [LOW freedom — run exactly]
Before implementing ANY backend pattern, verify:
- Check existing architecture:
ls -la src/server/ src/api/ app/api/ supabase/functions/ 2>/dev/null
cat package.json | grep -i "prisma\|drizzle\|supabase\|trpc"
- Check existing patterns:
rg "createTRPCRouter|publicProcedure" --type ts -l
rg "'use server'" --type ts -l
ls -la supabase/migrations/*.sql 2>/dev/null | tail -5
- Check database setup:
cat prisma/schema.prisma 2>/dev/null | head -50
cat supabase/config.toml 2>/dev/null
Why: Backend changes have wide impact. Understand existing architecture first.
Server Actions (Next.js 16+) [HIGH freedom]
Next.js 16: Turbopack default; 'use cache' + cacheComponents; reactCompiler: true; middleware.ts → proxy.ts (grep both). Instant navigations → enhance-web-instant-nav.
Basic Pattern
// app/actions/users.ts
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
})
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string; fieldErrors?: Record<string, string[]> }
export async function createUser(
prevState: ActionResult<User> | null,
formData: FormData
): Promise<ActionResult<User>> {
// 1. Auth check
const session = await auth()
if (!session?.user) {
return { success: false, error: 'Unauthorized' }
}
// 2. Validate input
const result = CreateUserSchema.safeParse({
email: formData.get('email'),
name: formData.get('name'),
})
if (!result.success) {
return {
success: false,
error: 'Invalid input',
fieldErrors: result.error.flatten().fieldErrors,
}
}
// 3. Execute
try {
const user = await db.user.create({
data: result.data,
})
revalidatePath('/users')
return { success: true, data: user }
} catch (error) {
if (isPrismaError(error, 'P2002')) {
return { success: false, error: 'Email already exists' }
}
console.error('createUser error:', error)
return { success: false, error: 'Failed to create user' }
}
}
With Background Tasks
'use server'
import { after } from 'next/server'
export async function createOrder(formData: FormData) {
const order = await db.order.create({ data: { ... } })
// Run after response sent (Next.js 16)
after(async () => {
await sendOrderConfirmation(order.id)
await updateInventory(order.items)
await notifyWarehouse(order.id)
})
revalidatePath('/orders')
return { success: true, data: order }
}
tRPC Setup [HIGH freedom]
Router Definition
// server/api/routers/users.ts
import { z } from 'zod'
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'
export const usersRouter = createTRPCRouter({
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
return ctx.db.user.findUnique({
where: { id: input.id },
})
}),
create: protectedProcedure
.input(z.object({
email: z.string().email(),
name: z.string().min(1),
}))
.mutation(async ({ ctx, input }) => {
return ctx.db.user.create({
data: {
...input,
createdById: ctx.session.user.id,
},
})
}),
list: protectedProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(),
}))
.query(async ({ ctx, input }) => {
const items = await ctx.db.user.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
orderBy: { createdAt: 'desc' },
})
let nextCursor: string | undefined
if (items.length > input.limit) {
const nextItem = items.pop()
nextCursor = nextItem?.id
}
return { items, nextCursor }
}),
})
Supabase Edge Functions [HIGH freedom]
Basic Function
// supabase/functions/process-webhook/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
Deno.serve(async (req) => {
// Handle CORS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
// Verify webhook signature
const signature = req.headers.get('x-webhook-signature')
if (!verifySignature(signature, await req.text())) {
return new Response('Invalid signature', { status: 401 })
}
const payload = await req.json()
// Create admin client (bypasses RLS)
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Process webhook
await supabase.from('events').insert({
type: payload.type,
data: payload.data,
})
return new Response(
JSON.stringify({ success: true }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
} catch (error) {
console.error('Webhook error:', error)
return new Response(
JSON.stringify({ error: 'Internal error' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
})
Database Patterns [HIGH freedom]
Optimistic Locking
-- Add version column
ALTER TABLE orders ADD COLUMN version INT DEFAULT 1;
-- Update with version check
UPDATE orders
SET
status = 'shipped',
version = version + 1
WHERE id = $1 AND version = $2;
-- Returns 0 rows if version mismatch (concurrent update)
Soft Deletes
model Post {
id String @id @default(cuid())
title String
deletedAt DateTime?
@@index([deletedAt])
}
// Query active records
const posts = await db.post.findMany({
where: { deletedAt: null }
})
// Soft delete
await db.post.update({
where: { id },
data: { deletedAt: new Date() }
})
Audit Logging
-- Audit table
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
table_name TEXT NOT NULL,
record_id UUID NOT NULL,
action TEXT NOT NULL, -- INSERT, UPDATE, DELETE
old_data JSONB,
new_data JSONB,
user_id UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
-- Trigger function
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_logs (table_name, record_id, action, old_data, new_data, user_id)
VALUES (
TG_TABLE_NAME,
COALESCE(NEW.id, OLD.id),
TG_OP,
CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN row_to_json(OLD) END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW) END,
auth.uid()
);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Apply to table
CREATE TRIGGER orders_audit
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION audit_trigger();
Caching Patterns [HIGH freedom]
Next.js Cache
// Cached fetch
const data = await fetch('https://api.example.com/data', {
next: {
revalidate: 3600, // 1 hour
tags: ['data']
}
})
// Revalidate on demand
import { revalidateTag } from 'next/cache'
revalidateTag('data')
// unstable_cache for database queries
import { unstable_cache } from 'next/cache'
const getCachedUser = unstable_cache(
async (id: string) => db.user.findUnique({ where: { id } }),
['user'],
{ revalidate: 3600, tags: ['users'] }
)
Redis Caching
import { Redis } from '@upstash/redis'
const redis = Redis.fromEnv()
async function getCachedData<T>(
key: string,
fetcher: () => Promise<T>,
ttl = 3600
): Promise<T> {
// Try cache
const cached = await redis.get<T>(key)
if (cached) return cached
// Fetch and cache
const data = await fetcher()
await redis.set(key, data, { ex: ttl })
return data
}
// Usage
const user = await getCachedData(
`user:${id}`,
() => db.user.findUnique({ where: { id } }),
600 // 10 minutes
)
Background Jobs [HIGH freedom]
Inngest
// inngest/functions.ts
import { inngest } from './client'
export const processOrder = inngest.createFunction(
{ id: 'process-order' },
{ event: 'order/created' },
async ({ event, step }) => {
// Step 1: Validate inventory
const inventory = await step.run('check-inventory', async () => {
return await checkInventory(event.data.items)
})
if (!inventory.available) {
await step.run('notify-out-of-stock', async () => {
await notifyCustomer(event.data.userId, 'out-of-stock')
})
return { status: 'cancelled' }
}
// Step 2: Charge payment
const payment = await step.run('charge-payment', async () => {
return await chargeCustomer(event.data.paymentMethod)
})
// Step 3: Send confirmation
await step.run('send-confirmation', async () => {
await sendOrderConfirmation(event.data.orderId)
})
return { status: 'completed', paymentId: payment.id }
}
)
// Trigger from server action
await inngest.send({
name: 'order/created',
data: { orderId, userId, items, paymentMethod }
})
Trigger.dev
// trigger/jobs.ts
import { client } from './client'
export const syncJob = client.defineJob({
id: 'sync-data',
name: 'Sync External Data',
version: '1.0.0',
trigger: intervalTrigger({ seconds: 3600 }), // Every hour
run: async (payload, io, ctx) => {
const data = await io.runTask('fetch-external', async () => {
return await fetchExternalAPI()
})
await io.runTask('update-database', async () => {
await db.externalData.upsert({
where: { externalId: data.id },
create: data,
update: data,
})
})
return { synced: data.length }
},
})
Rate Limiting [HIGH freedom]
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
analytics: true,
})
export async function rateLimitedAction(userId: string) {
const { success, limit, remaining, reset } = await ratelimit.limit(userId)
if (!success) {
return {
success: false,
error: 'Too many requests',
retryAfter: Math.ceil((reset - Date.now()) / 1000),
}
}
// Proceed with action...
}
Architecture patterns (distributed systems) [HIGH freedom]
Gateway, BFF, bulkhead, circuit breaker, outbox+CDC, saga, hexagonal, ACL, and strangler-fig →
references/architecture-patterns.md. Pick the pattern for the
topology (no mesh on a monolith; no CQRS unless reads/writes diverge). Timeouts/retries/idempotency
→ audit-resilience; structural gap report → audit-backend-architecture.
Validation [LOW freedom — do not skip]
After implementing backend patterns:
- Error handling → All errors caught, logged, safe response returned
- Auth checks → Every mutation verifies authentication
- Input validation → Zod schema on all inputs
- Rate limiting → Sensitive endpoints protected
- Idempotency → Critical operations handle retries
- Logging → Structured logs without sensitive data
- Testing → Unit tests for business logic, integration for APIs