Every article so far has used one Claude Code session. One terminal, one conversation, one context. That works for small projects, but what about larger codebases where different parts can be built in parallel?

Claude Code supports multiple agents working on the same project. You can have one agent building the backend, another building the frontend, and a third writing tests — all at the same time.

This article tests whether multi-agent development actually works. Does it save time? What happens when agents step on each other’s code? How do you coordinate?

Total time: 3 hours 47 minutes. 4 agent sessions. Integration took longer than expected.

What We’re Building

A task management application (like a minimal Trello) with:

  • Backend API — Express + PostgreSQL with Prisma

    • Auth (register, login, JWT)
    • CRUD projects
    • CRUD tasks with status columns (Todo, In Progress, Done)
    • Assign tasks to users
    • Task comments
  • Frontend — React + Tailwind CSS

    • Login and register pages
    • Dashboard with project list
    • Kanban board (drag and drop)
    • Task detail modal with comments
  • Tests — Playwright E2E + Supertest API tests

    • User registration and login flow
    • Create project and task
    • Drag task between columns
    • API endpoint tests with auth

Tech stack: Node.js (Express), React (Vite), PostgreSQL, Prisma, Playwright, Supertest, Tailwind CSS

Prerequisites

  • Claude Code installed and configured
  • Node.js 18+ installed
  • PostgreSQL running locally (or Docker)
  • 3 terminal windows (or tmux/screen)

The Multi-Agent Setup

Before writing any code, I needed a coordination plan. The key challenge is that three agents working on the same codebase can create merge conflicts and integration mismatches. Here is how I set it up.

The Shared Contract: OpenAPI Spec

The most important decision: create an API contract before any agent starts coding. This is the single source of truth that all three agents reference.

I wrote the OpenAPI spec manually in about 15 minutes:

# docs/api.yaml
openapi: 3.0.3
info:
  title: TaskBoard API
  version: 1.0.0

