惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Blog — PlanetScale
Blog — PlanetScale
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Last Watchdog
The Last Watchdog
AI
AI
Recent Announcements
Recent Announcements
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
J
Java Code Geeks
TaoSecurity Blog
TaoSecurity Blog
L
LangChain Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Project Zero
Project Zero
Microsoft Security Blog
Microsoft Security Blog
量子位
T
Threatpost
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
博客园 - Franky
博客园 - 聂微东
L
LINUX DO - 最新话题
Security Archives - TechRepublic
Security Archives - TechRepublic
Hugging Face - Blog
Hugging Face - Blog
T
The Blog of Author Tim Ferriss
P
Proofpoint News Feed
The GitHub Blog
The GitHub Blog
C
Check Point Blog
宝玉的分享
宝玉的分享
G
Google Developers Blog
Spread Privacy
Spread Privacy
Cloudbric
Cloudbric
SecWiki News
SecWiki News
有赞技术团队
有赞技术团队
www.infosecurity-magazine.com
www.infosecurity-magazine.com
W
WeLiveSecurity
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
美团技术团队
V
Vulnerabilities – Threatpost
Cyberwarzone
Cyberwarzone
A
Arctic Wolf
P
Privacy & Cybersecurity Law Blog
P
Palo Alto Networks Blog
H
Help Net Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Cisco Talos Blog
Cisco Talos Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
A
About on SuperTechFans
N
Netflix TechBlog - Medium
罗磊的独立博客
月光博客
月光博客

Graphite blog

Introducing Code Tours: a new way to review Introducing Cursor Cloud Agents in Graphite Building the future of software development with Cursor Reimagining the PR Page: Designing for speed and focus Graphite changelog [11-20-2025] Graphite changelog [11-04-2025] Graphite changelog [10-16-2025] The future of engineering is collaborative (and already here) Meet Graphite Agent: the next evolution of AI code review Introducing frozen branches: A safer way to build on your teammates’ work Graphite changelog [09-17-2025] How we sped up code search for Graphite Chat Introducing Graphite Chat AI is writing code—here's why it also needs to review that code How I got Claude to write code I could actually ship How we built the first stack-aware merge queue (and why it matters) Graphite brings stacking to Tower Code review tooling: Should you build or buy? Making AI code review available to everyone Introducing: The new Graphite + Linear integration Graphite raises $52M and launches Diamond to reimagine code review for the age of AI Why AI will never replace human code review How stacked PRs unblock distributed development teams Graphite is going to Developer Week 2025 Beating the end of year code freeze How Graphite’s eng team ships code remarkably fast Why we chose Anthropic's Claude to power Graphite Reviewer AI code generation will remain fragmented How we redesigned Graphite's landing page in-house Introducing Graphite Reviewer: your AI code review companion How AI code review reduces review cycles to improve developer productivity What if you could get instant feedback on your code? The new developer toolchain Not Rocket Science - How Bors and Google’s TAP inspired modern merge queues Graphite's State of code review 2024 How Google migrated billions of lines of code from Perforce to Piper Going from 0 to 1: How to write better unit tests when there are none Speed up your merges: Parallel CI is now generally available for teams using Graphite’s merge queue Down for less than four minutes a month: how AWS deploys code BitKeeper, Linux, and licensing disputes: How Linus wrote Git in 14 days Graphite is now free for startups and open source projects Launch week wrap-up (May 2024) Reduce CI costs for Buildkite and GitHub Actions Cheaper CI & faster merging with batching How Google does code review The technical learning curve at a startup is gentler than you might think Graphite will now automatically rebase your partially-merged stacks Multiple engineers can now seamlessly collaborate on the same stack of PRs Do you ever outgrow GitHub? From the 80's to 2024 - how CI tests were invented and optimized Graphite changelog [4/10/2024] 🎺 Graphite changelog [4/25/2024] 🐸 How Stack Overflow replaced Experts Exchange How GitHub monopolized code hosting Graphite changelog [3/27/2024] 🤝 The core principles of building a good AI feature Onboarding roulette: deleting our employee accounts daily Graphite changelog [3/13/2024] 🚁 Why Facebook doesn’t use Git How to recreate the Phabricator code review workflow Types of code reviews: Improve performance, velocity, and quality What's the best GitHub pull request merge strategy? Phabricator vs GitHub vs Graphite: How do they stack up? Improving team velocity through better pull request practices Moving fast breaks things: the importance of a staging environment Building trust as a software engineer Keeping code simple: moving fast by avoiding over-engineering What's better than GitHub pull request filters? The Graphite pull request inbox 7 Best Phabricator alternatives for PR stacking + code review [2024] Accurate eng estimations: predicting and negotiating the future Tracking and understanding GitHub PR stats: A step-by-step guide 8 pull request best practices for optimal engineering What’s next for Graphite Graphite Q1 Launch week: Stacking with the tools you love Graphite Q1 Launch week: Making stacking seamless Accelerating code review The Mom Test How to use stacked PRs to unblock your entire team Graphite Q1 launch week 2024 The practical and philosophical problems with AI code review Empirically sup code review best practices Call site attribution: how to pinpoint rogue SQL queries throttling your performance Every engineer should understand git reflog Post mortem: we took 124 seconds from you, here's 378 back Your GitHub pull request workflow is slowing everyone down Optimizing CI/CD workflows for trunk-based development Why we use AWS instead of Vercel to host our Next.js app How large pull requests slow down development 3 key lessons in application server optimization Trunk-based development: why you should stop using feature branches Git was built in 5 days Why large companies and fast-moving startups are banning merge commits How long should your CI take? Experimenting with AI code review CRA to AppRouter in 5 Steps: A case study with Graphite Graphite Changelog [10/18/2023] The comprehensive guide to writing the best PR title of all time How 10,000 Developers All Contribute to the same Repo
How we organize our monorepo to ship fast
Greg Foster · 2025-05-28 · via Graphite blog

