Skip to main content
This document covers three critical quality attributes that are foundational to long-term system success: Maintainability (the ease of evolving and fixing the system), Testability (the ability to verify system correctness), and Deployability (the ease and safety of releasing changes).
These quality attributes are not added after the fact but are designed into the architecture from the beginning, ensuring the system remains healthy and productive as it grows.
How to read this page. Several patterns below (Hexagonal Architecture, ports/adapters, event sourcing, replay testing) are presented as teaching models, not as a description of the current codebase. Tuturuuu’s apps are conventional Next.js App Router apps (plus a Rust backend) that share a single Supabase project — there are no domain//infrastructure/ hexagonal layers, no event store, and no per-app inter-service contracts package. Code samples that show those layers are illustrative; sections labeled “In Tuturuuu today” describe the real, verifiable structure. Treat the rest as design guidance for how we want isolated modules to behave.
Active migration. apps/web (Next.js, port 7803) is being incrementally replaced by apps/tanstack-web (TanStack Start) plus apps/backend (Rust, port 7820). The maintainability and deployability properties described here are being carried forward into that target architecture. See TanStack + Rust Migration for the current state.
To understand how different architectural patterns affect these quality attributes, see Architectural Patterns Comparison.

Maintainability

Maintainability determines how easily the system can be modified to fix bugs, add features, or adapt to changing requirements. Poor maintainability leads to increasing development costs and slower delivery over time.

1. Clear Domain Boundaries Reduce Cognitive Load

Architectural Choice

The microservices architecture and modular frontend design establish clear boundaries between different business domains, each with well-defined responsibilities and interfaces.

Impact and Justification

In a monolithic, tightly-coupled system, understanding how to make a change requires comprehending vast swaths of interconnected code. A developer wanting to modify the billing logic might need to understand authentication, user management, and inventory systems simply because they’re all tangled together. Clear domain boundaries solve this by ensuring each service or module has a focused responsibility. A developer working on the FinanceService only needs to understand finance-related concepts and the clearly-defined interfaces for interacting with other services. This dramatically reduces cognitive load and enables faster, more confident changes. In Tuturuuu today:
apps/
├── web/          // Main Next.js platform (port 7803) - workspace management
├── tanstack-web/ // TanStack Start successor to apps/web (migration in progress)
├── backend/      // Rust backend (port 7820)
├── finance/      // Finance surface
├── calendar/     // Calendar surface
├── tasks/        // Tasks surface
├── nova/         // AI challenges
├── rewise/       // AI chat
└── mobile/       // Flutter mobile app

// Each app owns a focused surface. The web-facing apps are conventional
// Next.js App Router apps; they share one Supabase project rather than
// each owning a private datastore. Cross-cutting product logic lives in
// shared packages (packages/internal-api, packages/types, packages/ui).
The boundary that reduces cognitive load here is the app + feature folder, not a separate per-service runtime with its own database. A developer working on finance can stay inside apps/finance (and the finance helpers in packages/apis/packages/internal-api) without learning the calendar surface. Module boundaries in frontend:
// apps/web/src/app/[locale]/(dashboard)/[wsId]/
finance/
├── layout.tsx              // Finance-specific layout
├── transactions/           // Transaction management
├── reports/                // Financial reports
└── settings/               // Finance settings

// Finance module is self-contained
// Changes here don't affect calendar or tasks modules

Clarifying Additions

Engineers focus on one area at a time without understanding the entire system. New team members can become productive quickly by mastering a single domain rather than the entire codebase. This reduces onboarding complexity. Clear boundaries make it obvious where to find code related to specific features, accelerating the learning curve for new developers. Maintenance tasks become easier to isolate and execute. Bug fixes and feature additions are scoped to specific services or modules, reducing the risk of unintended side effects.

2. Technology Independence Prevents Legacy Lock-In

Architectural Choice

Isolating technology choices (databases, frameworks, external services) behind well-defined interfaces keeps core business logic decoupled from the tools that implement it.

Impact and Justification

