Introduction
Many WordPress sites start with good intentions and end up fragile. A development team builds a custom theme, registers the custom post types the business needs, adds the taxonomies that organize content, wires up the relationships between data, and drops the supporting business rules straight into the theme files. Everything works. The site launches. Then a redesign begins, a new theme is commissioned, or a major theme update lands, and suddenly every custom post type, every taxonomy, and every piece of logic that depended on those files has to be reconstructed or carefully migrated. The business data itself may still sit in the database, but the code that defines and protects it is gone.
This pattern is common because it feels convenient during the first build. The theme is the place developers already work. It is the place the client sees changes. Yet the convenience is temporary. Once the theme becomes the container for data models and business rules, every future theme decision carries the risk of breaking the application layer. That risk shows up as delayed launches, duplicated effort, broken admin screens, missing content relationships, and quiet data integrity problems that only surface after the new theme is live.
The load order of WordPress itself explains why this happens and where the code should have lived instead.
Why Theme Bound Custom Code Becomes Expensive
When custom post types, taxonomies, and supporting logic live inside a theme, they are treated as presentation code even though they are not. Presentation can change. The underlying data model usually cannot without real cost. A company that has spent years accumulating posts of a custom type, related taxonomies, and custom fields now faces a rebuild of the registration code, the rewrite rules, the capability mapping, and any hooks that depended on those definitions. In practice this work is rarely a clean copy. Naming conventions drift, arguments change, and side effects appear in other plugins or templates that assumed the old definitions would always be present.
The cost is not only development hours. Editorial teams lose familiar interfaces. Automated processes that relied on specific post types stop working. SEO structures that depended on particular rewrite rules shift. Reporting tools that queried certain meta keys return incomplete results. All of this occurs because the architectural boundary between the application and the presentation layer was never drawn.
The Decision Every WordPress Project Faces
Every serious WordPress project eventually confronts the same choice. Should custom functionality live in the theme, in a regular plugin, or in a must use plugin? The answer is not a matter of preference or speed of delivery. It is a matter of what must survive a theme change and what may safely change with the visual design.
Themes exist to control output and the editorial experience of that output. Plugins and must use plugins exist to define capabilities, data structures, and business behavior that should remain stable across visual redesigns. Treating the theme as the home for data models collapses that distinction and forces the application layer to be rewritten whenever the design layer is updated.
Understanding the WordPress Load Order
WordPress does not load everything at once. It follows a deliberate sequence that determines which code is available at each stage and which code can safely depend on earlier code.
Core
WordPress core loads first. This includes the database connection, the object cache when configured, the basic request handling, and the foundational APIs that everything else will use. At this stage no custom post types or taxonomies from the project yet exist. Core itself is intentionally lean so that the rest of the stack can extend it.
Must Use Plugins
Next come must use plugins, often abbreviated as mu plugins. These are loaded automatically from the mu plugins directory and cannot be deactivated through the normal admin interface. Because they load before regular plugins and before the theme, they are the earliest place custom code can register post types, taxonomies, and foundational business logic while still remaining outside core. Code that lives here is available to every subsequent layer.
The must use plugins directory sits inside the WordPress content directory. Its standard location is the following path.
wp-content/mu-plugins/
├── application-core.php ← loader only (keeps the plugin header)
└── app-core/
├── post-types.php
├── taxonomies.php
├── capabilities.php
├── relationships.php
└── api/
├── api-endpoint.php
└── newsletter.php
WordPress automatically includes every PHP file placed directly in that folder. It does not automatically load files that sit inside subdirectories. For anything beyond a single small file, the practical approach is to place one loader file in the root of the mu-plugins directory and have that loader pull in the separate modules from a subdirectory.
A typical loader looks like this.
<?php
/**
* Plugin Name: Project Application Layer
* Description: Registers core data models and business logic that must survive theme changes.
* Version: 1.0.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Bump this value whenever you add/rename a CPT, taxonomy or rewrite slug.
* The next page load will flush rewrite rules exactly once.
*/
define( 'BMC_REWRITE_VERSION', '1.0.0' );
require_once __DIR__ . '/app-core/post-types.php';
require_once __DIR__ . '/app-core/taxonomies.php';
require_once __DIR__ . '/app-core/capabilities.php';
require_once __DIR__ . '/app-core/relationships.php';
require_once __DIR__ . '/app-core/api/api-endpoint.php';
// Or make it tiny
define( 'PATH', __DIR__ . '/app-core/' );
foreach ( array(
'post-types.php',
'taxonomies.php',
'capabilities.php',
'relationships.php',
'api/api-endpoint.php',
) as $file ) {
require_once PATH . $file;
}
// Run this after every registration
add_action( 'init', 'bmc_maybe_flush_rewrite_rules', 99 );
// One-time rewrite flush (replaces the activation hook mu-plugins don't have)
function bmc_maybe_flush_rewrite_rules() {
if ( get_option( 'bmc_rewrite_version' ) !== BMC_REWRITE_VERSION ) {
flush_rewrite_rules( false ); // soft flush: skips rewriting .htaccess.
update_option( 'bmc_rewrite_version', BMC_REWRITE_VERSION, false );
}
}
// BMC_ and bmc_ prefixes stand for "BoostMonitor Core", you change that to your preference.
Each required file then contains only the focused registration or logic it is responsible for. The loader itself stays minimal and remains the single entry point that WordPress discovers automatically.
Active Plugins
After must use plugins, WordPress loads the active plugins in the order determined by their filenames and any network activation rules. This is the layer where most reusable functionality belongs. A well structured custom plugin can register the entire data model of the site, expose REST endpoints, enforce capabilities, and provide the services that both the theme and other plugins will consume. Because plugins survive theme switches, the data model remains intact when the visual design changes.
The Theme
The active theme loads last. By the time the theme’s functions file and templates run, the core, the must use plugins, and the active plugins have already defined the available post types, taxonomies, and hooks. The theme’s job is to decide how that already registered data is displayed and how the editorial interface feels. When a theme also attempts to register the data model itself, it is registering something that will disappear the moment a different theme is activated.
What Happens When Business Logic Lives in the Theme
Putting custom post types and taxonomies inside the theme creates a hidden dependency. The database still contains the posts, but WordPress no longer knows how to interpret them once the theme that registered the types is deactivated. Admin menus vanish. The post type no longer appears in queries that rely on the registered name. Rewrite rules that depended on the registration are flushed away or left in an inconsistent state. Any template or plugin that assumed the post type would always be present begins to fail in subtle ways.
The same problem appears with supporting logic. Capability checks, meta box registration, save post hooks, and relationship management code that lived in the theme stop running. The result is not always a dramatic white screen. More often it is a gradual erosion of functionality that forces developers to reconstruct the missing pieces under time pressure while the new theme is being finished.
Under the Hood of WordPress Bootstrapping
When a request arrives, WordPress begins by loading the configuration and establishing the database connection. It then includes the must use plugins, followed by the active plugins, and only then loads the theme. Registration functions for post types and taxonomies are typically hooked to the init action. Because init fires after the plugins have loaded, any registration that occurs inside a plugin is available to the theme. Registration that occurs only inside the theme is available only while that theme remains active.
This sequence is not an accident. It is the mechanism that allows the application layer to be stable while the presentation layer remains replaceable. When teams ignore the sequence and place registration inside the theme, they effectively make the application layer temporary. Future architectural decisions then become constrained by the need to preserve or recreate that temporary code.
A practical consequence appears during local development and staging. Switching themes to test a redesign immediately disables the data model if it lived in the previous theme. Developers must either keep the old theme active in parallel or extract the registration code before they can safely evaluate the new design. Both approaches add friction that would never have existed if the data model had lived in a plugin from the start.
What We Recommend
For any WordPress project that defines its own content types, taxonomies, or non trivial business rules, place that code in a custom plugin or a must use plugin rather than the theme. The theme should consume the registered types and provide the templates and editorial experience. It should not own the definition of the types themselves.
Use a must use plugin when the functionality is foundational to the entire site and must never be deactivated, even accidentally. Use a regular custom plugin when the functionality is still project specific but may legitimately be toggled or versioned independently. Keep the theme focused on presentation, block patterns, template hierarchy, and the controlled editorial interface that sits on top of the already registered data model.
There are narrow exceptions. Purely presentational helpers that have no meaning outside a particular visual design can live in the theme. Everything that defines what the site is, rather than how a particular version of the site looks, belongs outside the theme.
How We Approach Architecture at Boostmonitor
At Boostmonitor we treat the separation of the application layer from the presentation layer as a non negotiable starting point. When we begin a project we identify the data models and business rules that must survive any future redesign and place them in dedicated plugins or must use plugins before the first template is written. Custom post types, taxonomies, capability maps, and core business logic are registered early and remain available regardless of which theme is active.
This approach lets us redesign, rebuild, or replace a theme without touching the underlying application. It also makes the site more testable, because the data model can be exercised independently of any particular set of templates. When a client later needs a new public face or a different editorial experience, the work stays focused on the presentation layer instead of becoming a reconstruction of years of accumulated business logic.
We apply the same discipline to larger WordPress applications, customer portals, and headless setups. The principle remains constant. WordPress is capable of powering durable business systems. The limitation appears only when the architecture mixes temporary presentation code with permanent application code.
The load order is not merely a technical detail. It is the map that shows where each kind of code should live if the site is to remain maintainable as the business evolves.
Frequently Asked Questions
Can I move existing custom post types from a theme into a plugin without losing data
Yes. The posts themselves remain in the database. You register the same post type names and arguments inside a plugin, deactivate the theme registration, and flush the rewrite rules. The existing content becomes available again under the newly registered types. Care is required with any meta boxes, save hooks, or capability checks that also lived in the theme, because those must be moved as well.
Is a must use plugin always better than a regular plugin for data models
Not always. Must use plugins cannot be deactivated through the admin interface, which makes them appropriate for code that is truly foundational. Regular plugins are preferable when you want version control, clearer activation control, or the ability to disable functionality during debugging. Both keep the code outside the theme.
What about block themes and full site editing
The same principle applies. Block themes still load after plugins. Custom post types and taxonomies should still be registered in a plugin so that the data model survives any future change of theme or template set. Full site editing improves the presentation layer. It does not change the need to keep the application layer independent.
Does this approach slow down the initial build
It adds a small amount of structure at the beginning and removes far larger costs later. Registering types in a plugin takes roughly the same effort as registering them in a theme. The difference appears when the first redesign arrives and the data model does not have to be rebuilt.