New Features in git11

git11 is an AI workspace for engineering teams. It understands repositories, auto-generates documentation, answers deep code questions, and works securely across your GitHub organization.

✦ AI Documentation for any repository

✦ Ask anything about your codebase

✦ Organization & team access with permissions

✦ Secure GitHub App integration with audit logs

git11 repository intelligence dashboard

Scale is where clean code goes to die

How we actually structure a React monolith for millions of Git analyses without losing our minds.

By git11 Engineering · Thu Feb 26 2026 · 7 min read

Scale is where clean code goes to die

Most React architectural advice is written for todo lists. When you're processing millions of lines of code and rendering complex dependency graphs in real-time, the 'standard' way of doing things becomes a bottleneck.

At git11, we stopped following the trends two years ago. We found that most patterns advocated by 'thought leaders' don't survive a hundred thousand lines of TypeScript and fifty engineers shipping to the same repo.

The Folder Structure Lie

Stop nesting folders five levels deep. We tried the features/ directory approach, the atomic design approach, and the shared/ junk drawer approach. They all failed.

Deep nesting kills velocity. If a developer has to click through six levels of folders to find a CSS module, they’ll just write inline styles or duplicate the code. It's human nature.

We moved to a flattened src/modules structure. Every module is a self-contained unit: components, hooks, and tests live together. If a module exceeds 20 files, it’s too big. We split it. No exceptions.

src/modules/repo-graph/ contains Graph.tsx, useGraphData.ts, and Graph.test.ts. Simple. Obvious. If it needs a utility, it goes in src/lib. If it's a UI primitive like a button, it’s in src/ui.

State is a Liability

Redux is a graveyard for productivity. We ripped it out in 2022. It turns out that 90% of what people put in global state is actually just server cache.

We use TanStack Query for everything that comes from an API. It handles caching, retries, and loading states better than your custom middleware ever will. If you're manually managing isLoading booleans in a global store, you're wasting company money.

For actual UI state—the stuff that doesn't touch a database—we use high-up React Context or simple useState lifting. If a piece of state is needed by more than three disconnected branches of the tree, only then do we reach for Zustand.

Zustand is small, fast, and doesn't require boilerplate. It treats you like an adult. You write a function, it updates a value. Done.

The Component Fetish

Abstractions are expensive. We see junior devs creating a <GenericWrapper /> for everything. This is a mistake.

Every abstraction is a debt. You’re betting that the requirements won't change in a way that breaks your generic logic. In a fast-moving codebase, that’s a bad bet.

We prefer duplication over the wrong abstraction. If two components look similar but have different business logic, we let them be separate. We only dry things out when the logic is truly identical across three or more use cases.

Our AnalysisCard component has 400 lines of code. It’s ugly. It has a switch statement for different repo types. But it’s easy to read, easy to grep, and anyone can fix a bug in it without checking ten other files.

Managing the DOM Hell

Git11 visualizes large repos. Rendering 5,000 nodes in the DOM will crash a browser. React’s reconciliation isn't magic; it’s still JavaScript running on a single thread.

We use canvas for the heavy lifting and React to manage the overlays. If you're trying to build a high-performance graph tool using only <div> tags, you've already lost.

We offload heavy calculation—like calculating cyclomatic complexity across a thousand files—to a Web Worker. The main thread is for UI. The worker is for math. We use comlink to bridge the gap. It makes the worker look like a standard async function, which keeps the code clean.

TypeScript is Not Negotiable

We use strict: true. We ban any. We use unknown for API responses until they are validated with Zod.

If you don't validate your API boundaries, your types are just a suggestion. We use a script to generate TypeScript interfaces from our backend schema. If the backend changes a field name, the frontend build fails.

This prevents the 'undefined is not a function' errors that used to haunt our on-call rotations. If it builds, it usually works.

Testing What Matters

Unit testing every single pure function is a waste of time. We don't care if your formatDate helper works in isolation if the entire repository analysis page white-screens because of a race condition.

We focus on Integration tests with Playwright. We simulate real user flows: log in, connect GitHub, run analysis, view results.

We use MSW (Mock Service Worker) to intercept network requests. It allows us to test edge cases—like a 500 error from the GitHub API—without actually hitting the network. It’s faster, more reliable, and it documents exactly what the frontend expects from the backend.

Performance as a Feature

We don't optimize until we measure. We use the Profiler API in development to find rerender bottlenecks.

Most of the time, the fix isn't useMemo. It’s moving state lower in the tree. If typing in a search bar causes the entire sidebar to rerender, your state is too high.

We also use React.lazy for every route. There is zero reason for a user on the dashboard to download the code for the billing page. Our initial bundle size stays under 200kb, which is essential for our users on slower connections in different regions.

The Truth About Tools

We use Vite. Webpack is a legacy tool that requires a full-time engineer to maintain its config. If your dev server takes more than three seconds to start, you're losing developer hours every day.

We use Tailwind for CSS. It’s polarizing, but it solves the naming problem. 'What should I call this container?' is a question that shouldn't exist. With Tailwind, you just write the classes and move on. No more style.css files with 4,000 lines of dead code.

Finally, we use ESLint with a custom internal plugin that prevents common pitfalls, like using window.localStorage directly instead of our encrypted wrapper. Automated linting is the only way to enforce standards across a large team. Discussions in PRs should be about logic, not semi-colons.

Your architecture isn't a monument. It's a tool to ship features. If your code is 'elegant' but takes six weeks to change, it's garbage. Keep it flat, keep it typed, and stop over-complicating things.

Ask this question directly on your own repo →