paths:
  /api/auth/register:
    post:
      summary: Register a new user
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
      responses:
        201:
          description: User created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"

  /api/auth/login:
    post:
      summary: Login
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                email: { type: string }
                password: { type: string }
      responses:
        200:
          description: Login successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"

  /api/projects:
    get:
      summary: List user's projects
      security: [{ bearerAuth: [] }]
      responses:
        200:
          description: Project list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Project"
    post:
      summary: Create project
      security: [{ bearerAuth: [] }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string }
      responses:
        201:
          description: Project created

  /api/projects/{projectId}/tasks:
    get:
      summary: List tasks in project
      security: [{ bearerAuth: [] }]
      parameters:
        - name: projectId
          in: path
          required: true
          schema: { type: string }
      responses:
        200:
          description: Task list grouped by status

    post:
      summary: Create task
      security: [{ bearerAuth: [] }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                description: { type: string }
                status: { type: string, enum: [todo, in_progress, done] }
                assigneeId: { type: string }

  /api/tasks/{taskId}:
    patch:
      summary: Update task (status, assignee, etc.)
      security: [{ bearerAuth: [] }]

  /api/tasks/{taskId}/comments:
    get:
      summary: List comments on task
    post:
      summary: Add comment to task

components:
  schemas:
    AuthResponse:
      type: object
      properties:
        token: { type: string }
        user:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
            email: { type: string }

    Project:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        description: { type: string }
        createdAt: { type: string, format: date-time }

    Task:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        description: { type: string }
        status: { type: string, enum: [todo, in_progress, done] }
        assignee:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
        createdAt: { type: string, format: date-time }

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

The Directory Structure

I created the project structure manually to prevent agents from fighting over it:

mkdir -p taskboard/{server,client,tests,docs}

Each agent works in its own directory. The only shared files are in docs/.

Agent Instructions

Each agent got a system prompt via a CLAUDE.md file in their directory.

The Build Session

I opened 3 terminal windows side by side (tmux with 3 panes).

Agent 1: Backend (Terminal 1)

You are the backend agent for a task management app.

Your workspace: server/
API contract: ../docs/api.yaml (follow this exactly)

Build an Express + PostgreSQL API with Prisma:
1. User auth (register, login) with JWT
2. CRUD projects — users see only their own projects
3. CRUD tasks within projects — with status (todo, in_progress, done)
4. Task assignment — assign to project members
5. Task comments — add and list comments

Database schema:
- User: id, name, email, passwordHash, createdAt
- Project: id, name, description, ownerId, createdAt
- ProjectMember: userId, projectId, role (owner/member)
- Task: id, title, description, status, projectId, assigneeId, createdAt, updatedAt
- Comment: id, content, userId, taskId, createdAt

Requirements:
- TypeScript throughout
- Input validation with zod
- Proper error handling with status codes
- CORS enabled for http://localhost:5173
- Runs on port 3001
- Follow the OpenAPI spec in ../docs/api.yaml exactly

What Agent 1 generated:

The agent scaffolded the entire backend in about 18 minutes. Clean TypeScript, zod validation, Prisma schema, and all REST endpoints matching the OpenAPI spec.

The project structure:

server/
  src/
    routes/
      auth.ts
      projects.ts
      tasks.ts
      comments.ts
    middleware/
      auth.ts
      validate.ts
      errorHandler.ts
    lib/
      prisma.ts
      jwt.ts
    index.ts
  prisma/
    schema.prisma
  package.json
  tsconfig.json

The Prisma schema was correct. The auth middleware extracted the JWT from the Authorization header and added req.user to the request. The zod schemas matched the OpenAPI spec.

Problem: The agent created a ProjectMember join table, but the task assignment endpoint allowed assigning to any user, not just project members. I noted this for the integration phase.

Time spent: 52 minutes (including setup and fixes)

Agent 2: Frontend (Terminal 2)

Started at the same time as Agent 1.

You are the frontend agent for a task management app.

Your workspace: client/
API contract: ../docs/api.yaml (follow this exactly)
API base URL: http://localhost:3001

Build a React frontend with Vite + Tailwind CSS:
1. Login page at /login
2. Register page at /register
3. Dashboard at / — list of user's projects with "New Project" button
4. Project page at /projects/:id — kanban board with 3 columns:
   - Todo, In Progress, Done
   - Cards show: title, assignee avatar, comment count
   - Drag and drop between columns (use @dnd-kit/core)
5. Task detail modal — click a card to open:
   - Title, description, status dropdown, assignee dropdown
   - Comments section with add comment form
6. Header with user name and logout button

Requirements:
- TypeScript throughout
- Tailwind CSS for styling
- Dark sidebar, light content area
- Store JWT in memory (not localStorage)
- Use React Query for API calls
- Mobile responsive

What Agent 2 generated:

The frontend agent was slower because it had more UI to build. It took about 25 minutes for the first working version.

The kanban board component:

import { DndContext, DragEndEvent, closestCenter } from "@dnd-kit/core";

function KanbanBoard({ projectId }: { projectId: string }) {
  const { data: tasks, refetch } = useQuery({
    queryKey: ["tasks", projectId],
    queryFn: () => api.getTasks(projectId),
  });

  const updateTask = useMutation({
    mutationFn: ({ taskId, status }: { taskId: string; status: string }) =>
      api.updateTask(taskId, { status }),
    onSuccess: () => refetch(),
  });

  const columns = [
    { id: "todo", title: "Todo", color: "border-gray-400" },
    { id: "in_progress", title: "In Progress", color: "border-blue-400" },
    { id: "done", title: "Done", color: "border-green-400" },
  ];

  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event;
    if (!over) return;

    const taskId = active.id as string;
    const newStatus = over.id as string;

    updateTask.mutate({ taskId, status: newStatus });
  }

  return (
    <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
      <div className="flex gap-4 h-full overflow-x-auto p-4">
        {columns.map((column) => (
          <KanbanColumn
            key={column.id}
            id={column.id}
            title={column.title}
            color={column.color}
            tasks={tasks?.filter((t: Task) => t.status === column.id) || []}
          />
        ))}
      </div>
    </DndContext>
  );
}

The API client:

class ApiClient {
  private token: string | null = null;

  setToken(token: string) {
    this.token = token;
  }

  clearToken() {
    this.token = null;
  }

