Handbook
Scaling Applications

Scaling Frontend Applications

Last updated by Noel Varanda (opens in a new tab),
Scaling applications
Code organization
Collaboration
Domain-driven Design
TailwindCSS
Monorepo

Malte Ubl, the CTO of Vercel, gave a talk at React Summit 2021 on the principles of scaling frontend applications. He narrowed down the principles to six key points that can be applied to any frontend project to create a scalable, maintainable, and adaptable repository.

This article will summarize the key points from his talk and provide practical tips on how to apply these principles in your frontend projects in a practical way.

Watch the full talk: Malte Ubl - Principles for Scaling Frontend Application Development, React Summit 2023 (opens in a new tab)

Teardown barriers

  • Foster collaboration and ownership through a monorepo structure.

  • Use access restrictions to specific parts within the monorepo to manage contributions.

Monorepo structure

Monorepos are difficult to setup, however there are powerful tools such as nx that abstract these complexitites. Check out the Boostrapping a Project section for a detailed guide on setting up your monorepo.

Access restriction

You can restrict certain parts of codebases to teams or users, or require peer reviews from specific people by using the CODEOWNERS file. This is easy to setup on both GitHub and GitLab.

Make it easy to delete code

  • Keep the codebase lean by minimizing unnecessary code.

  • Move data calls and filtering inside components (Data-in-JS) to reduce code clutter.

Domain-Driven Design (DDD)

Domain-Driven Design (DDD) focuses on building software systems around the core domain, representing the application's business logic. Applying DDD principles in frontend development leads to modular, maintainable, and scalable codebases.

By adopting DDD principles in frontend development, you can create a more structured and maintainable codebase that aligns with the business domains of your application, leading to increased productivity and scalability.

Organize code using a directory structure that reflects different domains and subdomains. This makes it so that if we needed to delete anything to do with cart, we only need to remove it in one place:

~/my-project/apps/my-app
├── src
│   ├── common
│   │   ├── components
│   │   │   ├── Button.tsx
│   │   │   └── ...
│   │   ├── hooks
│   │   ├── utils
│   │   └── ...
│   ├── domains
│   │   ├── shopping
│   │   │   ├── cart
│   │   │   │   ├── components
│   │   │   │   │   ├── Cart.tsx
│   │   │   │   │   ├── CheckoutButton.tsx
│   │   │   │   │   └── ...
│   │   │   │   ├── hooks
│   │   │   │   ├── utils
│   │   │   │   └── ...
│   │   │   ├── products
│   │   │   ├── orders
│   │   │   └── ...
│   │   └── ...
│   ├── layouts
│   │   ├── MainLayout.tsx
│   │   └── ...
│   ├── styles
│   ├── App.tsx
│   ├── index.tsx
│   └── ...

Read Martin Fowler's DomainDrivenDesign (opens in a new tab) for more insights.

DRY vs. WET

⚠️
This is a warning on shared code.

We learn early on that DRY (Don't Repeat Yourself) is the main principle to follow in programming. However WET (Write Everything Twice) is often overlooked and there is a place for a small duplication over the wrong abstraction (opens in a new tab).

When introducing shared code in a project, it can be very easy to just stick anything that "could" be shared into a shared directory. You'll soon find however that if everyone on a team does this, your shared codebase will end up doing everything and anything under the sun, and you'll end up importing a library you wanted to do one small function, but you've also got a library that does multiple things with thousands of dependencies. It's the old You wanted a banana but you got a gorilla holding the banana (opens in a new tab) problem.

Soon enough, multiple teams are deependant on a shared lib that does much more than it should and these dependencies become extremely difficult to rip apart. So when using a shared lib folder, make sure that:

  • Packages are small and have a point (avoid utils where possible).
  • Ask yourself if having this shared dependency is worth the potential future cost.

Data fetching in TS

Delegate data fetching to child components. Not all data needs to be at the top level of the applications. The following code is encouraged as deleting Cart usages is in one place.

import type { FC } from 'react';
import { Cart } from '@/domains/cart';
 
type SidebarProps = { cartId: string };
 
export const Sidebar: FC<CartProps> = ({ cartId }) => (
  // Data fetching done in Cart component
  <Cart cartId={cartId} />
);

Tailwind

Use a utility-first approach such as tailwind (opens in a new tab) which reduces the amount of CSS you need to write, but it also avoids needing an adjacent CSS file for each React component.

Migrate incrementally

  • Scale the application through incremental migrations rather than massive overhauls.

  • Take small steps and gradually migrate parts of the codebase to mitigate risks.

Always get better, never get worse

  • Prioritize addressing technical debt and improving the codebase over time.

  • Continuously raise the code quality bar by resolving eslint errors and adding new rules.

Linting rules

Leverage linters to get your codebase to a standard you would like. In line with the Migrate Incrementally principle, make sure to not let violations stop entire codebases from being comitted.

Embrace a lack of knowledge

  • Document the codebase knowledge in a machine-readable format for easy reference.

  • Enable team members to quickly understand and collaborate on the codebase.

Documentation

Document how to interact with different codebases and teams. Read more on how in the Documentation and Communication section.

Testing and automation

Integrating testing frameworks and tools for automated testing. Read more on how in our Testing section.

Implement a continuous integration and delivery (CI/CD) pipeline for seamless scalability. Read more on how in our CI/CD section.

Codeowners

As stated in Access Restrictions, add CODEOWNERS.

Eliminate systematic complexity

  • Maintain a catalog of difficulties encountered by the team within the app.

  • Develop strategies, tools, or processes to reduce systematic complexity.

  • Use tools like zod to keep TypeScript types consistent with APIs for a smoother development experience.

Documentation

Document how to interact with different codebases and teams. Read more on how in the Documentation and Communication section.

Leverage type-safe API methods

Leverage zod (opens in a new tab) to validate contracts, or GraphQL (opens in a new tab) or tRPC (opens in a new tab) to keep APIs typed.


Keep up to date with any latest changes or announcements by subscribing to the newsletter below.