At Trellis, we've been leveraging Nx to manage our monorepo for years, and it's become an integral part of our development workflow. In a previous post, I discussed how we structure our Nx monorepo, but today I we are going to dive deeper into the task graph that powers our build system and the incredible time savings we've achieved.

nx-cloud-overall-stats-30-days.png The last 30 days of Nx Cloud usage as of August 20th, 2025.

Nx

The Nx task graph is the backbone of our build system. It's a directed acyclic graph (DAG) that represents the dependencies between tasks (build, test, lint, etc) across our projects (apps, libraries, packages, etc). When you run a command like nx compile app1, Nx constructs a task graph that includes not just the build task for app1, but all the tasks that need to run first to ensure a successful build (for our orient app this will include tasks like: build, generate-browsers-list-rc, generate-ngsw-config, inject-debug-ids and dependency targets like graphql-codegen).

What makes this powerful is that Nx understands the relationships between our projects and can intelligently determine what needs to be built, tested, or linted based on what has changed. This is critical for a monorepo of our size, with 14 applications and over 1,200 libraries.

We only have TypeScript (and SCSS/HTML) code in our repo since we are a full-stack TypeScript company (Angular + Nest) but if you aren't, worry not, Nx is language agnostic!

Target Distribution: A Snapshot of Our Monorepo

We have many, many targets in our task graph, some of which are boring and only have a couple of instances.

Here's a snapshot of our target distribution for targets that have enough cases to be interesting:

Most Common Targets

  • lint: 1075 targets
  • test: 591 targets
  • storybook/build-storybook: 159 targets
  • graphql-codegen: 139 targets
  • feature-e2e: 48 targets
  • build: 36 targets
  • typecheck: 17 targets

Generally speaking, our libraries have the most targets. We have some certain rules around what targets each library type has, some notable exceptions are:

  • type: Libraries specifically for holding type definitions do not have a test target.
  • Unless necessary for a very specific reason, we don't have a build target for libraries.
    • One notable exception is libraries that need to be compiled to JS to be used in other places (like one of our GraphQL Codegen Plugins)

Specialized Targets

Beyond the common targets, we have a long tail of specialized targets that serve specific purposes:

  • graphql-codegen: Generates TypeScript types from our GraphQL schemas
  • feature-e2e: Runs server side feature libraries using Jest and supertest
    • Currently, these are being migrated to an integration target to use vitest and test against a running cluster of our servers in CI (and local using an AppRunner utility we built so ensure the DX is acceptable)
  • typecheck: Performs TypeScript type checking on certain targets that do not have a build target (like our Plugins)
  • inject-debug-ids: Adds debug IDs to elements for Sentry.io tracking
  • generate-prisma-client: Generates Prisma clients for our different databases
  • deploy-to-s3: Deploys our apps static assets to AWS S3 Buckets for website hosting
  • circleci-plan/circleci-update: Configure Deployments and Deploy Markers in our CircleCI pipeline

Among many others, is there something you are curious about how we do with Nx? Reach out to me at the bottom of this post!

Quantifying our time savings with Nx

Nx Cloud is what enables us to save so much time across these targets (the ones marked as cacheable). Here are some of our stats around our Nx Cloud time savings:

img.png Time saved over the last 30 days by Nx Cloud

That's right—Nx Cloud has saved us 1 year and 243 days of computation time by pulling targets from the remote cache. This isn't just an abstract metric; it represents real developer hours that would have been spent waiting for builds, tests, and other tasks to complete (we save the most time on GraphQL Codegen).

Breaking it down further, we're saving an average of ~25 days of computation time daily through caching:

img_1.png Average time saved per day by Nx Cloud over the last 30 days

This is possible because Nx Cloud intelligently caches the results of tasks and reuses them when the inputs haven't changed. When a developer runs a task that has already been run with the same inputs (on CI, we disable local writes), Nx Cloud can instantly restore the results instead of recomputing them.

Time to Green

Another interesting metric to track is "Time to Green"—the time it takes for a PR to pass all checks and be ready for review. Nx gives us data on this over time:

img.png Both actual Time to Green and hypothetical Time to Green (if Nx Cloud were not enabled) for the selected metrics.

img_3.png img_1.png

Over the last 90 days, our average Time to Green per PR has been reduced by 1.8 hours, from almost 4 hours to just 1.5 hours. There is a notable spike in the last 3 weeks, this is due to us testing out AI agents. We are learning how best to prompt and guide them so that they do the job right and reduce the time to green (the first time).

Target-Specific Time Savings

Looking at specific targets, the time savings become even more impressive. For our most time-consuming targets:

Build Target

The build target for four of our main applications shows significant time savings:

  • thomas: 16.0 hours missed, 5.1 hours saved locally, 0.3 hours saved remotely
  • orient: 13.1 hours missed, 4.6 hours saved locally, 1.2 hours saved remotely
  • bullet: 8.5 hours missed, 4.4 hours saved locally, 0.3 hours saved remotely
  • aurora: 6.9 hours missed, 4.1 hours saved locally, 0.3 hours saved remotely

These are 4 of our most compiled applications (Angular and Nest), you may be wondering why not that much time has been saved remotely? Our workflow is very incremental and atomic, we do MANY small changes that are constantly being merged, which means every PR we are pushing is affecting one or more apps and often are merging multiple PRs simultaneously. This causes the targets to constantly have their cache broken on CI due to the large number of small changes. Additionally, since we disallow pushing to the cache from local, every change pushed to CI will not be able to push from cache if a developer hasn't had their change run on CI yet.

Storybook Targets

Our Storybook builds, which are particularly resource-intensive, show even more dramatic savings:

  • build-storybook: 738 hours saved locally, 1.3 hours saved remotely

The Storybook build cache falls into the same case with being affected incredibly often without a warm cache because of our atomic workflow.

We are really hoping that we can speed these up in the future using vite and TypeScript Go

Conclusion

Nx and the Nx task graph is a game-changer for our development workflow at Trellis. By providing a structured, efficient way to manage tasks across our monorepo, it has saved us years of computation time and dramatically improved our developer experience.

Over the lifetime of using Nx Cloud the numbers speak for themselves: 26+ years of computation time saved, daily savings of 6+ days just by pulling targets from the cache.

img_4.png

If you're managing a monorepo or considering adopting one, I can't recommend Nx enough. The investment in setting up a proper task graph and leveraging Nx Cloud will pay dividends in developer productivity and happiness.


Want to learn more about our architecture or have questions about implementing something similar? Feel free to reach out to me on Bluesky or LinkedIn. We're always happy to share what we've learned.

Trellis is a fundraising platform that helps charities run more effective fundraising events. Learn more about what we do at trellis.org.