We are building infrastructure that must almost never fail. To achieve this, we move fast while shipping amazing quality software with no shortcuts or compromises. This document outlines the engineering standards that will guide us through 2026 and beyond.
Team Structure
Our engineering organization consists of six core teams, each with distinct responsibilities:
Foundation Team: Focuses on establishing and maintaining coding standards and architectural patterns. This team works collaboratively with other teams to establish best practices across the organization.
Consumer, Enterprise, and Platform Teams: Product-focused teams that ship features rapidly while maintaining the quality standards set forth in this document. These teams prove that speed and quality are not mutually exclusive.
Community Team: Responsible for quickly reviewing PRs from the open source community, providing feedback, and shepherding that work through to merge. This team ensures our open source contributors have a great experience and their contributions meet our quality standards.
Our Results Thus-far
The data speaks for itself. Over the past year and a half, we have fundamentally transformed how we build software:

We've roughly doubled our engineering output while simultaneously improving quality. Even more impressive is the shift in what we're building:

We've successfully reallocated approximately 20% of engineering effort away from fixes and toward features, performance improvements, refactors, and chores. This shift demonstrates that investing in quality and architecture doesn't slow you down. It accelerates you.
Cal.com's Foundation Enables Excellence for coss.com
Cal.com is a stable, profitable business that we will continue to grow.
This success gives us a unique advantage as we build coss.com. Unlike the early days of Cal.com, where we needed to move fast to establish product-market fit and build a sustainable business, coss.com starts from a position of strength.
We don't need to rush coss.com.
Cal.com's stability means we can afford to build coss.com the right way from day one. We have the luxury of implementing these engineering standards without the pressure of immediate market demands or funding constraints. This is a fundamentally different starting position.
The "slowness" is an investment, not a cost.
Yes, following these standards might feel slower initially and might even be frustrating for some engineers. Writing DTOs takes more time than passing database types directly to the frontend. Creating proper abstractions and dependency injection requires more upfront design. Maintaining 80%+ test coverage for new code demands discipline. But this apparent slowness is temporary, and the payoff is exponential.
Consider the compounding returns...
Code that's architected correctly from the start doesn't need massive refactors later
High test coverage prevents bugs that would otherwise consume weeks of debugging and hotfixes (see 2023 to mid 2024)
Proper abstractions make adding new features dramatically faster over time
Clean boundaries and DTOs prevent the architectural erosion that eventually demands complete rewrites
The Cal.com trajectory shows what happens when you optimize for immediate velocity. High initial speed that gradually degrades as technical debt accumulates, architectural shortcuts create bottlenecks, and more time gets spent fixing problems than building features (see previous chart where we were spending 55-60% of engineering effort on fixes).
The coss.com trajectory will embrace the power of building correctly from day one. Slightly slower initial velocity while establishing proper patterns, followed by exponential acceleration as those patterns pay dividends and enable faster development with higher confidence.
Core Principles
1. No deferred quality
We'll minimize "I'll do it in a follow-up PR" for small refactors.
Follow-up PRs for minor improvements rarely materialize. Instead, they accumulate as technical debt that burdens us months or years later. If a small refactor can be done now, do it now. Follow-ups should be reserved for substantial changes that genuinely warrant separate PRs or for exceptional, urgent cases.
2. High standards in code review
Don't let PRs through with a lot of nits just to avoid being "the bad person."
This is precisely how codebases become sloppy over time. Code review is not about being nice. It's about maintaining the quality standards our infrastructure demands. Every nitpick matters. Every pattern violation matters. Address them before merging, not after.
3. Push each other to do the right thing
We hold each other accountable for quality
Cutting corners might feel faster in the moment, but it creates problems that slow everyone down later. When you see a teammate about to merge a PR with obvious issues, speak up. When someone suggests a quick hack instead of the proper solution, push back. When you're tempted to skip tests or ignore architectural patterns, expect your teammates to challenge you.
This isn't about being difficult or slowing people down
It's about collective ownership of our codebase and our reputation. Every shortcut one person takes becomes everyone's problem. Every corner cut today means more debugging sessions, more hotfixes, and more frustrated customers tomorrow.
Make it normal to challenge poor decisions, respectfully
If someone says "let's just hard-code this for now," the expected response should be "what would it take to do it the proper way the first time?" If someone wants to commit untested code, the team should push back. If someone suggests copying and pasting instead of creating a proper abstraction, call it out respectfully.
We're building something that needs to almost never fail
That level of reliability doesn't happen by accident. It happens when every engineer feels responsible for quality, not just their own code but the entire system. We succeed as a team or we fail as a team.
4. Aim for simplicity
Prioritize clarity over cleverness
The goal is code that is easy to read and understand quickly, not elegant complexity. Simple systems reduce the cognitive load for every engineer.
Ask yourself the right questions
Am I actually solving the problem at hand?
Am I thinking too much about possible future use cases?
Have I considered at least 1 other alternative for solving this? How does it compare?
Simple doesn't mean lacking in features
Just because our goal is to create simple systems, this doesn't mean they should feel anemic and lacking obvious functionality.
5. Automate everything
Leverage AI
Generate 80% of boilerplate and non-critical code using AI, allowing us to focus solely on complex business logic and critical architectures.
Build zero-noise alerting and smart error handling.
Manual testing is more and more a thing of the past. AI can quickly and intelligently build mega test suites for us.
Our CI is the final boss
Everything in this standards document is checked before code is merged in PRs
No surprises make it into main
Checks are fast and useful
Architectural Standards
We are transitioning to a strict architectural model based on Vertical Slice Architecture and Domain-Driven Design (DDD). The following patterns and principles will be enforced rigorously in PR reviews and via linting.
Vertical Slice Architecture: packages/features
Our codebase is organized by domain, not by technical layer. The packages/features
directory is the heart of this architectural approach. Each folder inside represents a complete vertical slice of the application, driven by the domain it touches.
Structure:
Each feature folder is a self-contained vertical slice that includes everything needed for that domain:
Domain logic: The core business rules and entities specific to that feature
Application services: Use case orchestration for that domain
Repositories: Data access specific to that feature's needs
DTOs: Data transfer objects for crossing boundaries
UI components: Frontend components related to this feature (where applicable)
Tests: Unit, integration, and e2e tests for this feature
Why Vertical Slices Matter
Traditional layered architecture organizes by technical concerns:
This creates several problems:
Changes to one feature require touching files scattered across multiple directories
It's hard to understand what a feature does because its code is fragmented
Teams step on each other's toes when working on different features
You can't easily extract or deprecate a feature
Vertical slice architecture organizes by domain:
This solves these problems:
Everything related to availability lives in
packages/features/availability
You can understand the entire availability feature by exploring one directory
Teams can work on different features without conflicts (if the Cal.com engineering team grows but most certainly in coss.com we will have teams take on major packages)
Features are loosely coupled and can evolve independently
Guidelines for Feature Organization
In theory, each feature is independently deployable. While we might not actually deploy them separately, organizing this way forces us to keep dependencies clear and coupling minimal. This is the premise and success of microservices, although we won't yet be deploying microservices.
Features communicate through well-defined interfaces. If bookings needs availability data, it imports from @calcom/features/availability
through exported interfaces, not by reaching into internal implementation details.
Shared code lives in appropriate places:
Domain-agnostic utilities and cross-cutting concerns (auth, logging) :
packages/lib
Shared UI primitives:
packages/ui
(and soon coss.com ui)
Domain boundaries are enforced automatically. We will build linting that prevents reaching into the internals of features where you shouldn't be allowed. If packages/features/bookings
tries to import from packages/features/availability/services/internal
, the linter will block it. All cross-feature dependencies must go through the feature's public API.