  private async request(path: string, options: RequestInit = {}) {
    const headers: Record<string, string> = {
      "Content-Type": "application/json",
      ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
    };

    const res = await fetch(`http://localhost:3001${path}`, {
      ...options,
      headers: { ...headers, ...(options.headers as Record<string, string>) },
    });

    if (res.status === 401) {
      this.clearToken();
      window.location.href = "/login";
      throw new Error("Unauthorized");
    }

    if (!res.ok) {
      const error = await res.json();
      throw new Error(error.message || "Request failed");
    }

    return res.json();
  }

  login(email: string, password: string) {
    return this.request("/api/auth/login", {
      method: "POST",
      body: JSON.stringify({ email, password }),
    });
  }

  register(name: string, email: string, password: string) {
    return this.request("/api/auth/register", {
      method: "POST",
      body: JSON.stringify({ name, email, password }),
    });
  }

  getProjects() {
    return this.request("/api/projects");
  }

  createProject(name: string, description: string) {
    return this.request("/api/projects", {
      method: "POST",
      body: JSON.stringify({ name, description }),
    });
  }

  getTasks(projectId: string) {
    return this.request(`/api/projects/${projectId}/tasks`);
  }

  updateTask(taskId: string, data: Partial<Task>) {
    return this.request(`/api/tasks/${taskId}`, {
      method: "PATCH",
      body: JSON.stringify(data),
    });
  }

  getComments(taskId: string) {
    return this.request(`/api/tasks/${taskId}/comments`);
  }

  addComment(taskId: string, content: string) {
    return this.request(`/api/tasks/${taskId}/comments`, {
      method: "POST",
      body: JSON.stringify({ content }),
    });
  }
}

export const api = new ApiClient();

My review: The frontend looked good. The kanban board had proper drag-and-drop, the task detail modal worked, and the dark sidebar layout was professional.

Problem: The API client stored the JWT in a class property, which is in-memory and refreshes on page reload. Claude did not implement token persistence or a refresh flow. Users had to log in again after every page refresh. This is technically more secure than localStorage, but terrible for UX. I noted this for the integration phase.

Time spent: 58 minutes

Agent 3: Tests (Terminal 3)

Started 30 minutes after Agents 1 and 2 (needed the API spec to be finalized).

You are the testing agent for a task management app.

Your workspace: tests/
API running at: http://localhost:3001
Frontend running at: http://localhost:5173
API contract: ../docs/api.yaml

Write two test suites:

1. API tests (tests/api/) using supertest + jest:
   - Auth: register, login, invalid credentials, duplicate email
   - Projects: create, list, get, update, delete, unauthorized access
   - Tasks: create, list, update status, assign, unauthorized access
   - Comments: create, list

2. E2E tests (tests/e2e/) using Playwright:
   - Registration flow: fill form, submit, redirected to dashboard
   - Login flow: fill form, submit, see projects
   - Create project: click "New Project", fill form, see it in list
   - Create task: click "Add Task", fill form, see it in kanban column
   - Drag task: drag from "Todo" to "In Progress", verify status changed
   - Task detail: click task card, see modal, add comment

Requirements:
- Each test should be independent (clean database state)
- Use a test database (different from development)
- Playwright tests should run in headless Chromium
- Include a test setup script that seeds the test database

What Agent 3 generated:

The API tests:

import request from "supertest";

const API = "http://localhost:3001";

describe("Auth", () => {
  let token: string;

  test("register new user", async () => {
    const res = await request(API).post("/api/auth/register").send({
      name: "Alex Test",
      email: `test-${Date.now()}@example.com`,
      password: "password123",
    });
    expect(res.status).toBe(201);
    expect(res.body.token).toBeDefined();
    expect(res.body.user.name).toBe("Alex Test");
    token = res.body.token;
  });

  test("login with valid credentials", async () => {
    const email = `login-${Date.now()}@example.com`;
    await request(API).post("/api/auth/register").send({
      name: "Sam",
      email,
      password: "password123",
    });

    const res = await request(API).post("/api/auth/login").send({
      email,
      password: "password123",
    });
    expect(res.status).toBe(200);
    expect(res.body.token).toBeDefined();
  });

  test("login with wrong password", async () => {
    const res = await request(API).post("/api/auth/login").send({
      email: "wrong@example.com",
      password: "wrongpassword",
    });
    expect(res.status).toBe(401);
  });

  test("register with duplicate email", async () => {
    const email = `dup-${Date.now()}@example.com`;
    await request(API).post("/api/auth/register").send({
      name: "User 1",
      email,
      password: "password123",
    });

    const res = await request(API).post("/api/auth/register").send({
      name: "User 2",
      email,
      password: "password123",
    });
    expect(res.status).toBe(409);
  });
});