Building developer tools is a meta-problem: Your architecture decisions directly impact how quickly you can build the very tools that make other developers more productive. At Graphite, we've learned that the right monorepo structure isn't just about code organization—it's about enabling a team of 15 engineers to ship over 1,000 pull requests per month while maintaining quality and sanity.


When I started Graphite with my co-founders, we made what many would consider (at the time) a contrarian bet: no microservices, no polyglot architecture, no distributed complexity. Just TypeScript, everywhere, in a single monorepo with a single server image. Years later, as we scale up to 40 engineers and millions of requests, I'm more convinced than ever that this approach was right for us—not because it's perfect or universally applicable, but because it optimizes for what actually matters at our scale: developer velocity.

This wasn't a decision made lightly. Coming from Facebook, we'd seen the power of internal tools like Phabricator operating at massive scale with monorepo principles. We also recognized that good architecture should fundamentally reduce complexity. Specifically, we identified three main sources of software complexity:

  1. Change amplification: How many places must change when you update something.

  2. Cognitive load: How much engineers must keep in their heads.

  3. Unknown unknowns: Untracked dependencies or side effects.

This isn't to say microservices or polyglot architectures are wrong, though. They solve real problems around team autonomy, technology diversity, and independent scaling. But for a team our size building a cohesive product, the coordination overhead often outweighs the benefits.

The philosophy: Composition over complexity

Our architecture philosophy draws heavily from "A Philosophy of Software Design" and clean architecture principles. We've learned that premature distribution is the root of much engineering evil. Instead of breaking apart our system into microservices, we break apart our code into composable modules.

Consider here how we handle external integrations. Rather than creating separate services for GitHub sync, AI processing, and email notifications, we use dependency inversion to create interfaces we own:

interface GitHubClient {

getPullRequest(repo: string, number: number): Promise<PullRequest>;

syncRepository(repo: Repository): Promise<SyncResult>;

}