New features start as vertical slices. When building something new, create a new folder in packages/features
with the complete vertical slice. This makes it clear what you're building and keeps everything organized from day one.
Benefits
Discoverability
Looking for booking logic? It's all in
packages/features/bookings
. No need to hunt through controllers, services, repositories, and utilities scattered across the codebase.
Easier testing
Test the entire feature as a unit. You have all the pieces in one place, making integration testing natural and straightforward.
Clearer dependencies
When you see
import { getAvailability } from '@calcom/features/availability'
, you know exactly which feature you're depending on. When dependencies grow too complex, it's obvious and can be addressed.
Repository Pattern and Dependency Injection
Technology choices must not seep through the application. The Prisma problem illustrates this perfectly. We currently have references to Prisma scattered across hundreds of files. This creates massive coupling and makes technology changes prohibitively expensive. We are feeling the pain of this now upgrading Prisma to v6.16. Something that should have been just a localized refactor behind shielded repositories has been a meandering, nearly-endless chase of issues across multiple apps.
The standard going forward:
All database access must go through Repository classes. We already have a nice head start on this.
Repositories are the only code that knows about Prisma (or any other ORM). No logic should be in them.
Repositories are injected via Dependency Injection containers
If we ever switch from Prisma to Drizzle or another ORM, the only changes required are:
Repository implementations
DI container wiring for new repositories
Nothing else in the codebase should care or change
This is not theoretical. This is how we build maintainable systems.
Data Transfer Objects (DTOs)
Database types should not leak to the frontend. This has become a popular shortcut in our tech stack, but it's a code smell that creates multiple problems.
Technology coupling (Prisma types end up in React components)
Security risks (accidental leakage of sensitive fields)
Fragile contracts between server and client (this is particularly problematic as we build many more APIs)
Inability to evolve the database schema independently
All DTOs conversions through Zod, even for an API response to ensure all data is being validated before sending to user. Better to fail than return something wrong.
The standard going forward:
Create explicit DTOs at every architectural boundary.
Data layer → Application layer → API: Transform database models into application-layer DTOs, then transform application DTOs into API-specific DTOs
API → Application layer → Data layer: Transform API DTOs through application layer and into data-specific DTOs
Yes, this requires more code. Yes, it's worth it. Explicit boundaries prevent the architectural erosion that creates long-term maintenance nightmares.
Domain-Driven Design Patterns
The following patterns must be used correctly and consistently:
Application Services
Orchestrate use cases, coordinate between domain services and repositories
Domain Services
Contain business logic that doesn't naturally belong to a single entity
Repositories
Abstract data access, isolate technology choices
Dependency Injection
Enable loose coupling, facilitate testing, isolate concerns
Caching Proxies
Wrap repositories or services to add caching behavior transparently
Not the only way to do caching, of course, but a nice jump-off point
Decorators
Add cross-cutting concerns (logging, metrics, etc.) without polluting domain logic
Codebase Consistency
Our codebases should feel like one person wrote them. This level of consistency requires strict adherence to established patterns, comprehensive linting rules that enforce architectural standards code reviews that reject pattern violations + the help of CodeRabbit.
Move Conditionals to the Application Entry Point
If statements belong at the entry point, not scattered throughout your services. This is one of the most important architectural principles for maintaining clean, focused code that doesn't spiral into unmaintainable complexity.
Here's how code degrades over time: A service is written for a clear, specific purpose. The logic is clean and focused. Then a new product requirement arrives, and someone adds an if statement. A few years and several more requirements later, that service is littered with conditional checks for different scenarios. The service has become:
Complicated and hard to read
Difficult to understand and reason about
More susceptible to bugs
Violating single responsibility (handling too many different cases)
Nearly impossible to test thoroughly
The service has overstepped its bounds in terms of responsibilities and logic.
A Solution: Factory Pattern with Specialized Services
Use the factory pattern to make decisions at the entry point, then delegate to specialized services that handle their specific logic without conditionals.
Example from our codebase:
The BillingPortalServiceFactory
determines whether billing is for an organization, team, or individual user, then returns the appropriate service:
Each service then handles its specific logic without needing to check "am I an org or a team?":
Why This Matters
Services stay focused
Each service has one responsibility and doesn't need to know about other contexts. The
OrganizationBillingPortalService
doesn't contain if statements checkingif (isTeam)
orif (isUser)
. It only knows how to handle organizations.
Changes are isolated
When you need to modify organization billing logic, you only touch
OrganizationBillingPortalService
. You don't risk breaking team or user billing. You don't need to trace through nested conditionals to figure out which path your code takes.
Testing is straightforward
Test each service independently with its specific scenarios. No need to test every combination of conditionals across different contexts.
New requirements don't pollute existing code
e.g. When you need to add enterprise billing with different rules, you create
EnterpriseBillingPortalService
. The factory gains one more conditional, but existing services remain untouched and focused.
How to achieve
Push conditionals up to controllers, factories, or routing logic. Let these entry points make decisions about which service to use.
Keep services pure and focused on a single responsibility. If a service needs to check "which type am I?", you probably need multiple services.
Prefer polymorphism over conditionals
Interfaces define the contract. Concrete implementations provide the specifics.
Watch for if statement accumulation
During code review, if you see a service gaining conditionals for different scenarios, that's a signal to refactor into specialized services.
API Design: Thin Controllers and HTTP Abstraction
Controllers are thin layers that handle only HTTP concerns.
They take requests, process them, and map data to DTOs that are passed to core application logic. Moving forward, no application or core logic should be seen in API routes or tRPC handlers.
We must detach HTTP technology from our application.
The way we transfer data between client and server (whether REST, tRPC, etc.) should not influence how our core application works. HTTP is a delivery mechanism, not an architectural driver.
Controller responsibilities (and ONLY these):
Receive and validate incoming requests
Extract data from request parameters, body, headers
Transform request data into DTOs
Call appropriate application services with those DTOs
Transform application service responses into response DTOs
Return HTTP responses with proper status codes
Controllers should NOT:
Contain business logic or domain rules
Directly access databases or external services
Perform complex data transformations or calculations
Make decisions about what the application should do
Know about implementation details of the domain
Example of thin controller pattern:
API Versioning and Breaking Changes
No breaking changes. This is critical. Once an API endpoint is public, it must remain stable. Breaking changes destroy developer trust and create integration nightmares for our users.
Strategies for avoiding breaking changes:
Always add new fields as optional
Use API versioning when you must change existing behavior
Deprecate old endpoints gracefully with clear migration paths
Maintain backward compatibility for at least two major versions
When you must make breaking changes:
Create a new API version using the date-specific versioning in API v2 (perhaps we'll look into the named versioning that Stripe recently introduced as well)
Run both versions simultaneously during transition (we already do this in API v2)
Provide automated migration tools when possible
Give users ample time to migrate (minimum 6 months for public APIs)
Document exactly what changed and why
Performance and Algorithm Complexity
We build for large organizations and teams. What works fine with 10 users or 50 records can collapse under the weight of enterprise scale. Performance is not something we optimize later. It's something we build correctly from the start.
Think About Scale From Day One
When building features, always ask: "How does this behave with 1,000 users? 10,000 records? 100,000 operations?" The difference between O(n) and O(n²) algorithms might be imperceptible in development, but catastrophic in production.
Common O(n²) patterns to avoid:
Nested array iterations (
.map
inside.map
,.forEach
inside.forEach
)Array methods like
.some
,.find
, or.filter
inside loops or callbacksChecking every item against every other item without optimization
Chained filters or nested mapping over large lists
Real-world example: For 100 available slots and 50 busy periods, an O(n²) algorithm performs 5,000 checks. Scale that to 500 slots and 200 busy periods, and you're doing 100,000 operations. That's a 20x increase in computational load for only a 5x increase in data.
Choose the Right Data Structures and Algorithms
Most performance problems are solved by picking better data structures and algorithms:
Sorting + early exit: Sort your data once, then break out of loops when you know remaining items won't match
Binary search: Use binary search for lookups in sorted arrays instead of linear scans
Two-pointer techniques: For merging or intersecting sorted sequences, walk through both with pointers instead of nested loops
Hash maps/sets: Use objects or Sets for O(1) lookups instead of
.find
or.includes
on arraysInterval trees: For scheduling, availability, and range queries, use proper tree structures instead of brute-force comparison
Example transformation:
Automated Performance Checks
We will implement multiple layers of defense against performance regressions:
Linting rules that flag:
Functions with nested loops or nested array methods
Multiple nested
.some
,.find
, or.filter
callsRecursion without memoization
Known anti-patterns for our domain (scheduling, availability checks, etc.)
Performance benchmarks in CI that:
Run critical algorithms on realistic, large-scale data
Compare execution times against baseline on every PR
Block merges that introduce performance regressions
Test with enterprise-scale data (thousands of users, tens of thousands of records)
Production monitoring that:
Tracks execution time for critical paths
Alerts when algorithms slow down as data grows
Catches regressions before users notice
Provides real-world performance data to inform optimizations
Performance is a Feature
Performance is not optional. It's not something we "get to later." For enterprise customers booking across large teams, slow responses mean lost productivity and frustrated users (our experience with some larger enterprise customers can be a testament to this).
Every engineer should:
Profile your code before optimizing, but think about complexity from the start
Test with realistic, large-scale data (not just 5 test records). We have seed scripts already built. We likely need to extend.
Choose efficient algorithms and data structures upfront
Watch for nested iterations in code review
Question any algorithm that scales with the product of two variables
The NP-Hard Reality of Scheduling
Scheduling problems are fundamentally NP-hard. This means that as the number of constraints, participants, or time slots grows, the computational complexity can explode exponentially. Most optimal scheduling algorithms have worst-case exponential time complexity, making algorithm choice absolutely critical.
Real-world implications:
Finding the optimal meeting time for 10 people across 3 time zones with individual availability constraints is computationally expensive
Adding conflict detection, buffers, and a plethora of other options amplifies the problem
Poor algorithm choices that work fine for small teams become completely unusable for large organizations
What takes milliseconds for 5 users might take many seconds for organizations
Strategies for managing NP-hard complexity:
Use approximation algorithms that find "good enough" solutions quickly rather than perfect solutions slowly
Implement aggressive caching of computed schedules and availability
Pre-compute common scenarios during off-peak hours
Break large scheduling problems into smaller, more manageable chunks
Set reasonable timeout limits and fallback to simpler algorithms when needed
This is why performance isn't just a nice-to-have in scheduling software. It's the foundation that determines whether your system can scale to enterprise needs or collapses under real-world usage patterns.
Code Coverage Requirements
Global coverage tracking
We track overall codebase coverage as a key metric that improves over time. This gives us visibility into our testing maturity and helps identify areas that need attention. The global coverage percentage is displayed prominently in our dashboards.
80%+ coverage for new code
Every PR must have near-80%+ test coverage for the code it introduces or modifies. This is enforced automatically in our CI pipeline. If you add 50 lines of new code, those 50 lines must be covered by tests. If you modify an existing function, your changes must be tested. This is overall test coverage. Unit test coverage needs to be near 100%, especially with the ability to leverage AI to help generate these.
Addressing the "coverage isn't the full story" argument: Yes, we know coverage doesn't guarantee perfect tests. We know you can write meaningless tests that hit every line but test nothing meaningful. We know coverage is just one metric among many. But, it's surely better to shoot for a high percentage than to have no idea where you are at all.
Measuring Success
"Velocity" (stealing this from Scrum even though we won't use Scrum)
Continued growth in monthly stats (features, improvements, refactors)
Quality
Reduce PR effort spent on fixes from the current 35% down to 20% or lower by end of 2026 (calculated based on file changes and additions/deletions)
Architectural health
Metrics on pattern adherence, technology coupling, boundary violations
Review efficiency
Smaller PRs, faster reviews, fewer rounds of feedback
Application and API uptime
How close are we to 99.99%?

Get started with Cal.com for free today!
Experience seamless scheduling and productivity with no hidden fees. Sign up in seconds and start simplifying your scheduling today, no credit card required!