Por

Keith Williams

14 oct 2025

Engineering in 2026 and beyond

Engineering in 2026 and beyond

Engineering in 2026 and beyond

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:

Screenshot 2025-10-01 at 9.40.36 PM.png

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

Screenshot 2025-10-01 at 9.41.51 PM.png

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.

good_architecture_diagram.pngbad_architecture_diagram.png

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.

  1. Data layer → Application layer → API: Transform database models into application-layer DTOs, then transform application DTOs into API-specific DTOs

  2. 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:

export class BillingPortalServiceFactory {  
  static async createService(teamId: number): Promise<BillingPortalService>

Each service then handles its specific logic without needing to check "am I an org or a team?":

// OrganizationBillingPortalService handles ONLY organization logic
class OrganizationBillingPortalService extends BillingPortalService {  
  async checkPermissions(userId: number, teamId: number): Promise<boolean> {    
    return await this.permissionService.checkPermission({      
      userId,      
      teamId,      
      permission: "organization.manageBilling",  // Organization-specific      
      fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],    
    });  
  }  
  // ... more organization-specific logic
}

// TeamBillingPortalService handles ONLY team logic
class TeamBillingPortalService extends BillingPortalService {  
  async checkPermissions(userId: number, teamId: number): Promise<boolean> {    
    return await this.permissionService.checkPermission({      
      userId,      
      teamId,      
      permission: "team.manageBilling",  // Team-specific      
      fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER]

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 checking if (isTeam) or if (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 callbacks

  • Checking 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 arrays

  • Interval trees: For scheduling, availability, and range queries, use proper tree structures instead of brute-force comparison

Example transformation:

// Bad: O(n²) - checks every slot against every busy time
availableSlots.filter(slot => {  
  return !busyTimes.some(busy => checkOverlap(slot, busy));
});

// Good: O(n log n) - sort once, break early
const sortedBusy = [...busyTimes]

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 calls

  • Recursion 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!