Skip to main content

Designing scalable Drupal backend architectures

· 6 min read
Eduardo Telaya
Founder · Drupal Backend Architect

Most Drupal architecture problems aren't Drupal problems. They're decisions made early in a project — content modeling choices, caching strategies, integration patterns — that compound into performance and maintainability issues as the platform grows. Getting the architecture right means making those decisions deliberately, before they're made by accident.

Here's how we think about scalable Drupal backend design.

Start with the access patterns, not the content types

The most common architecture mistake is starting with content modeling and figuring out delivery later. The right order is the reverse: understand how the content will be consumed, then model around that.

Questions that should be answered before creating the first content type:

  • Who are the consumers? Editorial team only, external APIs, a decoupled frontend, third-party integrations?
  • What are the read vs. write ratios? A site that publishes 10 times a day and serves 10 million page views needs a very different cache strategy than one publishing 1,000 times a day to 10,000 users.
  • What are the latency requirements? Sub-100ms for public pages? Real-time for editorial previews?
  • Is content relationship complexity high? Deeply nested entity references kill performance in ways that are hard to fix after the fact.

The answers to these questions shape almost every subsequent architectural decision.

Decoupled vs. traditional: making the right call

Decoupled Drupal (Drupal as a headless CMS with a separate frontend) is not inherently better than traditional Drupal. It's a tradeoff, and the wrong call in either direction creates problems.

Traditional Drupal is the right choice when:

  • The team has strong Drupal frontend expertise (Twig, theme layer)
  • SEO and time-to-first-byte are critical and you want the simplest possible stack
  • The editorial team needs in-context preview and layout tools (Layout Builder, etc.)
  • The integration surface is primarily between Drupal and a few internal systems

Decoupled is the right choice when:

  • The frontend is genuinely complex and requires a modern JavaScript framework
  • Multiple consumers need the same content (web, mobile app, third-party systems)
  • The frontend team is stronger in React/Vue/Next than in Twig
  • Content delivery needs to be independent of Drupal's publishing pipeline

The mistake we see most often is organizations going decoupled because it sounds modern, without a frontend team capable of owning the frontend platform. The result is two systems to maintain and none of the benefits.

Content modeling that scales

A few principles that consistently improve long-term maintainability:

Model for the editor, not just the output. Fields that make sense as a data structure aren't always fields that make sense to an editorial team. If editors are regularly misusing a field — putting body content in a summary field, for example — that's a modeling failure.

Avoid deeply nested entity references. A node referencing a paragraph referencing a media entity referencing a file sounds fine on paper. At 50,000 nodes with complex views, it becomes a join nightmare. Flatten where you can. Use denormalized data structures when read performance matters more than write simplicity.

Use configuration management from day one. All content types, fields, views, and display modes should live in version-controlled YAML. A content type that was created by clicking in the UI and never exported is a liability — it can't be deployed reproducibly, and it can't be code-reviewed.

Plan for multilingual from the start. Adding multilingual support to a site that wasn't designed for it is expensive. Even if you're launching in one language, if there's any chance of adding more, build the translation infrastructure in from the beginning.

Caching architecture

Caching is where Drupal backend architecture either pays off or falls apart under load.

The caching layers we work with, from outside in:

  1. CDN (CloudFront, Fastly, etc.) — Handles the majority of anonymous traffic. Drupal's cache tags enable precise invalidation: when a node is updated, only the CDN cache entries that contain that node are purged, not the entire cache. The purge module ecosystem handles this well.

  2. Reverse proxy (Varnish) — Useful for infrastructure setups where a CDN isn't in place, or as an additional layer. Drupal generates Surrogate-Control headers that Varnish respects.

  3. Drupal's internal page cache — Serves fully cached pages for anonymous users without bootstrapping the full Drupal stack. Critical for high-traffic public sites.

  4. Drupal's dynamic page cache — Caches pages for authenticated users with user-specific elements excluded via cache contexts. Often overlooked, but significant for sites with logged-in users.

  5. Render cache — Individual render elements are cached based on cache tags, contexts, and max-age. Deep understanding of the render cache is the difference between a Drupal site that scales and one that doesn't.

The critical concept throughout is cache tags. Every entity in Drupal has tags. When that entity is updated, all cache entries tagged with it are invalidated automatically. Lean into this system rather than fighting it.

API design

For sites exposing content via API — whether to a decoupled frontend or external consumers — JSON:API (built into Drupal core) covers most use cases well. It handles filtering, sorting, includes, sparse fieldsets, and pagination out of the box.

Custom REST endpoints are appropriate when:

  • The response shape needs to differ significantly from Drupal's entity structure
  • You need to aggregate data from multiple entity types into a single response
  • Performance requirements make the overhead of JSON:API's generic approach unacceptable

When building custom endpoints, keep them thin. Business logic should live in services, not in controllers or plugins. This makes the API layer testable and replaceable.

Database patterns

A few things that consistently show up as performance bottlenecks:

Views with no exposed indexes. Drupal's Views module generates SQL queries that can be devastating on large datasets if the underlying fields aren't indexed. Always check the query being generated for complex views. Add database indexes where needed — Views doesn't do this automatically.

Entity queries that load full entities unnecessarily. \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($ids) loads complete entities. If you only need a field value, use an entity query to get IDs and then query the field table directly. The difference at scale is significant.

Missing composite indexes on custom tables. If you're writing custom tables (for logging, for external data sync, for anything), design the indexes around the queries you'll run, not around the data you're storing.

A note on infrastructure

Good Drupal backend architecture is only as good as the infrastructure running it. A few things that matter more than teams typically realize:

  • PHP-FPM tuning — The default pm.max_children value is almost never right for production. Profile under realistic load.
  • OPcache — Should be enabled and sized appropriately. A warm OPcache makes a meaningful difference in response times.
  • Database read replicas — For read-heavy sites, routing read queries to replicas reduces load on the primary significantly. Drupal's database abstraction layer supports this natively.

The compounding effect

Architecture decisions compound. A good content model makes migrations easier. A clean caching strategy makes infrastructure simpler. Proper use of configuration management makes deployments safer. These things build on each other, and the gap between a well-architected Drupal platform and a poorly-architected one grows with time.

The investment in getting architecture right at the start is almost always returned in reduced maintenance cost within the first year.