describe("Projects", () => {
  let token: string;

  beforeAll(async () => {
    const res = await request(API).post("/api/auth/register").send({
      name: "Project Tester",
      email: `proj-${Date.now()}@example.com`,
      password: "password123",
    });
    token = res.body.token;
  });

  test("create project", async () => {
    const res = await request(API)
      .post("/api/projects")
      .set("Authorization", `Bearer ${token}`)
      .send({ name: "Test Project", description: "A test project" });
    expect(res.status).toBe(201);
    expect(res.body.name).toBe("Test Project");
  });

  test("list projects", async () => {
    const res = await request(API)
      .get("/api/projects")
      .set("Authorization", `Bearer ${token}`);
    expect(res.status).toBe(200);
    expect(Array.isArray(res.body)).toBe(true);
    expect(res.body.length).toBeGreaterThan(0);
  });

  test("unauthorized access", async () => {
    const res = await request(API).get("/api/projects");
    expect(res.status).toBe(401);
  });
});

Playwright E2E tests:

import { test, expect } from "@playwright/test";

test.describe("Task Management App", () => {
  const testEmail = `e2e-${Date.now()}@example.com`;
  const testPassword = "password123";

  test("user can register", async ({ page }) => {
    await page.goto("/register");

    await page.fill('[name="name"]', "E2E Tester");
    await page.fill('[name="email"]', testEmail);
    await page.fill('[name="password"]', testPassword);
    await page.click('button[type="submit"]');

    await expect(page).toHaveURL("/");
    await expect(page.locator("text=My Projects")).toBeVisible();
  });

  test("user can login", async ({ page }) => {
    await page.goto("/login");

    await page.fill('[name="email"]', testEmail);
    await page.fill('[name="password"]', testPassword);
    await page.click('button[type="submit"]');

    await expect(page).toHaveURL("/");
  });

  test("user can create a project", async ({ page }) => {
    // Login first
    await page.goto("/login");
    await page.fill('[name="email"]', testEmail);
    await page.fill('[name="password"]', testPassword);
    await page.click('button[type="submit"]');
    await expect(page).toHaveURL("/");

    // Create project
    await page.click("text=New Project");
    await page.fill('[name="projectName"]', "Test Project");
    await page.fill('[name="projectDescription"]', "Created by E2E test");
    await page.click("text=Create");

    await expect(page.locator("text=Test Project")).toBeVisible();
  });

  test("user can create a task", async ({ page }) => {
    // Login and navigate to project
    await page.goto("/login");
    await page.fill('[name="email"]', testEmail);
    await page.fill('[name="password"]', testPassword);
    await page.click('button[type="submit"]');

    await page.click("text=Test Project");

    // Create task
    await page.click("text=Add Task");
    await page.fill('[name="taskTitle"]', "Test Task");
    await page.fill('[name="taskDescription"]', "Created by E2E test");
    await page.click("text=Create Task");

    await expect(page.locator("text=Test Task")).toBeVisible();
  });

  test("user can drag task between columns", async ({ page }) => {
    // Login and navigate
    await page.goto("/login");
    await page.fill('[name="email"]', testEmail);
    await page.fill('[name="password"]', testPassword);
    await page.click('button[type="submit"]');

    await page.click("text=Test Project");

    // Drag task from Todo to In Progress
    const task = page.locator('[data-testid="task-card"]').filter({ hasText: "Test Task" });
    const inProgressColumn = page.locator('[data-testid="column-in_progress"]');

    await task.dragTo(inProgressColumn);

    // Verify task is now in In Progress column
    await expect(
      inProgressColumn.locator('[data-testid="task-card"]').filter({ hasText: "Test Task" })
    ).toBeVisible();
  });
});

My review: The tests were comprehensive. The API tests covered happy paths and error cases. The E2E tests covered the core user flows. Using Date.now() in email addresses ensured test isolation without needing to clean the database.