interface AIClient {

generateCodeReview(diff: string, context: ReviewContext): Promise<Review>;

}

const app = createApplication({

github: new GitHubClientImpl(githubToken),

ai: createOpenAIClient(openaiConfig),

logger: new DatadogLogger(),

});

const testApp = createApplication({

github: new MockGitHubClient(),

ai: new MockAIClient(),

logger: new NoOpLogger(),

});

This pattern has saved us countless times. When we needed to swap from one AI provider to another, replace our logging infrastructure, or add a new feature flag system, the business logic never changed. We just swapped the implementation behind the interface.

The Vercel AI SDK has been particularly helpful here, providing a unified interface across different LLM providers while handling streaming, function calling, and other complexities. Instead of maintaining provider-specific integration code, we can focus on the AI features themselves.

TypeScript everywhere: One language, many benefits

In early 2020, we faced the classic startup choice: Python/Ruby for rapid prototyping or something more structured for long-term maintainability. We chose TypeScript not just because it was gaining momentum, but because it eliminated an entire class of problems.

The type system catches bugs at compile time that would otherwise surface in production. More importantly, when half your complexity lives in the frontend and half in the backend, sharing a single language with shared type definitions creates a development experience that feels magical:

export const CreatePullRequestRequestSchema = z.object({

title: z.string().min(1),

description: z.string(),

baseBranch: z.string(),

headBranch: z.string(),

});

export type CreatePullRequestRequest = z.infer<typeof CreatePullRequestRequestSchema>;

app.post('/api/pull-requests', async (req: Request) => {

const validatedData = CreatePullRequestRequestSchema.parse(req.body);

const pr = await createPullRequest(validatedData);

return { success: true, pullRequest: pr };

});

const response = await fetch('/api/pull-requests', {

method: 'POST',

body: JSON.stringify({

title: formData.title,

description: formData.desc,

baseBranch: 'main',

headBranch: currentBranch

})

});

When you change an API contract, TypeScript tells you everywhere that needs updating. When you refactor a core type, the compiler becomes your pair programming partner, showing you exactly what broke and where. In an llm-assisted world, this benefit as only increased.

Zod has been crucial for keeping this type safety consistent between compile-time and runtime. It lets us define schemas once and use them for both TypeScript types and runtime validation, which eliminates the common problem of API contracts drifting between frontend assumptions and backend reality.

Monorepo structure: Libraries as first-class citizens

Our monorepo isn't just a collection of apps thrown together—it's architected as a ecosystem of composable libraries:

libs/

├── public/

│ ├── auth/

│ ├── git-diff-parser/

│ └── shared-types/

├── private/

│ ├── db-client/

│ ├── github-client/

│ └── ai-client/

apps/

├── private/server/

└── public/

├── graphite-app/

├── cli/

└── vscode/

Each library has a single responsibility and clean interfaces. Our database client wraps Typeorm heavily—we've learned to distrust ORMs' query generation and prefer explicit control:

export async function findPullRequestsForUser(

userId: string,

filters: PullRequestFilters

): Promise<PullRequest[]> {

const query = db

.select('*')

.from('pull_requests')

.where('author_id', userId)

.where('status', 'in', filters.statuses);

if (filters.repository) {

query.where('repository_id', filters.repository);

}

return query.execute();

}

This approach has prevented countless N+1 query problems and makes performance optimization straightforward.

Build system: Turbo powers our velocity

With 50+ packages in our monorepo, naive builds would be painfully slow. In our early days, we experienced what internal Slack threads called "rebuild hell"—small changes in the server would trigger lengthy builds for unrelated libraries. This growing pain pushed us toward better modularization and ultimately led us to adopt Turbo.

Turbo transforms our build pipeline into a directed acyclic graph where each library only rebuilds when its dependencies change:

{

"tasks": {

"build": {

"dependsOn": ["^build"],

"outputs": ["dist/**"],

"inputs": ["src/**", "package.json", "tsconfig.json"]

},

"test": {

"dependsOn": ["^build"],

"outputs": [],

"inputs": ["src/**", "test/**"]

}

}

}

