Skip to content
Parvat
Philosophy

Engineering Principles

Not abstract values — the concrete defaults I reach for on every project, and why.

Code Quality

Readability is a correctness property, not a style preference.

Clever code is a liability the moment its author isn't the one reading it. I optimize for the engineer six months from now with no context — which usually means fewer abstractions, more explicit naming, and functions that do one thing at a call site you can read top to bottom. TypeScript's strict mode isn't there to catch typos; it's there to make illegal states unrepresentable, so entire categories of runtime bugs simply can't compile.

In practice

  • `strict: true` with no `any` escape hatches merged without an explicit justification comment
  • Discriminated unions over optional fields when a value's shape depends on its state
  • Functions named for what they return, not how they compute it
  • Code review comments framed as questions about intent, not style nitpicks a linter should catch

Clean Architecture

Organize by feature and dependency direction, not by technical layer.

A folder structure of controllers/, services/, models/ tells you nothing about what the product does and encourages every feature to reach into every layer. I organize by feature boundary instead — each feature owns its components, hooks, and data access — with a strict rule that shared primitives never import from a feature, only the reverse. That one-way dependency rule is what actually keeps a codebase untangleable as it grows, far more than any folder-naming convention.

In practice

  • Feature modules own their own state, components, and API calls end to end
  • Shared `lib/` and `components/ui/` never import from `features/`
  • New features are additive folders, not new branches inside existing shared files
  • A dependency-cruiser or similar rule enforced in CI, not just a convention in a README

Performance

Performance budgets belong in CI, not in a quarterly audit.

Performance regressions are cheapest to fix in the same PR that introduced them and most expensive to fix six months later when three other features depend on the slow path. I treat Core Web Vitals and API latency budgets as build-breaking checks, not dashboards someone checks occasionally. The goal is that a regression is a failed CI check with a clear diff, not a support ticket three weeks later.

In practice

  • Lighthouse CI budgets that fail the build on LCP/CLS regression, not just report them
  • Server Components by default for data-heavy views, client components only where interactivity requires it
  • Database queries reviewed for N+1 patterns at PR time, not discovered under load
  • Bundle size tracked per route, with new dependencies requiring a stated justification

Accessibility

Accessible by default beats accessible by audit.

Retrofitting accessibility after a feature ships means relearning every interaction pattern under time pressure. I default to semantic HTML and native interactive elements first, reach for ARIA only when no native element expresses the semantics I need, and treat keyboard navigation as a first-class interaction mode I test myself — not something QA checks with a screen reader once before launch.

In practice

  • Native `<button>`, `<a>`, and form elements before any custom interactive component
  • Every custom component built on Radix or equivalent primitives, never raw `div onClick`
  • Color paired with icon or text for any status/state — never color as the only signal
  • `prefers-reduced-motion` respected globally, not per-component as an afterthought

Scalability

Most scaling problems are team and ownership problems wearing an infrastructure costume.

Before reaching for microservices or horizontal sharding, I ask what's actually failing to scale: is it request throughput, or is it that three teams can't deploy without coordinating? Splitting a system before that distinction is clear usually just moves the coordination problem into network calls and adds operational overhead. I scale the parts that are provably the bottleneck, and keep everything else boring.

In practice

  • Modular monolith as the default; service extraction only when a module has genuinely divergent scaling or ownership needs
  • Horizontal scaling behind a load balancer before reaching for sharding or multi-region complexity
  • Caching added at the layer closest to the actual bottleneck, measured, not guessed
  • Ownership boundaries drawn around team structure, since Conway's Law wins eventually anyway

Reusable Components

Abstract on the third repetition, not the first.

The fastest way to a brittle component library is abstracting after one use case, guessing at what 'configurable' means before a second real requirement exists. I let real duplication accumulate to two, sometimes three instances before extracting a shared component — by then the actual variance is visible, and the abstraction fits reality instead of a guess. Components are designed around capabilities ('supports async validation') rather than specific call sites, which is what makes them survive contact with the fourth use case.

