Skip to main content
This document explains four foundational architectural decisions that inform Tuturuuu’s system design. Each section explores the rationale, detailed justification, and the alternatives considered.
Conceptual vs. implemented. This page teaches the reasoning behind several classic patterns (microservices, event-driven, hexagonal, modular monolith). Tuturuuu adopts some of these in pragmatic, partial form and deliberately does not adopt others. Each “In Tuturuuu” callout below states what is actually true of the codebase today; clearly labelled examples are illustrative and may not map to a real file path. Trust the code over any architectural prose.
Active migration. The legacy apps/web Next.js runtime (port 7803) is being replaced by apps/tanstack-web (TanStack Start) plus apps/backend (a Rust API runtime on port 7820). The “React Modular Monolith” and “tRPC API Gateway” framing on this page describes the legacy apps/web stack that is being superseded. See TanStack Start And Rust Migration for the migration contract.

1. Decomposition Into Independently Deployable Apps

Tuturuuu is organized as a Turborepo monorepo containing many independently deployable applications under apps/* plus shared packages/*. This is not a textbook distributed-microservices system with per-service data stores and an inter-service event bus; rather, each app is a conventional deployment unit, and the apps share one Supabase project for auth and data. The microservices reasoning below explains why we split along app boundaries, with each “In Tuturuuu” callout grounded in what is actually true today.

Core Rationale

To support long-term growth by enabling organizational scaling, independent deployment, and technological flexibility.

Detailed Justification

Organizational Scaling and Team Autonomy

A monolithic architecture forces all developers to work on a single, large codebase, leading to high coordination overhead, merge conflicts, and slower development cycles as the team grows. The microservices approach aligns with Conway’s Law, allowing us to structure autonomous teams around specific business capabilities (e.g., an “Identity Team,” a “Payments Team”). Each team can develop, test, and deploy their services independently, drastically increasing agility. In Tuturuuu: the repo splits responsibility along app boundaries under apps/*:
  • apps/web (legacy main platform) and apps/tanstack-web (its replacement)
  • AI surfaces such as apps/rewise (chatbot) and apps/nova (prompt engineering)
  • Productivity surfaces such as apps/calendar, apps/tasks, and apps/meet
  • apps/finance for finance workflows
  • apps/backend (Rust API runtime) plus apps/database (Supabase migrations) and shared packages/*
These are deployment and ownership boundaries within a single monorepo, not fully isolated services with private databases.

Independent Scalability

In a monolith, if one feature (e.g., a real-time data processing endpoint) experiences high load, the entire application must be scaled. This is inefficient and costly. Separate deployment units allow for granular scaling. A high-load app can be scaled independently of a low-traffic one, optimizing resource utilization instead of scaling one monolith as a whole. Illustrative only (not a real config in the repo):
// Conceptual per-app scaling targets. Each Tuturuuu app deploys independently,
// so it can scale on its own rather than as part of one bundle.
{
  "web": { "min": 2, "max": 100 },   // Main app needs high availability
  "rewise": { "min": 1, "max": 20 }, // AI chat can scale separately
  "finance": { "min": 1, "max": 10 } // Finance moderate scaling
}

Fault Isolation and Resilience

A critical bug or memory leak in a non-essential module of a monolith can bring down the entire application. In a microservices architecture, a failure is contained within the boundary of that service. A crash in the ReportingService will not affect critical user-facing services like AuthenticationService or OrderService, leading to a much more resilient and available system. In Tuturuuu:
  • Because apps deploy independently, a failure in one app’s deployment does not take the others down with it
  • Supabase Auth runs as a managed service, so authentication stays available even if an individual app deployment fails
  • Background jobs (Trigger.dev) run out-of-band and fail independently from user-facing requests
Note that the apps share one Supabase project, so database-level failures are a shared dependency, not fully isolated per app.

Technology Flexibility (Polyglot Architecture)

Microservices communicate over standard protocols (like events or APIs). This allows each service to be built with the technology best suited for its purpose. While most apps use TypeScript and Next.js/React, the new apps/backend runtime is written in Rust for the performance-critical API tier, and a future machine-learning service could be written in Python. This flexibility is hard to achieve in a single-language monolith and keeps the system adaptable. Current polyglot examples in Tuturuuu:
  • Most apps: TypeScript + Next.js + React (apps/web) and TanStack Start + React (apps/tanstack-web)
  • apps/backend: Rust (dedicated API runtime, port 7820; see TanStack Start And Rust Migration)
  • apps/discord: Python (Discord bot utilities)
  • Database: PostgreSQL (via Supabase)
  • Background jobs: TypeScript on Trigger.dev v4 (packages/trigger)
  • Possible future: Python ML services

Why Not a Monolith?

While a monolith offers simplicity at the start of a project, it becomes a significant impediment to growth:
Monolith LimitationImpactMicroservices Solution
Tight CouplingChanges ripple across the entire codebaseService boundaries isolate changes
Slow DeploymentAll-or-nothing deploys, high-risk releasesIndependent, low-risk deployments
All-or-Nothing ScalingWasteful resource allocationGranular, cost-effective scaling
Technological RigidityLocked into initial technology choicesFreedom to choose best-fit technologies
Team Coordination OverheadMerge conflicts, slow code reviewsAutonomous team workflows
A monolith is unsuitable for a complex, long-lived enterprise system designed for agility.

2. Asynchronous Background Work (Event-Driven Patterns)

Long-running and side-effecting work is pushed off the request path and into Trigger.dev v4 background tasks (packages/trigger, @trigger.dev/sdk ^4.4.5). Tuturuuu does not run a general-purpose inter-service event bus where every app publishes and subscribes to a shared broker; instead, it uses Trigger.dev task() definitions that are invoked explicitly (or on a schedule) to do durable, retryable async work. The event-driven reasoning below explains why async, decoupled background work is valuable, and the examples use the real v4 task() API.

Core Rationale

To achieve decoupling between request handling and durable background work, which is the foundation for a system that stays resilient, scalable, and extensible under load.

Detailed Justification

Ultimate Decoupling

If a request handler does its slow work inline, it must know about and wait on every downstream side effect (email, indexing, analytics). That tightly couples the user-facing path to all of them. By offloading that work to a background task, the request handler only needs to enqueue the work and return. The task owns the side effects and can be changed, retried, or extended without touching the request handler. Real v4 task definition (packages/trigger/src/schedule-tasks.ts):
import { task } from '@trigger.dev/sdk/v3';
import { schedulableTasksHelper } from './schedule-tasks-helper';

// A durable, retryable background task with its own queue/concurrency limit.
export const scheduleTask = task({
  id: 'schedule-task',
  queue: { concurrencyLimit: 10 },
  run: async (payload: { ws_id: string }) => {
    const result = await schedulableTasksHelper(payload.ws_id);
    if (!result.success) {
      throw new Error(result.error || 'Schedule tasks failed');
    }
    return { ws_id: payload.ws_id, ...result, success: true };
  },
});
The request handler enqueues this with scheduleTask.trigger({ ws_id }) and returns immediately; the task runs out-of-band.

Inherent Resilience and Asynchronicity

Synchronous API calls create “chains of failure.” If a downstream service is slow or unavailable, the upstream caller is blocked, and the failure can cascade, bringing down the entire user request. With background tasks, Trigger.dev acts as a durable buffer. If a downstream dependency is slow or temporarily down, the task can retry without blocking the original request, and the user-facing response is unaffected. In Tuturuuu:
  • A request can complete even if a downstream side effect (e.g. an email send) is temporarily failing
  • Tasks are retried automatically (Trigger.dev manages retries and backoff)
  • Failed runs are visible in the Trigger.dev dashboard for investigation
  • A failing background task does not cascade into the user-facing request path

Natural Scalability and Load Leveling

A synchronous API can be overwhelmed by sudden traffic spikes. Background tasks act as a shock absorber. Trigger.dev queues bursts of task runs and processes them at a sustainable rate, governed by per-task queue.concurrencyLimit. Scaling is a matter of letting more runs execute in parallel rather than overwhelming a synchronous endpoint. Illustrative v4 task (patterned on packages/trigger/src):
import { task } from '@trigger.dev/sdk/v3';

// During a burst, many onboarding runs are enqueued; the concurrency limit
// keeps execution at a sustainable rate.
export const onboardUserTask = task({
  id: 'onboard-user',
  queue: { concurrencyLimit: 25 },
  run: async (payload: { userId: string; email: string; wsId: string }) => {
    await sendWelcomeEmail(payload.email);
    await provisionDefaultResources(payload.userId, payload.wsId);
    return { userId: payload.userId, success: true };
  },
});

// Enqueue from a request handler without blocking the response:
// await onboardUserTask.trigger({ userId, email, wsId });

Extensibility and Future-Proofing

This is a key strategic advantage. EDA allows for new business capabilities to be added by simply deploying new services that listen to existing event streams. For example, a new background task can be added to perform extra work after an existing flow, often by triggering it from the same place that already enqueues related work, without rewriting the core request handler. In Tuturuuu:
  • New async features: add a new Trigger.dev v4 task() in packages/trigger and enqueue it where needed
  • Scheduled work: use Trigger.dev scheduling (see unifiedScheduleTask / scheduleTask in packages/trigger/src) without changing request handlers
  • Heavier processing: move slow work into a task so the user-facing path stays fast

Why Not a Purely Synchronous/REST Architecture?

While synchronous APIs are excellent for user-facing queries and commands that require an immediate response (a pattern supported by our API Gateway), using them for core inter-service communication creates a brittle, tightly coupled system.
Synchronous Pattern IssueImpactEvent-Driven Solution
Tight CouplingServices must know each other’s APIsServices only know event schemas
Cascading FailuresOne slow service blocks entire chainFailures are isolated by broker
Difficult EvolutionAPI changes break all consumersNew consumers added without changes
Poor Load HandlingTraffic spikes overwhelm servicesBackground queue buffers and load-levels
Testing ComplexityMust mock all downstream side effectsTask run functions can be unit-tested in isolation
Our approach is hybrid: synchronous APIs (Next.js API routes under /api/v1, the apps/backend Rust API, and packages/internal-api helpers) handle user-facing queries and commands, while durable Trigger.dev v4 tasks handle slow or side-effecting background work. (Note: apps/web/src/trpc is a minimal stub today, not the product API gateway — see Section 4.)

3. Ports-and-Adapters Thinking (Partial Hexagonal)

Tuturuuu does not implement full hexagonal layering. There is no packages/types/src/domain directory and no apps/web/src/infrastructure/repositories layer. apps/web uses a conventional Next.js App Router structure (app, components, lib, utils, hooks, features). The code samples in this section are illustrative pseudo-code to teach the ports/adapters idea, not real file locations. Where Tuturuuu applies the spirit of this pattern, it does so through shared helper packages (packages/internal-api, packages/supabase) rather than formal domain/port interfaces.

Core Rationale

To keep business logic separable from technology dependencies where it pays off, improving maintainability and testability — applied pragmatically rather than as a strict architectural mandate.

Detailed Justification

Protection of the Domain Core

In traditional N-Tier architecture, business logic often becomes dependent on infrastructure concerns (e.g., database annotations within domain models). This couples the business rules to the technology. The Hexagonal Architecture inverts this dependency. The core business logic is pure and has zero knowledge of any external technology. It defines “Ports” (interfaces) for the functionality it needs, such as OrderRepository. Illustrative pseudo-code (no such path exists in the repo):
// Conceptual domain port (not a real file)
export interface WorkspaceRepository {
  findById(id: string): Promise<Workspace | null>;
  save(workspace: Workspace): Promise<void>;
}

// Business logic doesn't know about Supabase
export class WorkspaceService {
  constructor(private repo: WorkspaceRepository) {}

  async activateWorkspace(id: string) {
    const workspace = await this.repo.findById(id);
    workspace.activate(); // Pure domain logic
    await this.repo.save(workspace);
  }
}

Technology Agnosticism and Interchangeability

External technologies are implemented as “Adapters” that plug into the core’s ports. We have a PostgresOrderRepositoryAdapter and a KafkaEventPublisherAdapter. If we decide to migrate from PostgreSQL to MongoDB, we only need to write a new MongoOrderRepositoryAdapter and plug it in. The core business logic remains completely unchanged, making technology migrations low-risk and straightforward. Illustrative pseudo-code (no such path exists in the repo):
// Conceptual infrastructure adapter (not a real file)
export class SupabaseWorkspaceRepository implements WorkspaceRepository {
  constructor(private client: SupabaseClient) {}

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

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

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

// Easy to swap: Could create DrizzleWorkspaceRepository or PrismaWorkspaceRepository

Superior Testability

Because the domain core is isolated from the outside world, it can be tested completely without a running database, web server, or any other infrastructure. We can use simple, in-memory mock adapters to test complex business rules. This results in tests that are extremely fast, reliable, and easy to write, leading to higher code quality. Illustrative pseudo-code (conceptual test, not a real file):
class InMemoryWorkspaceRepository implements WorkspaceRepository {
  private workspaces = new Map<string, Workspace>();

  async findById(id: string) {
    return this.workspaces.get(id) || null;
  }

  async save(workspace: Workspace) {
    this.workspaces.set(workspace.id, workspace);
  }
}

describe('WorkspaceService', () => {
  it('activates workspace', async () => {
    const repo = new InMemoryWorkspaceRepository();
    const service = new WorkspaceService(repo);

    // No database, no network calls - tests run in milliseconds
    await service.activateWorkspace('ws-123');

    const workspace = await repo.findById('ws-123');
    expect(workspace.isActive).toBe(true);
  });
});

Why Not a Traditional Layered/N-Tier Architecture?

The primary weakness of the traditional N-Tier architecture is its tendency to create leaky abstractions and tight coupling between layers.
N-Tier LimitationImpactHexagonal Solution
Business Logic LeaksDomain models have ORM annotationsPure domain models, infrastructure separate
Tight CouplingLayers depend on concrete implementationsLayers depend on abstractions (ports)
Hard to TestTests require full infrastructureMock adapters, fast unit tests
Technology Lock-inChanging DB means rewriting domainSwap adapters, domain unchanged
Unclear Boundaries”Service” and “Repository” blur togetherClear separation: domain, application, infrastructure
Business logic often becomes intertwined with persistence logic, making the system rigid, difficult to test in isolation, and hard to adapt to new technology requirements. The Hexagonal Architecture solves this by enforcing a strict, clean boundary around the application’s core.

4. A React Modular Monolith & Headless UI

The apps/web frontend is architected as a “Modular Monolith” using React, with reusable logic factored into hooks following a “Headless UI” mindset.
This decision describes the legacy apps/web stack. It is being superseded by apps/tanstack-web (TanStack Start + TanStack Router/Query) for the frontend and apps/backend (Rust) for the API tier. The modular-monolith and headless-UI principles below still inform the new frontend, but treat apps/web-specific references as legacy. See TanStack Start And Rust Migration.

Core Rationale

To balance the agility of a single codebase with the maintainability of a modular design, while ensuring long-term flexibility in presentation.

Detailed Justification

Maintainability at Scale (Modular Monolith)

A standard Single-Page Application (SPA) can quickly become a “big ball of mud” as it grows. A modular monolith enforces logical boundaries between different features or domains (e.g., Authentication, Dashboard, Settings, Finance) within a single codebase. This improves code organization, reduces unintended coupling, and allows teams to work on different features with fewer conflicts. In Tuturuuu:
apps/web/src/
├── app/
│   ├── [locale]/(dashboard)/[wsId]/
│   │   ├── finance/          # Finance module
│   │   ├── calendar/         # Calendar module
│   │   ├── tasks/            # Tasks module
│   │   └── settings/         # Settings module
├── components/
│   ├── finance/              # Finance-specific components
│   ├── calendar/             # Calendar-specific components
│   └── shared/               # Shared components
Each module is self-contained with clear boundaries, yet benefits from shared infrastructure.

Module Size Guardrail

Code that crosses 400 LOC per file or 200 LOC per component/widget should be split automatically before final verification. Prefer focused route modules, cards, panels, hooks, data loaders, and helper modules over monolithic files. When other modules already import an entrypoint, keep that entrypoint as a thin barrel re-export so the refactor improves maintainability without forcing unrelated import churn.

UI and Logic Separation (Headless UI)

In this pattern, React components are split into two parts:
  1. A “headless” hook that manages all logic, state, and accessibility (e.g., useUserDropdown)
  2. A presentation component that simply renders the UI based on the state provided by the hook
This clean separation allows us to completely change the look and feel (the “head”) of a component by swapping out its presentation layer, without rewriting any of the complex business logic. Example from Tuturuuu:
// Headless logic hook (packages/ui/src/hooks/useTaskList.ts)
export function useTaskList(boardId: string) {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  const addTask = useCallback((task: Task) => {
    setTasks(prev => [...prev, task]);
  }, []);

  const removeTask = useCallback((id: string) => {
    setTasks(prev => prev.filter(t => t.id !== id));
  }, []);

  // All logic, accessibility, keyboard shortcuts
  return { tasks, isLoading, addTask, removeTask };
}

// Presentation component (apps/web/src/components/tasks/TaskList.tsx)
export function TaskList({ boardId }: Props) {
  const { tasks, isLoading, addTask, removeTask } = useTaskList(boardId);

  // Only UI rendering, no logic
  return (
    <div className="task-list">
      {tasks.map(task => (
        <TaskCard key={task.id} task={task} onRemove={removeTask} />
      ))}
    </div>
  );
}

Future-Proofing for Multiple Platforms

The headless hooks are pure application logic. This means they can be reused to power completely different UIs. The same useUserProfile hook that powers a web application could later be used to power a React Native mobile app or even a command-line interface, providing maximum code reuse and flexibility. Future possibilities:
// Same hook, different presentations
import { useTaskList } from '@tuturuuu/hooks';

// Web app (current)
<WebTaskList boardId={id} />

// Future: Mobile app
<MobileTaskList boardId={id} />

// Future: Desktop app (Electron/Tauri)
<DesktopTaskList boardId={id} />

// All use the same useTaskList() logic

Why Not Other Frontend Architectures?

Micro-frontends

While powerful, this architecture introduces significant complexity in:
  • Build tooling and module federation
  • Deployment pipelines and version coordination
  • Routing and state management across boundaries
  • Performance overhead of loading multiple bundles
The modular monolith provides many of the same organizational benefits with a fraction of the operational overhead, making it a more pragmatic starting point.

Traditional Coupled (Server-Rendered) Frontends

These architectures tightly couple the frontend presentation to the backend logic, making it difficult to create modern, rich user experiences.
Server-Rendered LimitationImpactReact Modular Monolith Solution
Tight Backend CouplingFrontend changes require backend deploysClean API separation via /api/v1 routes and packages/internal-api
Limited InteractivityComplex UIs are difficultFull React capabilities
Poor Multi-Client SupportCan’t easily support mobileReusable hooks and shared packages/*
Slow IterationFull page reloads, slower developmentFast HMR, component-level updates
They do not provide a clean API separation and are not flexible enough to support multiple client types (e.g., web and mobile) from a single backend.
On the API gateway. Earlier system-design pages referred to a tRPC API gateway. In the current codebase, apps/web/src/trpc is only a minimal stub: its router exports a single healthCheck procedure and its context is a hardcoded placeholder. There are no workspace/user/tasks/AI routers and no auth middleware there. Product data actually flows through packages/internal-api helpers and REST /api/v1 routes (and, increasingly, the apps/backend Rust API). A repo guard also forbids /trpc calls from apps/tanstack-web/src.

Architectural Advantages and Drawbacks

The chosen architecture provides significant advantages but also introduces tradeoffs that must be managed. Understanding both sides enables informed decision-making.
For a comprehensive comparison of all architectural patterns (N-Tier, Hexagonal, Clean, Onion, Monolithic, Modular Monolith, Microservices, Event-Driven) with detailed pros and cons, see Architectural Patterns Comparison.

Four Key Advantages

1. Superior and Enduring Maintainability through Strong Architectural Boundaries

Architectural Choice: The architecture deliberately enforces separation of concerns at two critical levels: between business domains using Microservices, and between business logic and technology using Hexagonal Architecture. Impact and Justification: This layered approach to separation is crucial for long-term project health. At the macro level, isolating business domains like “Web Platform” and “Finance” into separate microservices prevents the system from degrading into a “big ball of mud” where changes in one area have unintended consequences in another. At the micro level, the Hexagonal Architecture’s strict isolation of the core domain logic ensures that the most valuable business rules are shielded from technological churn. This means we can upgrade a database, switch a messaging provider, or change a web framework with minimal, predictable impact, drastically reducing the cost and risk of maintenance over the system’s lifespan. Clarifying Additions: This structure makes responsibilities clearly defined and easier for teams to understand. Changes stay local to the affected boundary instead of leaking across the system. This reduces the likelihood of unintended side effects and simplifies long-term evolution.

2. High Organizational Agility and Independent Deployability

Architectural Choice: The Microservices architecture decomposes the system into a suite of small, autonomous services, each aligned with a specific business capability. Impact and Justification: This architectural choice is a direct enabler of organizational agility. It allows us to structure development teams to have full ownership of their respective services, from code to deployment (the “you build it, you run it” model). This autonomy eliminates the bottlenecks of a monolithic release process. A team can deploy updates to their service multiple times a day without coordinating with or waiting for other teams. This dramatically accelerates the feature delivery lifecycle and allows the organization to respond more quickly to changing business requirements. Clarifying Additions: Teams can deliver updates without waiting on other domains. This separation increases overall development speed and lowers coordination overhead. The architecture naturally supports rapid and continuous improvement.

3. Enhanced Fault Isolation and System-Wide Resilience

Architectural Choice: By decomposing the system into separate, independently running Microservices, we contain the “blast radius” of any potential failure. Impact and Justification: In any complex system, failures are inevitable. A monolithic architecture is fragile because a single critical bug (like a memory leak or an unhandled exception) in a non-essential module can bring the entire application down. Our microservices design ensures that a failure in one service—for instance, the Finance app—is completely isolated. Core services like the main Web Platform and Calendar continue to run unaffected. This transforms a potentially catastrophic failure into a manageable, localized degradation of service, leading to a far more resilient and reliable platform for end-users. Clarifying Additions: The system remains partially functional rather than failing entirely. Troubleshooting becomes simpler because issues are naturally localized. This architecture improves uptime and user experience during partial disruptions.

4. UI/Logic Separation for Multi-Platform Potential and Design Flexibility

Architectural Choice: A Headless UI pattern is employed on the frontend, cleanly separating the presentation layer (the “head”) from the application logic, state management, and accessibility hooks (the “body”). Impact and Justification: This choice provides two strategic advantages. Firstly, it offers immense design flexibility. The entire look and feel of the application can be radically redesigned by creating a new presentation layer that plugs into the existing, stable application logic, allowing for rapid rebranding or UX overhauls. Secondly, and more importantly, it makes the application logic platform-agnostic. The “headless” hooks and state management code can be reused to power completely different frontends in the future, such as a native mobile application or a voice-activated interface, maximizing code reuse and ensuring the system can adapt to new user touchpoints. Clarifying Additions: Changes to visual design do not risk breaking functional behavior. Functional logic can be reused across new user interfaces with minimal changes. This future-proofs the frontend against new device types or presentation styles.

Four Key Drawbacks

1. Significant Inherent Operational Complexity

Architectural Choice: A distributed Microservices architecture is not a single application but a system of systems, requiring a sophisticated support infrastructure including Service Discovery, an API Gateway, and centralized observability. Impact and Justification: This introduces a steep increase in operational complexity compared to a monolith. The team must now manage the deployment, networking, and health of multiple independent applications. This requires specialized DevOps expertise and a robust toolchain for logging, metrics, and tracing to understand the system’s behavior. The infrastructure itself becomes a critical product that must be built and maintained, representing a significant investment of time and resources. Clarifying Additions: More moving parts require greater architectural discipline. Each service adds overhead that must be monitored and understood. Organizations must prepare for the ongoing effort required to operate a distributed system.

2. Challenges of Distributed Data Management and Consistency

Architectural Choice: The principle of decentralized data ownership in a Microservices architecture prohibits the use of traditional, simple ACID transactions across service boundaries. Impact and Justification: This forces the system to embrace Eventual Consistency. Business processes that span multiple services must be carefully designed using complex patterns like Sagas to handle failures and ensure that the system eventually reaches a consistent state. This is a fundamentally harder paradigm for developers to reason about and requires a shift in mindset away from the guarantees provided by a single relational database. Clarifying Additions: Teams must think differently about data reliability across boundaries. Data no longer updates everywhere at the same moment, which requires intentional design. This increases cognitive load when building cross-service workflows.

3. Intrinsic Complexity of Distributed System Debugging and Performance Analysis

Architectural Choice: A single user request can trigger a complex, asynchronous chain of interactions that traverses the API Gateway and fans out to multiple backend services and background jobs. Impact and Justification: When something goes wrong, diagnosing the root cause is no longer as simple as looking at a single log file or stack trace. It requires correlating logs, metrics, and traces from multiple services to piece together the full story. This necessitates a mature and well-integrated observability stack (e.g., distributed tracing with OpenTelemetry) to make debugging and performance tuning manageable. Clarifying Additions: Architectural visibility becomes essential for diagnosing the flow of requests. Failures may appear in one service even if the root cause lies elsewhere. This makes system-wide insight a core architectural requirement.

4. Governance Overhead of Maintaining Modularity

Architectural Choice: Both the backend Microservices and the frontend Modular Monolith rely on maintaining strict boundaries to be effective. Impact and Justification: This requires active architectural governance. Without discipline, developers can easily create inappropriate dependencies, turning the microservices into a distributed monolith or eroding the boundaries of the frontend modules. The architecture requires ongoing vigilance, clear standards, and automated checks (e.g., dependency analysis tools) to prevent architectural decay over time. Clarifying Additions: Teams must follow agreed-upon boundaries consistently. Regular architecture reviews help identify early signs of erosion. Maintaining clean boundaries becomes an ongoing responsibility rather than a one-time setup.

Decision Summary

DecisionAdoption in TuturuuuProblem SolvedTrade-off Accepted
Independently deployable appsAdopted (Turborepo apps/*, shared Supabase)Monolith scaling/ownership limitsIncreased operational complexity
Async background workAdopted (Trigger.dev v4 task())Tight synchronous couplingMore moving parts to observe
Ports-and-adapters thinkingPartial (shared helper packages, not formal layers)Technology lock-inN/A — applied pragmatically
React modular monolithAdopted in legacy apps/web; principles carry to apps/tanstack-webFrontend chaos at scaleDiscipline required for boundaries

Next Steps