The result? Running `yarn build` intelligently skips unchanged packages and only rebuilds what's necessary. We've seen 10x+ speedups compared to naive rebuilding everything. Most build operations are now sub-second for unchanged packages, transforming our development experience from frustrating waits to near-instant feedback.

Even better, our testing strategy leverages the same dependency graph. We maintain a shared testing library that re-exports consistent patterns:

export {

describe,

it,

expect,

beforeEach,

afterEach,

jest,

} from '@jest/globals';

export { createMockDatabase } from './database';

export { createMockGitHubClient } from './github';

export { setupTestEnvironment } from './environment';

Every package in our monorepo uses identical Jest testing patterns, making it trivial for any engineer to write tests anywhere in the codebase.

Single server image: Simplicity at scale

While the industry often preaches microservices, we've found a single, stateless Docker container scales surprisingly far. Our deployment strategy is dead simple:

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./

RUN yarn install --frozen-lockfile

COPY . .

RUN yarn build

EXPOSE 8000

CMD ["node", "dist/index.js"]

This stateless approach, combined with horizontal scaling through AWS ECS, handles millions of requests with ease. When we need to scale, we add more instances. When we need to deploy, we replace them atomically.

The real win isn't performance—it's operational simplicity. One image to build, one deployment pipeline to maintain, one set of logs to monitor. Our continuous delivery pipeline automatically promotes builds from staging to production based on health checks and performance metrics.

Development experience: one command to rule them all

Great architecture enables great developer experience. Our entire development environment boots with a single command:

This command:

  1. Installs all dependencies across 50+ packages.

  2. Builds libraries in dependency order using Turbo.

  3. Starts the development server with hot reload.

  4. Connects to our long-lived staging database.

That last point is controversial but powerful. Instead of maintaining complex local database seeding scripts, we develop against staging data. It's a form of "testing in production," but staging is effectively an isolated production environment used only by employees. New engineers can start contributing meaningful code on day one without wrestling with incomplete local data.

Functional programming: Why we avoid classes

We actively discourage class-based programming, not out of dogma, but because of practical issues we've observed in our codebase and codified in our engineering principles. Classes don't encourage static dependency injection by default—dependencies get hidden in constructors and instance variables. They tend to grow into large files with many methods shoved inside a single class. Most problematically, they indirectly encourage inheritance hierarchies that become difficult to refactor.

This philosophy emerged from early internal debates and is now reinforced in our onboarding and code review practices. Classes aren't evil, but we've found that leaning toward functional composition leads to cleaner, more testable code:

class PullRequestService {

constructor(private github: GitHubClient, private db: Database) {}

async createPullRequest(data: CreatePRData): Promise<PullRequest> {

}

}

export async function createPullRequest(

github: GitHubClient,

db: Database,

data: CreatePRData

): Promise<PullRequest> {

const githubPR = await github.createPullRequest(data);

const dbPR = await db.pullRequests.create({

githubId: githubPR.id,

title: data.title,

});

return transformToApiResponse(dbPR, githubPR);

}

This style makes testing trivial, side effects explicit, and refactoring safer. Dependencies are explicit parameters rather than hidden instance state. Functions naturally stay focused on single responsibilities.

Database strategy: Postgres + Redis + S3

Our data layer is deliberately simple: Postgres for transactional data, Redis for caching and pub/sub, S3 for blob storage. This trinity handles everything from user authentication to real-time notifications to storing git diffs.

We lean heavily on Postgres's diverse strengths—JSONB for flexible schemas, full-text search for code search, transactions for consistency. For years, we even powered our entire PR inbox functionality with custom filtering rules implemented directly in Postgres. The combination of JSONB queries, complex WHERE clauses, and careful indexing let us build surprisingly sophisticated filtering without additional infrastructure.