Technology evolves rapidly. Frameworks fall out of favor, databases become outdated, and new tools emerge. Systems that tightly couple business logic to specific technologies become legacy systems—expensive and risky to modernize. Technology independence protects the valuable business logic (which changes rarely) from technological churn (which happens frequently). When infrastructure details sit behind an interface, you can upgrade or replace technologies without rewriting business rules.
Illustrative pattern. The WorkspaceRepository / SupabaseWorkspaceRepository example below is a teaching model. Tuturuuu does not ship packages/types/src/domain/workspace.ts or apps/web/src/infrastructure/repositories/*; there is no repository-interface layer. In the real code, route handlers and shared helpers in packages/internal-api and packages/apis call the Supabase client directly (see “In Tuturuuu today” below). Read this as how to keep a swappable seam if you introduce one.
// Illustrative: business logic expressed against an interface, not an implementation
export interface WorkspaceRepository {
  findById(id: string): Promise<Workspace | null>;
  save(workspace: Workspace): Promise<void>;
  delete(id: string): Promise<void>;
}

// Core business logic depends on the interface
export class WorkspaceService {
  constructor(private repo: WorkspaceRepository) {}

  async archiveWorkspace(id: string) {
    const workspace = await this.repo.findById(id);
    if (!workspace) throw new Error('Workspace not found');

    workspace.archive(); // Pure business logic
    await this.repo.save(workspace);
  }
}

// Illustrative infrastructure implementation (the swappable seam)
export class SupabaseWorkspaceRepository implements WorkspaceRepository {
  constructor(private client: SupabaseClient) {}

  async findById(id: string) {
    const { data } = await this.client
      .from('workspaces')
      .select('*')
      .eq('id', id)
      .single();

    return data ? this.toDomain(data) : null;
  }

  async save(workspace: Workspace) {
    await this.client
      .from('workspaces')
      .upsert(this.toDatabase(workspace));
  }

  async delete(id: string) {
    await this.client
      .from('workspaces')
      .delete()
      .eq('id', id);
  }
}

// Future: Can swap Supabase for Prisma, Drizzle, or any other ORM
// WITHOUT changing business logic
Example technology evolution:
// Today: Supabase
const repo = new SupabaseWorkspaceRepository(supabaseClient);

// Tomorrow: Drizzle ORM (business logic unchanged)
const repo = new DrizzleWorkspaceRepository(drizzleClient);

// Next year: Different database (business logic still unchanged)
const repo = new MongoWorkspaceRepository(mongoClient);

// Business logic stays the same:
const service = new WorkspaceService(repo);
await service.archiveWorkspace('ws-123');
In Tuturuuu today: technology independence comes from a few concrete seams rather than a full ports/adapters layer:
  • Supabase access is centralized behind the @tuturuuu/supabase/next/server factories (createClient() and createDynamicClient() are async, createAdminClient() is sync). Swapping how a client is built happens in one place.
  • Generated DB types are imported from @tuturuuu/types/db, so schema changes propagate through types instead of hand-written mappers.
  • Shared product logic lives in packages/internal-api and packages/apis, so the web app and its TanStack successor can reuse the same helpers as the migration proceeds.
The biggest real-world example of this independence is the in-progress move of apps/web to apps/tanstack-web + a Rust apps/backend: shared packages let the data layer be re-pointed without rewriting every call site.

Clarifying Additions

No single technical choice restricts future evolution. The architecture accommodates technological progress without requiring expensive rewrites of core business logic. Components modernize incrementally as needed. Instead of “big bang” technology migrations that put the entire system at risk, individual components can be upgraded independently. This protects the system from long-term stagnation. The architecture remains flexible enough to adopt new technologies as they mature, preventing technical debt accumulation.

3. Hexagonal Architecture Minimizes Ripple Effects

Architectural Choice

The Hexagonal Architecture’s strict separation between core domain logic and external concerns ensures that changes to infrastructure do not ripple into business rules.

Impact and Justification

In traditional layered architectures, changes to infrastructure (like upgrading a database library or switching message brokers) often require modifications to business logic because the layers are tightly coupled. Hexagonal Architecture inverts the dependency direction. The core domain is completely isolated and defines interfaces (ports) for what it needs. Infrastructure adapters implement these interfaces but the core remains ignorant of implementation details. This means infrastructure changes stay contained in the adapter layer. Upgrading from PostgreSQL 14 to PostgreSQL 16, changing from REST to GraphQL, or switching message brokers requires modifying only the relevant adapters—the domain core remains untouched.
Illustrative pattern, not current code. Tuturuuu does not implement a TaskService with injected TaskRepository / EventBus / Logger ports, and there is no @tuturuuu/logging or @tuturuuu/observability package. Task logic lives in conventional Next.js route handlers and shared helpers. The example below shows the shape of a hexagonal seam so you can recognize where one would help if a piece of logic becomes worth isolating. For background work, Tuturuuu uses Trigger.dev v4 task() definitions in packages/trigger/src — not a custom EventBus.
// Illustrative domain core (unchanged when infrastructure changes)
export class TaskService {
  constructor(
    private taskRepo: TaskRepository,
    private eventBus: EventBus,
    private logger: Logger
  ) {}

  async createTask(data: CreateTaskData) {
    const task = Task.create(data);

    await this.taskRepo.save(task);
    await this.eventBus.publish(new TaskCreated(task));
    this.logger.info('Task created', { taskId: task.id });

    return task;
  }
}

// Infrastructure layer (changeable without affecting domain)
// Version 1: Supabase + Trigger.dev + Console logging
const service = new TaskService(
  new SupabaseTaskRepository(supabase),
  new TriggerEventBus(trigger),
  new ConsoleLogger()
);

// Version 2: Drizzle + NATS + Structured logging
// Domain core completely unchanged!
const service = new TaskService(
  new DrizzleTaskRepository(drizzle),
  new NatsEventBus(nats),
  new StructuredLogger(winston)
);
Real-world example - Upgrading Next.js:
// Business logic and components remain the same
// Only infrastructure adapters need updates

// Before Next.js 16
export function getWorkspaceData(id: string) {
  // Implementation using Next.js 15 patterns
}

// After Next.js 16
export function getWorkspaceData(id: string) {
  // Implementation using Next.js 16 patterns
  // Component code using this function unchanged
}

Clarifying Additions

Core logic remains stable even during broad technical changes. The most valuable part of the codebase (business rules) is shielded from the volatility of technology trends. Adaptation happens at edges rather than at the center. When technology changes, only the outer layers (adapters) need modification, keeping changes localized and predictable. This protects business rules against unnecessary modification. Domain experts can rely on business logic remaining stable, reducing the risk of introducing bugs when upgrading infrastructure.

4. Modular Frontend Prevents UI-Level Entanglement

Architectural Choice

The React Modular Monolith pattern organizes the frontend into self-contained modules with clear boundaries, preventing the “big ball of mud” problem common in large SPAs.

Impact and Justification

As frontend applications grow, they often become tangled webs of interdependent components, shared state, and implicit dependencies. Making a change in one area unexpectedly breaks seemingly unrelated features. The modular monolith approach enforces architectural discipline by organizing code into feature-based modules with explicit boundaries. Each module (Finance, Calendar, Tasks, etc.) owns its components, state management, and business logic. Inter-module dependencies are explicit and minimized. This structure makes maintenance dramatically easier because changes stay localized within module boundaries. In Tuturuuu today: apps/web follows exactly this shape — feature folders under app/[locale]/(dashboard)/[wsId]/ (finance, calendar, tasks, …) each own their routes and colocated _components/. Client state uses Jotai (and TanStack Query for server data); the per-module state.ts paths below are a representative shape rather than fixed filenames.
apps/web/src/
├── app/[locale]/(dashboard)/[wsId]/
│   ├── finance/                    // Finance module
│   │   ├── layout.tsx
│   │   ├── transactions/
│   │   ├── reports/
│   │   └── _components/           // Finance-specific components
│   ├── calendar/                   // Calendar module
│   │   ├── layout.tsx
│   │   ├── events/
│   │   └── _components/           // Calendar-specific components
│   └── tasks/                      // Tasks module
│       ├── layout.tsx
│       ├── boards/
│       └── _components/           // Task-specific components
├── components/
│   ├── finance/                   // Shared finance components
│   ├── calendar/                  // Shared calendar components
│   ├── tasks/                     // Shared task components
│   └── common/                    // Truly shared components
Module independence:
// Finance module - self-contained state management
// apps/web/src/app/[locale]/(dashboard)/[wsId]/finance/state.ts
import { atom } from 'jotai';

export const financeTransactionsAtom = atom<Transaction[]>([]);
export const financeFiltersAtom = atom<Filters>({});

// Calendar module - separate state management
// apps/web/src/app/[locale]/(dashboard)/[wsId]/calendar/state.ts
import { atom } from 'jotai';

export const calendarEventsAtom = atom<Event[]>([]);
export const calendarViewAtom = atom<'month' | 'week' | 'day'>('month');

// Modules don't share state - clear boundaries

Clarifying Additions

UI concerns grow independently instead of interfering with each other. Finance features can evolve without touching calendar code, and vice versa. Teams can refactor modules safely. Because module boundaries are clear, teams can confidently restructure their module knowing they won’t break other features. Visual and functional changes stay well-scoped. UI redesigns or feature enhancements affect only the relevant module, reducing regression testing burden.

5. Operational Independence per Service

Architectural Choice

Each microservice can be deployed, scaled, monitored, and maintained independently without coordinating with other services.

Impact and Justification

In a monolithic architecture, operational tasks affect the entire system. Deploying a minor bug fix to the reporting module requires rebuilding and redeploying the entire application. Scaling requires scaling everything, even if only one feature is under load. Operational independence means each service is a separate deployable unit with its own lifecycle. Teams can:
  • Deploy updates to their service multiple times per day
  • Scale their service based on its specific load patterns
  • Monitor their service’s health independently
  • Apply operational changes (like upgrading Node.js) on their own schedule
This independence accelerates development velocity and reduces the coordination overhead that plagues monolithic systems. In Tuturuuu today: there are no deploy:web / deploy:finance / deploy:calendar package scripts. Independence is achieved through per-app Vercel projects wired up by dedicated GitHub Actions workflows. Each app has its own preview workflow under .github/workflows/ (for example vercel-preview-finance.yaml, vercel-preview-calendar.yaml, vercel-preview-drive.yaml), and Turborepo only rebuilds the apps affected by a change:
# Build a single workspace and its dependency graph (Turborepo filters by app)
bun turbo run build --filter=@tuturuuu/web

# Run an app locally (each app owns its own dev script)
cd apps/finance && bun dev
# Real per-app preview workflow — .github/workflows/vercel-preview-finance.yaml
name: Vercel Finance Preview Deployment
# Builds only the shared deps the finance app needs, then deploys it to Vercel:
#   bun turbo:local run build --filter=@tuturuuu/types --filter=@tuturuuu/supabase --filter=@tuturuuu/internal-api
Because each app is its own Vercel project, finance can deploy without rebuilding or redeploying the web or calendar apps. Monitoring: Tuturuuu does not ship a custom per-service metrics SDK. Runtime/server logs in API, cron, and infrastructure code use the native console method matching severity, while retained request, cron, deployment, and usage data still flows through the internal log-drain storage where those wrappers are wired. Vercel provides per-project deployment and runtime insight. Treat the monitorErrorRate(...) snippet style as conceptual.

Clarifying Additions

Services update without impacting other services. A finance service deployment doesn’t require coordination with the calendar team or risk affecting calendar functionality. Maintenance tasks stay focused and manageable. Security patches, dependency updates, and refactoring efforts are scoped to individual services. This encourages timely updates and better long-term health. Teams aren’t hesitant to deploy updates because the blast radius is limited to their service.

Testability

Testability determines how easily we can write and maintain automated tests that verify system correctness. High testability leads to better quality, faster development, and greater confidence in changes.

1. Pure Logic Enables Fast Unit Testing

Architectural Choice

Keeping pure logic free of infrastructure dependencies enables fast unit tests that don’t require databases, network calls, or other external systems. (A full Hexagonal layer is one way to enforce this; Tuturuuu does not implement that layering, but the principle — isolate decision logic from I/O — still applies inside conventional modules.)

Impact and Justification

In traditional architectures, business logic is often tightly coupled to infrastructure (database access, API calls, file I/O). This makes testing painful because every test requires:
  • Setting up test databases
  • Mocking network requests
  • Managing test data cleanup
  • Dealing with test environment flakiness
These tests are slow (seconds per test instead of milliseconds), fragile (break when infrastructure changes), and difficult to write (require complex setup). Isolating pure logic solves this by making the decision code free of infrastructure dependencies. Tests instantiate the object and verify rules with simple in-memory inputs. These tests run in milliseconds and never fail due to infrastructure issues.
Illustrative pattern. The Workspace class below does not exist at packages/types/src/domain/workspace.ts (there is no domain/ directory). It models how a pure, framework-free unit would look. Real tests in the repo run on Vitest (vitest run) and live next to the code they cover as *.test.ts — for example the route handler tests under packages/apis/src/finance/**.
// Illustrative pure logic — no I/O, fully testable
export class Workspace {
  private constructor(
    public readonly id: string,
    public name: string,
    public status: 'active' | 'archived',
    public memberLimit: number
  ) {}

  static create(data: CreateWorkspaceData): Workspace {
    if (data.name.length < 3) {
      throw new Error('Workspace name must be at least 3 characters');
    }

    return new Workspace(
      uuid(),
      data.name,
      'active',
      data.memberLimit || 10
    );
  }

  archive() {
    if (this.status === 'archived') {
      throw new Error('Workspace already archived');
    }
    this.status = 'archived';
  }

  addMember() {
    // Business rule: enforce member limit
    if (this.getMemberCount() >= this.memberLimit) {
      throw new Error('Member limit reached');
    }
    // ... add member logic
  }
}

// Test - no infrastructure required (Vitest, colocated *.test.ts)
describe('Workspace', () => {
  describe('create', () => {
    it('creates workspace with valid name', () => {
      const workspace = Workspace.create({
        name: 'My Workspace',
        memberLimit: 10
      });

      expect(workspace.name).toBe('My Workspace');
      expect(workspace.status).toBe('active');
      expect(workspace.memberLimit).toBe(10);
    });

    it('rejects workspace with short name', () => {
      expect(() => {
        Workspace.create({ name: 'ab', memberLimit: 10 });
      }).toThrow('Workspace name must be at least 3 characters');
    });
  });

  describe('archive', () => {
    it('archives active workspace', () => {
      const workspace = Workspace.create({ name: 'Test', memberLimit: 10 });

      workspace.archive();

      expect(workspace.status).toBe('archived');
    });

    it('rejects archiving already archived workspace', () => {
      const workspace = Workspace.create({ name: 'Test', memberLimit: 10 });
      workspace.archive();

      expect(() => workspace.archive()).toThrow('Workspace already archived');
    });
  });
});

// Tests run in <1ms each, no database required

Clarifying Additions

Domain logic is easy to verify because it avoids external dependencies. Tests focus purely on business rules without the complexity of infrastructure setup. Tests remain fast and reliable. Unit tests execute in milliseconds, enabling tight feedback loops and encouraging developers to run tests frequently. This leads to strong coverage of critical business rules. Because tests are easy to write, developers write more of them, leading to better quality and fewer bugs.

2. Adapter Design Enables Test Doubles

Architectural Choice

All external dependencies are accessed through interfaces (ports), allowing test implementations (test doubles) to be easily substituted during testing.

Impact and Justification

Real external systems (databases, APIs, message brokers) are problematic for testing:
  • Slow: Network calls and database queries add latency
  • Flaky: External systems can be temporarily unavailable
  • Stateful: Tests can interfere with each other through shared state
  • Complex: Require extensive setup and teardown
Test doubles (in-memory implementations of interfaces) solve all these problems. They’re fast, reliable, isolated, and simple. The pattern works wherever a dependency is reachable behind an interface or an injected client.
Illustrative pattern. TaskRepository / InMemoryTaskRepository is a model, not repo code. In practice, Tuturuuu tests usually mock the Supabase client (or use Vitest mocks) at the boundary of a route handler rather than swapping a hand-written repository port. The shape below shows the underlying idea: keep the seam mockable.
// Illustrative interface seam
export interface TaskRepository {
  findById(id: string): Promise<Task | null>;
  save(task: Task): Promise<void>;
  findByWorkspace(workspaceId: string): Promise<Task[]>;
}

// Production implementation (Supabase)
export class SupabaseTaskRepository implements TaskRepository {
  // ... real database implementation
}

// Test implementation (in-memory)
export class InMemoryTaskRepository implements TaskRepository {
  private tasks = new Map<string, Task>();

  async findById(id: string): Promise<Task | null> {
    return this.tasks.get(id) || null;
  }

  async save(task: Task): Promise<void> {
    this.tasks.set(task.id, task);
  }

  async findByWorkspace(workspaceId: string): Promise<Task[]> {
    return Array.from(this.tasks.values())
      .filter(task => task.workspaceId === workspaceId);
  }

  // Test helpers
  clear() {
    this.tasks.clear();
  }

  count() {
    return this.tasks.size;
  }
}

// Test using in-memory repository
describe('TaskService', () => {
  let taskRepo: InMemoryTaskRepository;
  let service: TaskService;

  beforeEach(() => {
    taskRepo = new InMemoryTaskRepository();
    service = new TaskService(taskRepo);
  });

  it('creates task', async () => {
    const task = await service.createTask({
      title: 'Test task',
      workspaceId: 'ws-123'
    });

    expect(task.title).toBe('Test task');
    expect(taskRepo.count()).toBe(1);

    // Verify task was saved
    const saved = await taskRepo.findById(task.id);
    expect(saved).toBeDefined();
  });

  it('finds tasks by workspace', async () => {
    await service.createTask({ title: 'Task 1', workspaceId: 'ws-1' });
    await service.createTask({ title: 'Task 2', workspaceId: 'ws-1' });
    await service.createTask({ title: 'Task 3', workspaceId: 'ws-2' });

    const tasks = await taskRepo.findByWorkspace('ws-1');

    expect(tasks).toHaveLength(2);
  });
});

Clarifying Additions

External interactions can be simulated cleanly. Test doubles provide predictable, controllable implementations of external systems. Testing becomes predictable and thorough. Tests aren’t affected by external system availability or performance, leading to consistent, reliable test suites. Complex conditions are easier to replicate. Error scenarios (network failures, database constraints) can be easily simulated with test doubles that would be difficult to reproduce with real systems.

3. Contract Testing for Microservices

Architectural Choice

Service boundaries are verified through contract tests that ensure APIs and event schemas remain compatible as services evolve independently.

Impact and Justification

In a microservices architecture, services evolve independently. A breaking change to a service’s API can silently break consumers without the producer knowing until runtime. Traditional integration tests that spin up all services are slow, complex, and brittle. Contract testing solves this by verifying that:
  1. Producer honors the contract (API/event schema) that consumers expect
  2. Consumer can handle the responses/events that producers emit
This catches breaking changes early (at build time) without requiring full integration test environments.
Illustrative pattern. There is no packages/apis/src/contracts/workspace.contract.ts, and Tuturuuu does not run formal producer/consumer contract tests today. packages/apis (@tuturuuu/apis) holds REST route handlers (e.g. src/finance/debts/route.ts) with colocated *.test.ts files — not contract schemas. The closest real practice is validating request/response payloads with Zod inside route handlers and asserting on them in tests. The schema/contract code below is a conceptual model for how an explicit contract would look if introduced.
// Illustrative API contract definition
import { z } from 'zod';

export const WorkspaceContract = {
  getWorkspace: {
    input: z.object({
      id: z.guid()
    }),
    output: z.object({
      id: z.guid(),
      name: z.string(),
      status: z.enum(['active', 'archived']),
      memberCount: z.number(),
      createdAt: z.string().datetime()
    })
  },

  createWorkspace: {
    input: z.object({
      name: z.string().min(3),
      memberLimit: z.number().optional()
    }),
    output: z.object({
      id: z.guid(),
      name: z.string(),
      status: z.literal('active'),
      memberLimit: z.number()
    })
  }
};

// Producer test - verify service honors contract
describe('WorkspaceAPI - Producer Contract', () => {
  it('getWorkspace returns valid response', async () => {
    const workspace = await createTestWorkspace();

    const response = await api.getWorkspace({ id: workspace.id });

    // Verify response matches contract
    expect(() => {
      WorkspaceContract.getWorkspace.output.parse(response);
    }).not.toThrow();
  });

  it('createWorkspace accepts valid input and returns valid output', async () => {
    const input = { name: 'Test Workspace', memberLimit: 20 };

    // Verify input is valid
    expect(() => {
      WorkspaceContract.createWorkspace.input.parse(input);
    }).not.toThrow();

    const response = await api.createWorkspace(input);

    // Verify output matches contract
    expect(() => {
      WorkspaceContract.createWorkspace.output.parse(response);
    }).not.toThrow();
  });
});

// Consumer test - verify consumer can handle producer responses
describe('WorkspaceAPI - Consumer Contract', () => {
  it('handles workspace response', () => {
    // Mock response that matches contract
    const mockResponse = {
      id: '123e4567-e89b-12d3-a456-426614174000',
      name: 'My Workspace',
      status: 'active' as const,
      memberCount: 5,
      createdAt: '2024-11-17T00:00:00Z'
    };

    // Consumer code should handle this
    expect(() => {
      const workspace = parseWorkspaceResponse(mockResponse);
      expect(workspace.name).toBe('My Workspace');
    }).not.toThrow();
  });
});
Event contract testing:
// Event schema contract
export const WorkspaceEvents = {
  'workspace.created': z.object({
    workspaceId: z.guid(),
    name: z.string(),
    ownerId: z.guid(),
    createdAt: z.string().datetime()
  }),

  'workspace.member.added': z.object({
    workspaceId: z.guid(),
    userId: z.guid(),
    role: z.enum(['owner', 'admin', 'member']),
    addedAt: z.string().datetime()
  })
};

// Producer test
it('emits valid workspace.created event', async () => {
  const event = await captureEvent('workspace.created', () => {
    return createWorkspace({ name: 'Test' });
  });

  // Verify event matches schema
  expect(() => {
    WorkspaceEvents['workspace.created'].parse(event);
  }).not.toThrow();
});

// Consumer test
it('handles workspace.created event', async () => {
  const mockEvent = {
    workspaceId: '123e4567-e89b-12d3-a456-426614174000',
    name: 'Test Workspace',
    ownerId: '234e5678-e89b-12d3-a456-426614174000',
    createdAt: '2024-11-17T00:00:00Z'
  };

  // Consumer should handle valid events
  expect(async () => {
    await handleWorkspaceCreated(mockEvent);
  }).not.toThrow();
});

Clarifying Additions

Service boundaries remain stable through contract checks. Breaking changes are caught by failing contract tests before code reaches production. Teams catch breaking changes early. Instead of discovering API incompatibilities in staging or production, contract tests fail during CI/CD. Collaboration between services stays reliable. Contract tests act as executable documentation of service interfaces, ensuring compatibility as teams work independently.

4. Event-Driven Systems Support Replay Testing

Architectural Choice

The event-driven architecture preserves event history, enabling powerful testing techniques like event replay and scenario recreation.

Impact and Justification

Traditional request-response systems are ephemeral. Once a request completes, reproducing the exact conditions that led to a bug is difficult or impossible. Debugging production issues often involves guesswork. Event replay provides a time machine for testing. Because all events are preserved, we can:
  1. Capture event sequences from production
  2. Replay them in test environments to reproduce bugs
  3. Verify that fixes resolve the issue
  4. Test edge cases by constructing specific event sequences
This makes debugging significantly more reliable and enables regression testing for complex, multi-step workflows.
Aspirational pattern — not implemented. Tuturuuu has no event store and no event-sourcing layer, so there is no eventStore.query(...) or production event replay. Background work runs as Trigger.dev v4 task() definitions (packages/trigger/src), which are individually testable but are not a replayable event log. The code below illustrates the concept of replay testing; do not treat it as an available API.
// Aspirational: capture events from an event store (not present in Tuturuuu)
export async function captureEventSequence(
  workspaceId: string,
  startTime: Date,
  endTime: Date
): Promise<DomainEvent[]> {
  return await eventStore.query({
    workspaceId,
    from: startTime,
    to: endTime,
    orderBy: 'timestamp'
  });
}

// Replay events in test environment
export async function replayEvents(events: DomainEvent[]) {
  // Reset test environment
  await resetTestDatabase();

  // Replay each event in order
  for (const event of events) {
    await eventBus.publish(event);
    await waitForEventProcessing();
  }
}

// Test by replaying production scenario
describe('Bug: Workspace members not receiving invitations', () => {
  it('reproduces production bug', async () => {
    // Capture events from production incident
    const events = await captureEventSequence(
      'ws-problematic',
      new Date('2024-11-15T14:00:00Z'),
      new Date('2024-11-15T14:05:00Z')
    );

    // Replay in test environment
    await replayEvents(events);

    // Verify bug is reproduced
    const invitations = await getInvitations('ws-problematic');
    expect(invitations).toHaveLength(0); // Bug: no invitations sent
  });

  it('verifies fix resolves the issue', async () => {
    // Apply the fix (code changes)

    // Replay same event sequence
    const events = await captureEventSequence(
      'ws-problematic',
      new Date('2024-11-15T14:00:00Z'),
      new Date('2024-11-15T14:05:00Z')
    );

    await replayEvents(events);

    // Verify fix worked
    const invitations = await getInvitations('ws-problematic');
    expect(invitations).toHaveLength(3); // Fix: invitations sent correctly
  });
});

// Test edge cases by constructing event sequences
describe('Workspace state transitions', () => {
  it('handles rapid member additions and removals', async () => {
    const events = [
      createEvent('workspace.created', { id: 'ws-1' }),
      createEvent('member.added', { userId: 'user-1' }),
      createEvent('member.added', { userId: 'user-2' }),
      createEvent('member.removed', { userId: 'user-1' }),
      createEvent('member.added', { userId: 'user-3' }),
      createEvent('member.removed', { userId: 'user-2' }),
    ];

    await replayEvents(events);

    const workspace = await getWorkspace('ws-1');
    expect(workspace.members).toHaveLength(1);
    expect(workspace.members[0].userId).toBe('user-3');
  });
});

Clarifying Additions

Capturing event sequences makes reproducing issues easier. Production bugs can be reliably recreated in test environments by replaying the exact sequence of events that led to the failure. Historical flows can be studied and validated. Teams can analyze how the system responded to past events and verify that fixes prevent recurrence. This strengthens debugging across distributed processes. Complex, multi-service workflows can be tested end-to-end by replaying events, making it easier to verify system behavior.

5. Independent CI Pipelines

Architectural Choice

Each microservice has its own independent CI/CD pipeline that runs tests, builds, and deploys the service without coordinating with other services.

Impact and Justification

In a monolithic architecture, all tests run in a single, long CI pipeline. A failure in any part of the system blocks the entire deployment, and slow tests create bottlenecks. Independent CI pipelines for each service provide:
  • Parallelism: All services test simultaneously, dramatically reducing feedback time
  • Isolation: Failures in one service don’t block others from deploying
  • Focus: Teams see only their service’s test results, reducing noise
  • Speed: Smaller test suites run faster than monolithic suites
In Tuturuuu today: there are no web-ci.yml / finance-ci.yml per-app pipeline files. Instead, shared workflows test the whole graph with Turborepo’s affected-package filtering, and per-app Vercel preview workflows handle build/deploy. Real workflows under .github/workflows/ include:
  • turbo-unit-tests.yaml — runs unit tests across affected workspaces
  • type-check.yaml — type checking
  • biome-check.yaml — lint/format (Tuturuuu uses Biome, not ESLint/Prettier)
  • ci-check.yml — validates CI configuration
  • vercel-preview-finance.yaml, vercel-preview-calendar.yaml, vercel-preview-drive.yaml, … — one preview-deploy workflow per app
  • rust-backend.yml — builds/tests apps/backend
The independence comes from Turborepo only running tasks for changed packages plus separate Vercel projects per app, rather than from one CI file per service.
# How affected work is scoped locally and in CI
bun turbo run test --filter=@tuturuuu/web
bun turbo run build --filter=@tuturuuu/finance
Turborepo task configuration. The repo runs Turborepo 2.x, which uses the tasks key (the older pipeline key was removed in v2):
// turbo.json (real key is "tasks", not "pipeline")
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "test": {
      "cache": true
    },
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"],
      "cache": true
    }
  }
}

Clarifying Additions

Tests run in parallel without interfering with each other. Multiple teams can have their tests running simultaneously, reducing wait times. Failures stay localized to the affected service. A failing test in the finance service doesn’t prevent the web app from deploying. Delivery becomes smoother and less risky. Smaller, focused deployments are easier to verify and roll back if needed.

Deployability

Deployability determines how easily and safely we can release changes to production. Good deployability enables frequent, low-risk deployments.

1. Independent Deployment Pipelines

Architectural Choice

Each microservice is independently deployable with its own release cycle, allowing teams to ship updates without coordinating across the organization.

Impact and Justification

Monolithic architectures force all-or-nothing deployments. Every change, no matter how small or isolated, requires rebuilding and redeploying the entire application. This creates:
  • Deployment bottlenecks: Teams must coordinate release windows
  • High-risk releases: Large deployments contain many changes, increasing failure probability
  • Slow feedback: Small fixes take days to reach production
  • Deployment fear: Teams avoid deploying due to coordination overhead
Independent deployment eliminates these problems. Each service ships on its own schedule:
  • Fast iteration: Deploy bug fixes minutes after they’re written
  • Low risk: Small, focused deployments are easier to verify and roll back
  • Team autonomy: No cross-team coordination required for deployments
  • Continuous delivery: Deploy multiple times per day without fear
In Tuturuuu today: there is no bun deploy:web / bun deploy:finance / bun deploy:calendar command. Each app is a separate Vercel project, driven by its own GitHub Actions workflow, so deploys are already decoupled per app:
.github/workflows/
├── vercel-preview-finance.yaml    // Finance app preview/deploy
├── vercel-preview-calendar.yaml   // Calendar app preview/deploy
├── vercel-preview-drive.yaml      // Drive app preview/deploy
├── vercel-preview-apps.yaml       // Shared preview orchestration
└── rust-backend.yml               // apps/backend (Rust) build + deploy
Each workflow builds only the shared dependencies that app needs (via Turborepo --filter) and ships that one Vercel project. Deploying finance does not rebuild or redeploy web or calendar.
The Rust apps/backend (port 7820) deploys on its own track (rust-backend.yml), independent of the Vercel front-ends — another axis of deployment independence introduced by the migration.

Clarifying Additions

Teams ship updates without affecting others. Finance deployments don’t trigger web redeployments or risk breaking calendar functionality. Releases become smaller and less risky. Instead of deploying 50 changes from 10 teams, each team deploys 5 changes independently. This improves overall delivery speed. Removing coordination overhead and deployment fear leads to more frequent, confident releases.

2. Controlled Rollout Strategies Through Architectural Boundaries

Architectural Choice

Service boundaries enable granular deployment strategies like canary releases, blue-green deployments, and feature flags, allowing changes to be introduced gradually.

Impact and Justification

Even with small deployments, risk remains. A bug can slip through testing and impact production users. Controlled rollout strategies mitigate this risk by:
  • Gradual exposure: New versions serve only a small percentage of traffic initially
  • Monitoring: Observe error rates and performance before full rollout
  • Quick rollback: Instantly revert to previous version if issues detected
  • Confidence: Deploy with confidence knowing failures impact minimal users
Service boundaries make these strategies practical because each service can have independent routing and versioning.
Mixed: real platform capability + illustrative code. Per-app Vercel projects give Tuturuuu atomic, instantly-revertible deployments and preview URLs per change — that part is real. However, Tuturuuu does not ship the featureFlags.get/enable(...) service or the switchTraffic('blue' -> 'green') / supabase.rpc('run_migration') orchestration shown below; those are conceptual illustrations of canary, feature-flag, and blue-green strategies, not repo APIs. (Tuturuuu’s Supabase migrations are plain SQL applied via the migration workflow, not an RPC call.)
// Conceptual: canary deployment (Vercel handles atomic deploy + instant rollback)
// New version serves 10% of traffic initially

// vercel.json
{
  "name": "tuturuuu-finance",

  // Vercel automatically does canary deployments
  // 1. Deploy new version
  // 2. Route 10% traffic to new version
  // 3. Monitor error rates
  // 4. Gradually increase to 100% if healthy
  // 5. Automatic rollback if error rates spike
}

// Feature flags for gradual feature rollout
export async function getFinanceFeatures(workspaceId: string) {
  const flags = await featureFlags.get(workspaceId);

  return {
    advancedReports: flags.has('finance.advanced-reports'),
    aiForecasting: flags.has('finance.ai-forecasting'),
    multiCurrency: flags.has('finance.multi-currency'),
  };
}

// Enable feature for specific workspaces first
await featureFlags.enable('finance.ai-forecasting', {
  workspaces: ['ws-beta-1', 'ws-beta-2'] // Beta test
});

// Monitor performance and errors
const metrics = await monitorFeature('finance.ai-forecasting', '24h');

if (metrics.errorRate < 0.01 && metrics.userSatisfaction > 0.8) {
  // Gradually roll out to more users
  await featureFlags.enable('finance.ai-forecasting', {
    percentage: 50 // 50% of all workspaces
  });
}

// Full rollout after validation
await featureFlags.enable('finance.ai-forecasting', {
  percentage: 100 // Everyone
});
Blue-green deployment (conceptual snippet). Tuturuuu’s self-hosted Docker deploy does use a real blue/green release watcher (snapshot + cutover) for the web app, but it is driven by deployment scripts, not the switchTraffic(...) / supabase.rpc(...) calls shown here. Read this as the idea, not the API:
// Run migrations on "green" environment
// Switch traffic after validation

// 1. Green environment: Apply migration
await supabase.rpc('run_migration', {
  migration: '20241117_add_ai_forecasting'
});

// 2. Verify green environment health
const healthy = await verifyGreenEnvironment();

if (healthy) {
  // 3. Switch traffic from blue to green
  await switchTraffic('blue' -> 'green');

  // 4. Monitor for issues
  setTimeout(() => {
    if (metricsLookGood()) {
      // 5. Destroy blue environment
      destroyBlueEnvironment();
    } else {
      // Rollback: Switch back to blue
      switchTraffic('green' -> 'blue');
    }
  }, 3600000); // Monitor for 1 hour
}

Clarifying Additions

New versions introduce changes gradually instead of all at once. Risk is minimized by limiting exposure to a small percentage of users or specific cohorts. Issues surface early in controlled conditions. Problems are detected when only 10% of users are affected, allowing quick rollback before widespread impact. This improves deployment safety. Teams deploy confidently knowing that issues will be caught early and can be mitigated quickly.

3. Architecture-Supported Environment Consistency

Architectural Choice

Infrastructure as Code (IaC) and containerization ensure that development, staging, and production environments are consistent and reproducible.

Impact and Justification

The classic “works on my machine” problem stems from environment inconsistencies. Code behaves differently in production than in development due to different:
  • Dependencies and versions
  • Environment variables and configuration
  • Infrastructure and scaling behavior
  • Operating systems and system libraries
Architecture-supported environment consistency eliminates this by:
  • Defining environments as code that can be version-controlled
  • Using containers to ensure identical runtime across environments
  • Automating setup to prevent manual configuration drift
  • Validating consistency through automated checks
In Tuturuuu - Environment consistency:
// .env.example - template for all environments
// Developers, staging, and production use same variables

NEXT_PUBLIC_SUPABASE_URL=
SUPABASE_SECRET_KEY=
TRIGGER_API_KEY=
DATABASE_URL=

// Environment-specific .env files
// .env.local (development)
// .env.staging (staging)
// .env.production (production)

// All have same structure, different values
Vercel environment configuration:
// vercel.json ensures consistency
{
  "env": {
    // Required in all environments
    "NEXT_PUBLIC_SUPABASE_URL": "@supabase-url",
    "SUPABASE_SECRET_KEY": "@supabase-service-key"
  },

  "build": {
    "env": {
      // Build-time variables. The platform version is owned by Release Please
      // and read from TUTURUUU_PLATFORM_VERSION in
      // packages/utils/src/platform-release.ts — do not hard-code a literal here.
      "NEXT_PUBLIC_APP_VERSION": "$TUTURUUU_PLATFORM_VERSION"
    }
  }
}
Pinned toolchain ensures runtime consistency. The repo pins its toolchain so every environment resolves identical versions: packageManager is bun@1.3.14, and Node engines require >=22. A container image would mirror those pins:
# Illustrative Dockerfile — keep versions aligned with package.json pins
FROM oven/bun:1.3.14 as base

WORKDIR /app

# Install dependencies (frozen lockfile => same versions everywhere)
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

COPY . .
RUN bun run build

# Production image (identical across environments)
FROM oven/bun:1.3.14-slim
WORKDIR /app
COPY --from=base /app/.next ./.next
COPY --from=base /app/node_modules ./node_modules
COPY --from=base /app/package.json ./

CMD ["bun", "run", "start"]
Supabase local development matches production. Migrations are written once and applied identically; type generation keeps types in sync:
# Start local Supabase (mirrors the production schema)
bun sb:start

# Author and apply migrations locally
bun sb:new      # scaffold a new migration
bun sb:up       # apply migrations to the local database

# Regenerate DB types after schema changes
bun sb:typegen
Production migrations are applied by a maintainer, not from agent/CI prose. Do not run bun sb:push / bun sb:linkpush from this workflow — prepare the migration and let the owner apply it.

Clarifying Additions

Environments remain aligned through structured definitions. Configuration as code prevents drift between development, staging, and production. Deployment steps become predictable across stages. The same deployment process works identically in all environments, reducing deployment-specific issues. This reduces surprises when promoting changes. Code that works in staging will work in production because environments are consistent.

Quality Attributes Summary

The table separates what is implemented today from patterns presented as teaching models / aspirational so the mechanisms aren’t read as all-adopted.
AttributeImplemented todayIllustrative / aspirationalPrimary Benefit
MaintainabilityApp + feature-folder boundaries, shared Supabase + @tuturuuu/types/db, shared packages/internal-api / packages/apis, modular Next.js frontend, per-app Vercel projectsHexagonal ports/adapters, repository interfaces, technology-swap seamsSustainable long-term evolution
TestabilityVitest unit + colocated *.test.ts, Zod payload validation, Turborepo affected-package CIPure domain objects, in-memory test doubles, formal contract tests, event replayHigh confidence in changes
DeployabilityPer-app Vercel preview/deploy workflows, separate Rust apps/backend track, pinned toolchain (bun@1.3.14, Node >=22), Supabase migrations + typegenCanary/blue-green orchestration scripts, custom feature-flag serviceFrequent, low-risk releases
These properties are being preserved through the apps/webapps/tanstack-web + Rust apps/backend migration. See TanStack + Rust Migration.