Problem: The E2E tests depended on specific HTML attributes (name="projectName", data-testid="task-card") that Agent 2 might not have generated. This was the integration gap I expected.

Time spent: 38 minutes

Integration Phase: Bringing It All Together (Minute 90)

This was the hardest part. Three agents working independently created code that did not quite fit together. Here is what I had to fix.

Issue 1: Form field names did not match

Agent 2 (frontend) used name="name" for the project name field. Agent 3 (tests) expected name="projectName". The OpenAPI spec did not specify form field names — only API payloads.

Fix: Updated the frontend form field names to match what the tests expected. Could have gone either way.

Issue 2: Missing data-testid attributes

The frontend had no data-testid attributes. The E2E tests relied on them for element selection.

Fix: I added data-testid attributes to the kanban columns and task cards. This took about 10 minutes.

Issue 3: Task status values mismatch

Agent 1 used todo, in_progress, done as status values (matching the OpenAPI spec). Agent 2 used TODO, IN_PROGRESS, DONE (uppercase). The frontend sent uppercase values, the backend expected lowercase, and the API returned 400 Bad Request.

Fix: Updated the frontend to use lowercase values. Added a zod enum validation on the backend that returned a clear error message.

Issue 4: Auth token not persisted across page navigations

Agent 2 stored the JWT in a class property. When navigating between pages (React Router), the token was preserved. But when the Playwright tests navigated via URL (page.goto), the token was lost because the page fully reloaded.

Fix: Added sessionStorage for the token (compromise between security and usability). The Playwright tests needed the token to persist across page loads.

Issue 5: CORS configuration

Agent 1 configured CORS for http://localhost:5173. But Playwright runs the tests against the same URL, so that was fine. The problem was that the API tests (supertest) made requests directly to the API, and there was no CORS issue. But when I ran the frontend and tests simultaneously, the backend sometimes got confused by concurrent requests.

Fix: Not really a fix — I ran the API tests and E2E tests sequentially, not in parallel.

Issue 6: Database state between tests

Agent 3 created tests that depended on each other (register, then login, then create project, then create task). But Playwright runs tests in random order by default. The later tests failed when run independently.

Fix: Made each test self-contained by registering a new user at the start of each test. This was slower but more reliable.

Time spent: 49 minutes (integration fixes)

What Went Right

  • Parallel development saved real time. All three agents worked simultaneously during the first 50 minutes. Without multi-agent, this would have been sequential: backend first, then frontend, then tests. The build phase took about 60 minutes instead of 150.
  • The OpenAPI spec prevented most API mismatches. Endpoints, request bodies, and response shapes all matched between frontend and backend because both agents referenced the same spec.
  • Test coverage was good. Agent 3 wrote comprehensive tests without seeing the implementation. The tests found real bugs (status value mismatch) that I would have missed in manual testing.
  • Each agent stayed in its directory. No merge conflicts because agents worked in separate directories. The monorepo structure with clear boundaries was the right choice.

What Went Wrong

1. UI Naming Conventions Were Not in the Contract

The OpenAPI spec defined API shapes but said nothing about HTML form field names, CSS classes, or data-testid attributes. This caused the biggest integration issues between frontend and tests.

Lesson: For multi-agent projects, the shared contract needs to include UI conventions, not just API shapes. Define data-testid patterns, form field naming, and URL structures in the shared docs.

2. Status Value Casing Mismatch

One agent used lowercase (todo), another used uppercase (TODO). The OpenAPI spec had lowercase in the enum, but Agent 2 missed it.

Lesson: Enums and constants must be explicitly called out in agent instructions. Agents do not always read the full OpenAPI spec carefully.

3. Auth Persistence Strategy Was Not Specified

“Store JWT in memory” was too vague. Agent 2 used a class property. Agent 3 expected the token to survive page reloads. Neither was wrong — they just had different assumptions.

Lesson: Specify the auth persistence strategy in the shared contract: localStorage, sessionStorage, HTTP-only cookie, or in-memory with refresh tokens.

4. Integration Took 30% of Total Time

The build phase (3 agents in parallel) took about 60 minutes. The integration phase took 49 minutes — nearly half the build time. For a simple project, this ratio is high. For a complex project, it might be worth it.