Our database design also emphasizes idempotent, self-healing write paths and application-level joins rather than foreign key constraints. This approach, documented in our internal design docs, enables easier sharding and scaling while reducing operational incidents through built-in retry logic.

That said, we're starting to outgrow this approach. As our data volume increases, we're finally moving to dedicated search indices for faster results. Postgres got us remarkably far, but knowing when to graduate to specialized tools is part of scaling wisely.

async function storePullRequestDiff(prId: string, diff: string): Promise<void> {

if (diff.length > 1000000) {

const key = `pr-diffs/${prId}.diff`;

await s3.upload(key, diff);

await db.pullRequests.update(prId, { diffLocation: `s3://${key}` });

} else {

await db.pullRequests.update(prId, { diff });

}

}

Infrastructure as code: Terraform all the way down

Our infrastructure is defined entirely in imperative Terraform (CDKTF), from ECS clusters to RDS instances to CloudFront distributions. This means our entire production environment can be recreated from git history.

More importantly, infrastructure changes go through the same code review process as application changes. When we need to add a new service, scale a database, or modify networking rules, it's all code that can be reviewed, tested, and rolled back.

The contrarian choices that work

Several of our architectural decisions go against conventional wisdom:

  • We develop against staging data instead of local seed data. This eliminates the maintenance burden of keeping local development databases in sync with production schemas and gives developers realistic data to work with from day one.

  • We actively discourage classes in favor of pure functions. This naturally steers the codebase toward composition and makes testing significantly easier.

  • We chose a monorepo with a single deployment artifact instead of microservices. This reduces operational complexity and enables atomic deployments across the entire system. That said, we recognize this approach has limits—as teams grow beyond a certain size, the benefits of service autonomy and independent deployments may outweigh the coordination costs.

  • We wrapped our ORM heavily instead of trusting its abstractions. This gives us fine-grained control over database queries and prevents performance surprises.

These choices work because they optimize for our constraints: a growing team that needs to ship quickly while maintaining quality.

Results that matter

Our architecture enables the metrics that matter most, which we track obsessively - partly using our own Graphite Insights tools:

  • 15 engineers shipping 1,000+ PRs per month - and scaling to 40 engineers this year

  • Fast build times for most changes thanks to Turbo's intelligent caching

  • One-command development environment that new hires can use productively on day one

  • Zero-downtime deployments through stateless horizontal scaling

  • Shared code between frontend and backend eliminating entire classes of integration bugs

These aren't vanity metrics—they represent real developer happiness and productivity gains. The real test of any architecture isn't how it looks in a diagram—it's how it feels to work with every day. Our engineers spend their time building features, not fighting the build system. They think in one language all day, not context-switching between different tech stacks. They can confidently refactor across package boundaries because TypeScript catches the breakages.

Lessons for similar teams

These architectural choices work well for us, but they're not universal truths. If you're building developer tools or any product where engineering velocity directly impacts business outcomes, consider these principles while adapting them to your specific context:

  • Optimize for developer experience over theoretical scalability. You can always add complexity later, but you can't easily remove it.

  • Choose technologies that compound. TypeScript's benefits multiply when used everywhere. Monorepos pay dividends when tooling is shared across packages.

  • Libraries beat microservices at small scale. Modular code in a single deployment is often simpler than distributed services.

  • Functional programming isn't just academic. Pure functions and explicit dependencies make large codebases maintainable.

  • Testing strategies should be as systematic as your architecture. Shared testing utilities across a monorepo ensure consistent quality everywhere.

  • Continuous experimentation is key. We're always iterating—refining boundaries, experimenting with build optimizations, and evolving patterns as the codebase and team grow. Architecture is never "done."

The goal isn't to build the most impressive architecture—it's to build the one that best serves your team's ability to ship great software. For us, that's meant embracing simplicity, sharing everything we can, and optimizing relentlessly for the feedback loop between idea and deployed code.


Want to see how these principles work in practice? Graphite is hiring across engineering roles!