Viscosity & Velocity: Engineering Architectural-Grade Web Systems
Most products launched today look polished on the surface and collapse under a 10x traffic spike. We build the ones that don't.
At Brewstack Labs, Architectural-Grade is not a tagline — it is a build constraint. Every system we ship is designed to handle the traffic it has never seen yet.
The Viscosity Problem
In fluid dynamics, viscosity is resistance to flow. In software, technical viscosity is the accumulating friction that makes every new feature harder to ship than the last.
It manifests predictably:
- A database query that worked at 1,000 users becomes a full-table scan at 100,000.
- A tightly-coupled service means a single deploy takes down three unrelated features.
- No observability means the first sign of failure is a user tweet, not an alert.
The goal is not zero viscosity — some rigidity is structural. The goal is controlled viscosity: friction only where it protects correctness.
Stack Decisions That Compound
Every architectural decision has a compounding effect over 18 months. Here is how we make them.
Next.js App Router as the Rendering Boundary
We treat the server/client boundary as the primary architectural seam. Server Components handle data fetching and access control. Client Components own interaction state. Nothing leaks across the boundary without deliberate intent.
// Server Component — runs on the server, never ships JS to the client
// Database access, auth checks, and data shaping live here
async function ProjectList({ orgId }: { orgId: string }) {
const projects = await db.project.findMany({
where: { orgId, archived: false },
orderBy: { updatedAt: 'desc' },
select: { id: true, name: true, status: true, updatedAt: true },
});
return <ProjectGrid projects={projects} />;
}'use client';
// Client Component — owns interaction only
// Receives already-shaped data, never fetches directly
function ProjectGrid({ projects }: { projects: Project[] }) {
const [filter, setFilter] = useState<Status>('all');
const visible = filter === 'all' ? projects : projects.filter(p => p.status === filter);
return (
<div className="grid grid-cols-3 gap-4">
<FilterBar active={filter} onChange={setFilter} />
{visible.map(p => <ProjectCard key={p.id} project={p} />)}
</div>
);
}The discipline: no useEffect data fetching. If a Client Component needs server data, it receives it as props from a parent Server Component.
TypeScript as a Contract Layer
TypeScript strict mode is non-negotiable. It is not for IDE autocomplete — it is a machine-checkable contract between every layer of the system.
// Zod schema as the single source of truth
// Validates at the network boundary, infers TypeScript types automatically
import { z } from 'zod';
const CreateProjectSchema = z.object({
name: z.string().min(1).max(80),
orgId: z.string().cuid(),
template: z.enum(['blank', 'saas', 'ecommerce']).default('blank'),
});
type CreateProjectInput = z.infer<typeof CreateProjectSchema>;
// Server Action — validated at the boundary, typed throughout
async function createProject(raw: unknown) {
const input = CreateProjectSchema.parse(raw); // throws ZodError on invalid input
return db.project.create({ data: input });
}When the schema changes, every callsite that disagrees becomes a compile error. No runtime surprises.
Concurrency Without Complexity
High-concurrency does not require microservices. It requires knowing where your bottlenecks are.
Connection Pooling at the Edge
Serverless functions are ephemeral. Each cold start opens a new database connection. At scale, this exhausts the connection limit before the database has done any real work.
// Singleton pattern — one PrismaClient instance per process
// Prevents connection pool exhaustion in serverless environments
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'warn', 'error'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;For production workloads, we layer PgBouncer or Neon's connection pooler in front of Postgres. The application layer never manages raw connections.
Parallel Data Fetching
Sequential awaits are a common performance tax. Independent queries should never wait for each other.
// Sequential — 340ms total (120ms + 220ms)
const user = await db.user.findUnique({ where: { id } });
const projects = await db.project.findMany({ where: { orgId: user.orgId } });
// Parallel — 220ms total (the slower query sets the ceiling)
const [user, projects] = await Promise.all([
db.user.findUnique({ where: { id } }),
db.project.findMany({ where: { orgId } }),
]);This pattern consistently cuts data-layer latency by 30–60% on dashboards with multiple independent data sources.
The Scalability Checklist
Before any system goes to production, it passes a scalability audit across three dimensions.
1. Query Complexity
Every database query is reviewed for index coverage. An unindexed filter on a 1M-row table is a latency time bomb.
-- Bad: full table scan on every request
SELECT * FROM events WHERE metadata->>'source' = 'web';
-- Good: partial index on the specific value
CREATE INDEX CONCURRENTLY idx_events_web_source
ON events ((metadata->>'source'))
WHERE metadata->>'source' = 'web';We run EXPLAIN ANALYZE on every query that touches tables above 10k rows. Seq Scans in production are treated as bugs.
2. Bundle Size
JavaScript is the most expensive asset type — it blocks parsing, blocks rendering, and blocks interaction. We enforce a 200kb (gzipped) budget on the main bundle.
// next.config.js — bundle analyzer runs on every build in CI
import withBundleAnalyzer from '@next/bundle-analyzer';
export default withBundleAnalyzer({ enabled: process.env.ANALYZE === 'true' })({
experimental: { serverComponentsExternalPackages: ['@prisma/client'] },
});Heavy libraries (date-fns, lodash, chart libraries) are lazy-loaded at the route level, never in the root layout.
3. Cache Strategy
Caching is a correctness problem, not just a performance problem. Stale data served from cache can be worse than slow data fetched fresh.
// Next.js 15 fetch cache — explicit, not implicit
// 'force-cache' for data that changes rarely (product catalog, config)
const config = await fetch('/api/config', { cache: 'force-cache' });
// 'no-store' for user-specific or real-time data
const notifications = await fetch('/api/notifications', { cache: 'no-store' });
// Revalidate on a schedule for data with a known TTL
const posts = await fetch('/api/posts', { next: { revalidate: 300 } }); // 5 minObservability is Not Optional
If a service degrades at 3am, you should know before your users do.
Structured Logging
Plain console.log strings are unsearchable at scale. Every log entry is a structured JSON object.
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
base: { service: 'api', env: process.env.NODE_ENV },
});
// Usage — every log includes context for filtering
logger.info({ userId, action: 'project.create', projectId }, 'Project created');
logger.error({ userId, error: err.message, code: err.code }, 'Database write failed');Error Boundaries with Context
Client-side errors are caught at the component level with enough context to reproduce.
'use client';
export default function ErrorBoundary({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Ship to your error tracking service (Sentry, etc.)
captureException(error, { tags: { digest: error.digest } });
}, [error]);
return (
<div className="flex flex-col items-center gap-4 p-8">
<p className="font-mono text-sm text-red-400">Something went wrong.</p>
<code className="text-xs text-neutral-500">{error.digest}</code>
<button onClick={reset}>Try again</button>
</div>
);
}error.digest is a server-side hash that links the client error to a specific server log entry — you can find the root cause without exposing implementation details to the browser.
Closing Principle
Architecture is what remains when you remove everything that was convenient at the time.
We do not prototype our way to production and call it shipping. We design for the 10x scale upfront, keep the surface area small, and let the system grow without rewrites.
The foundation is what makes velocity possible. Not the other way around.