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.
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 theFinanceService 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/finance (and the finance helpers in packages/apis/packages/internal-api) without learning the calendar surface.
Module boundaries in frontend:
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.- Supabase access is centralized behind the
@tuturuuu/supabase/next/serverfactories (createClient()andcreateDynamicClient()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-apiandpackages/apis, so the web app and its TanStack successor can reuse the same helpers as the migration proceeds.
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.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.
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
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:
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
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/**.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
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.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:- Producer honors the contract (API/event schema) that consumers expect
- Consumer can handle the responses/events that producers emit
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.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:- Capture event sequences from production
- Replay them in test environments to reproduce bugs
- Verify that fixes resolve the issue
- Test edge cases by constructing specific event sequences
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
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 workspacestype-check.yaml— type checkingbiome-check.yaml— lint/format (Tuturuuu uses Biome, not ESLint/Prettier)ci-check.yml— validates CI configurationvercel-preview-finance.yaml,vercel-preview-calendar.yaml,vercel-preview-drive.yaml, … — one preview-deploy workflow per apprust-backend.yml— builds/testsapps/backend
tasks key (the older pipeline key was removed in v2):
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
- 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
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:
--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
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.)switchTraffic(...) / supabase.rpc(...) calls shown here. Read this as the idea, not the API:
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
- 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
packageManager is bun@1.3.14, and Node engines require >=22. A container image would mirror those pins:
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.| Attribute | Implemented today | Illustrative / aspirational | Primary Benefit |
|---|---|---|---|
| Maintainability | App + feature-folder boundaries, shared Supabase + @tuturuuu/types/db, shared packages/internal-api / packages/apis, modular Next.js frontend, per-app Vercel projects | Hexagonal ports/adapters, repository interfaces, technology-swap seams | Sustainable long-term evolution |
| Testability | Vitest unit + colocated *.test.ts, Zod payload validation, Turborepo affected-package CI | Pure domain objects, in-memory test doubles, formal contract tests, event replay | High confidence in changes |
| Deployability | Per-app Vercel preview/deploy workflows, separate Rust apps/backend track, pinned toolchain (bun@1.3.14, Node >=22), Supabase migrations + typegen | Canary/blue-green orchestration scripts, custom feature-flag service | Frequent, low-risk releases |
These properties are being preserved through the
apps/web → apps/tanstack-web + Rust apps/backend migration. See TanStack + Rust Migration.Related Documentation
- Core Architectural Decisions - Why we chose this architecture
- Layering Patterns - N-tier vs Hexagonal/Clean architecture
- Hexagonal Architecture - Ports and adapters in detail
- Microservices Patterns - Service boundaries and deployment
- Event-Driven Architecture - Event streaming patterns
- Observability & Monitoring - Testing in production