Lesson: Multi-agent development has a fixed integration cost. The project needs to be complex enough for the parallel build savings to exceed the integration overhead. For this project, it was borderline.

5. Test Ordering Dependencies

Sequential E2E tests that depend on shared state are fragile. Playwright’s default random ordering exposed these dependencies immediately.

Lesson: Every test should create its own state from scratch. This is good practice regardless of multi-agent development.

The Integration Checklist

After this experience, here is my checklist for future multi-agent projects:

  1. Write the OpenAPI spec first — all agents reference it
  2. Define UI conventions — data-testid patterns, form field names, CSS class prefixes
  3. Define shared constants — status enums, role names, error codes
  4. Define auth strategy — token storage, refresh flow, test auth helpers
  5. Define directory boundaries — each agent gets its own directory
  6. Run integration after each agent’s first prompt — do not wait until the end
  7. Use TypeScript shared types — a shared/types.ts file that all agents import
  8. Run all tests after every integration change — catch regressions immediately

Time Breakdown

PhaseTimeDetails
Planning & OpenAPI spec15 minManual — writing the API contract
Agent 1: Backend52 minExpress + Prisma + all endpoints
Agent 2: Frontend58 minReact + kanban + drag-and-drop
Agent 3: Tests38 minAPI tests + E2E tests
Integration & fixes49 min6 issues fixed
Final testing25 minAll tests passing
Total3 hr 47 min

Without multi-agent (sequential): estimated 4-5 hours for the same project. The time savings were modest — about 30-60 minutes. The real benefit was that the tests were written independently, which caught bugs the developer would have missed.

The Final Result

A working task management app with:

  • User registration and login
  • Create and manage projects
  • Kanban board with drag-and-drop
  • Task detail modal with comments
  • API tests (12 tests, all passing)
  • E2E tests (5 tests, all passing)

To run it yourself:

git clone https://github.com/kemalcodes/vibe-coding-projects.git
cd vibe-coding-projects
git checkout multi-agent

# Start the database
docker compose up -d postgres

# Start backend
cd server
npm install
npx prisma migrate dev
npm run dev

# Start frontend (new terminal)
cd client
npm install
npm run dev

# Run API tests (new terminal)
cd tests
npm install
npm run test:api

# Run E2E tests
npm run test:e2e

Time and Cost

  • Total time: 3 hours 47 minutes
  • Claude sessions: 3 agents + 1 integration session
  • Estimated token usage: ~90,000 tokens (across all agents)
  • Hosting cost: $0 (local development)
  • Parallel efficiency: ~20% faster than sequential

Lessons Learned

1. Multi-agent works best for projects with clear boundaries. Backend, frontend, and tests are natural boundaries. If agents need to modify the same files, conflicts will eat the time savings.

2. The shared contract is everything. Without the OpenAPI spec, the agents would have built incompatible APIs. Invest 15 minutes in the contract before starting any agent.

3. Integration is the bottleneck, not development. The code generation is fast. Making it all work together is where you spend the time. Budget 30% of total time for integration.

4. Independent tests are the biggest win. Having tests written by an agent that never saw the implementation is valuable. The tests found real bugs and enforced the API contract.

5. Multi-agent is not always faster. For this project (3-4 hours), the overhead nearly canceled the parallel savings. Multi-agent shines for projects that would take 8+ hours sequentially, where the parallel savings are more significant.

When to Use Multi-Agent

Use multi-agent when:

  • The project has 3+ clearly separable components
  • Each component takes 2+ hours to build
  • You have a well-defined interface contract
  • The components can be developed independently

Skip multi-agent when:

  • The project is small (under 3 hours total)
  • Components are tightly coupled
  • You do not have a clear API contract
  • You are learning a new tech stack (hard to debug integration issues)

Source Code

Full source code: kemalcodes/vibe-coding-projects (branch: multi-agent)

What’s Next?

Next up: Vibe Coding a Mobile App: KMP Productivity App with Claude. We test Claude on Kotlin Multiplatform — the most complex build system in this series. Can Claude handle expect/actual declarations, Gradle multiplatform config, and platform-specific code for Android and iOS?