From coding-agent
Patterns for apps with external clients — singleton clients, factories, connection pools, retry/timeout wrappers, graceful shutdown, service-layer organization. Apply when an app talks to DBs/APIs/caches/queues.
npx claudepluginhub devjarus/coding-agentThis skill uses the workspace's default tool permissions.
How to structure the code between your business logic and external systems (databases, APIs, caches, queues, LLM providers). Covers lifecycle management, client creation, error boundaries, and the patterns that prevent "it works on my machine" bugs.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
How to structure the code between your business logic and external systems (databases, APIs, caches, queues, LLM providers). Covers lifecycle management, client creation, error boundaries, and the patterns that prevent "it works on my machine" bugs.
Use for clients that are expensive to create and safe to share (DB pools, HTTP clients with connection reuse, SDK clients).
// clients.ts — factory creates singleton instances
export function createClients(config: AppConfig) {
const db = new Pool({
connectionString: config.database.url,
max: config.database.poolSize ?? 20,
idleTimeoutMillis: 30_000,
});
const redis = new Redis(config.redis.url, {
maxRetriesPerRequest: 3,
retryStrategy: (times) => Math.min(times * 200, 2000),
});
const stripe = new Stripe(config.stripe.secretKey, {
apiVersion: "2024-12-18",
timeout: 10_000,
maxNetworkRetries: 2,
});
const s3 = new S3Client({
region: config.aws.region,
credentials: config.aws.credentials,
});
return { db, redis, stripe, s3 };
}
// composition-root.ts
const clients = createClients(config);
const userService = new UserService(clients.db, clients.redis, logger);
const paymentService = new PaymentService(clients.stripe, clients.db, logger);
Rules for singletons:
Use for clients that carry request-specific state or can't be shared (database transactions, authenticated API clients with per-user tokens).
// Transaction per request
app.use(async (req, res, next) => {
const tx = await db.beginTransaction();
req.tx = tx;
try {
await next();
await tx.commit();
} catch (err) {
await tx.rollback();
throw err;
}
});
// Per-user API client
function createUserGitHubClient(accessToken: string) {
return new Octokit({ auth: accessToken });
}
Use when the client might not be needed in every code path, or initialization is slow.
class LazyClient<T> {
private instance: T | null = null;
constructor(private factory: () => T) {}
get(): T {
if (!this.instance) {
this.instance = this.factory();
}
return this.instance;
}
async shutdown(): Promise<void> {
if (this.instance && 'close' in this.instance) {
await (this.instance as any).close();
}
}
}
// Usage
const searchClient = new LazyClient(() => new MeiliSearch({ host: config.search.url }));
// Only creates MeiliSearch client when first accessed
Every database and HTTP client should use connection pooling. Creating a new connection per request is slow and can exhaust server resources.
// Database — pool is the singleton, connections are per-query
const pool = new Pool({ max: 20 }); // 20 connections shared
const result = await pool.query(sql); // borrows a connection, returns it after
// HTTP — reuse agent/client
const httpClient = new undici.Pool(baseUrl, {
connections: 10,
pipelining: 1,
keepAliveTimeout: 30_000,
});
# Python — httpx with connection pooling
client = httpx.AsyncClient(
base_url="https://api.stripe.com",
timeout=10.0,
limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
)
# Use client across requests — don't create per request
// Go — http.Client is safe to share, reuses connections by default
var httpClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
},
}
External calls fail. Wrap them with retries and timeouts.
// Generic retry wrapper
async function withRetry<T>(
fn: () => Promise<T>,
opts: { maxRetries?: number; baseDelayMs?: number; retryOn?: (err: any) => boolean } = {}
): Promise<T> {
const { maxRetries = 3, baseDelayMs = 500, retryOn = () => true } = opts;
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err as Error;
if (attempt === maxRetries || !retryOn(err)) throw lastError;
const delay = baseDelayMs * Math.pow(2, attempt); // exponential backoff
await new Promise(r => setTimeout(r, delay));
}
}
throw lastError!;
}
// Usage — retry on rate limits and server errors
const result = await withRetry(
() => stripe.charges.create({ amount: 1000, currency: "usd" }),
{
maxRetries: 3,
retryOn: (err) => err.statusCode === 429 || err.statusCode >= 500,
}
);
Timeout pattern:
// AbortController for fetch timeouts
async function fetchWithTimeout(url: string, timeoutMs: number = 10_000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeout);
}
}
Rules:
Every singleton client must be shut down when the process exits. Leaked connections exhaust pool limits.
// shutdown.ts
type ShutdownFn = () => Promise<void>;
const shutdownFns: ShutdownFn[] = [];
export function onShutdown(fn: ShutdownFn) {
shutdownFns.push(fn);
}
async function shutdown(signal: string) {
logger.info({ signal }, "Shutting down");
for (const fn of shutdownFns.reverse()) { // reverse order: last created, first closed
try { await fn(); } catch (err) { logger.error({ err }, "Shutdown error"); }
}
process.exit(0);
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
// Register in composition root
onShutdown(() => clients.db.end());
onShutdown(() => clients.redis.quit());
onShutdown(() => server.close());
Rules:
src/
├── config.ts # Reads env vars, exports typed config
├── clients.ts # Creates singleton external clients
├── composition-root.ts # Wires everything together
│
├── services/ # Business logic — depends on repositories, NOT on clients directly
│ ├── user-service.ts # Accepts UserRepository interface
│ └── payment-service.ts # Accepts PaymentGateway interface
│
├── repositories/ # Data access — wraps database clients
│ ├── user-repo.ts # Accepts Pool, implements UserRepository
│ └── order-repo.ts
│
├── gateways/ # External API wrappers — wraps SDK clients
│ ├── stripe-gateway.ts # Accepts Stripe client, implements PaymentGateway
│ ├── email-gateway.ts # Accepts SendGrid client
│ └── search-gateway.ts # Accepts MeiliSearch client
│
├── middleware/ # HTTP concerns (auth, logging, error handling)
└── routes/ # HTTP handlers — thin, delegate to services
Layering rules:
new Pool() inside a handler.