In practice

  • Rule of three: duplication is fine until a third real use case appears
  • Component APIs expressed as capabilities/variants, not booleans named after a single caller
  • Composition (slots, `asChild`, render props) preferred over a growing prop-configuration surface
  • A component's test suite is the actual documentation of what variants are supported

Design Systems

Tokens are a contract between design and code, not a color palette.

A design system that only ships color and spacing values misses the point — the parts that actually keep a product feeling coherent as it grows are the ones nobody notices: consistent focus rings, consistent easing curves, consistent elevation logic for what's 'above' what. I treat motion tokens and interaction states as first-class citizens of the system, defined once and referenced everywhere, so a product doesn't slowly accrete twelve slightly different button hover animations.

In practice

  • Color, spacing, radius, elevation, and motion tokens defined once as CSS custom properties, referenced everywhere
  • Every interactive state (hover, focus-visible, disabled, loading) specified at the token level, not per-component
  • Dark mode as a first-class token override, not a separate parallel stylesheet
  • New components reviewed against the token set before a one-off value is approved

API Architecture

Let contract violations fail at compile time, not in production logs.

The right API shape depends on the boundary: standard CRUD operations belong on a stable, versioned REST contract, while anything that needs to push updates to the client — live collaboration, status changes, notifications — belongs on a persistent real-time channel like SignalR rather than being forced through polling. I pick the transport based on whether the client or the server needs to initiate the update, not out of habit.

In practice

  • REST for standard request/response CRUD operations against the backend
  • A persistent real-time channel (SignalR or equivalent) for anything server-initiated — live updates, notifications, presence
  • A shared, typed API client layer so every feature module handles requests, auth headers, and errors consistently
  • Breaking changes require a new version, never a silent field-shape change

State Management

Server state, module-scoped state, and shared entity state are three different problems.

The bugs I've spent the most time debugging all trace back to treating server data, module-specific UI state, and state shared across modules as one undifferentiated blob in a single store. In practice, with Redux Toolkit, that means each feature module gets its own slice for its own concerns, while entities multiple modules need — a contract, a device, a user — live in a shared slice everyone reads from, so no module quietly holds a stale copy of something another module just changed.

In practice

  • Redux Toolkit slices scoped per feature module, not one giant undifferentiated store
  • Shared entity data (the thing multiple modules care about) lives in its own slice, referenced everywhere, never duplicated
  • Real-time updates dispatched into the same slices the rest of the UI already reads from, not a separate parallel state tree
  • Derived state computed with selectors at read time, not stored and manually kept in sync

Animations

Motion should explain a state change, not decorate one.

Every animation I ship has to answer 'what is this movement telling the user?' — that an item was removed, that this panel is now in focus, that an action succeeded. Motion that doesn't communicate causality or hierarchy is just latency with extra steps. I keep a small, shared set of easing and duration tokens across a whole product specifically so animations feel like one coherent system rather than a pile of individually-tuned effects.

In practice

  • A shared easing/duration token set used by every animation in the product, no one-off tuning per component
  • `prefers-reduced-motion` respected globally as a hard override, not a per-component opt-in
  • Layout-affecting animations (reordering, removal) always animated so users can track object permanence
  • Purely decorative motion (particles, ambient background) kept off the interaction critical path and cheap to disable

Developer Experience

Feedback loop speed compounds into shipping velocity more than almost anything else.

The single highest-leverage investment on most teams I've been on wasn't a feature — it was cutting the time between writing a line of code and knowing whether it's correct. Fast type-checking, fast local builds, and a preview deployment on every PR mean an engineer stays in flow instead of context-switching while CI runs. I treat DX tooling as product infrastructure, not a nice-to-have side project.

In practice

  • Preview deployments on every pull request, not just staging after merge
  • Typecheck and lint run incrementally in the editor, not discovered for the first time in CI
  • Local dev environment reproducible in one command — no tribal-knowledge setup steps
  • Flaky tests treated as a build-blocking bug, not muted and ignored