Layered architecture in WordPress: get logic out of functions.php

The standard advice for WordPress code is still “just throw it in functions.php,” and you pay for that later in both performance and patience. If adding one small feature to your WooCommerce checkout feels like open-heart surgery, the problem probably is not your skills. It is the missing Layered Architecture.

Fourteen years in this ecosystem and the failure mode barely changes: a 5,000-line functions.php where business logic, database queries and HTML rendering sit tangled together in one giant “spaghetti hook.” That is what Layered Architecture is for. You cannot safely change code you cannot find, and a file that does everything hides everything.

Why WordPress code needs structure

Most WordPress tutorials tell you to hook into an action and run your logic right there. That is fine for a ten-line snippet. On a large application it falls apart. Mix persistence (the database) with presentation (hooks) and you cannot test anything without spinning up a full staging site. Change one column in the schema and you are grepping dozens of files for raw SQL.

I wrote earlier about stopping over-engineering in WordPress, and this is not a contradiction. Structure is not the same thing as over-engineering. It is what stops you guessing where code lives.

The five layers of a WordPress app

The fix is to slice the application into zones of responsibility. That is all Layered Architecture really means.

  • The Interface Layer: Your entry points, so REST API controllers, AJAX handlers, action and filter callbacks. They validate the input and hand off to the next layer, and that is the whole job.
  • The Application Layer: The orchestrator. It has never heard of $_POST or WP_REST_Request. It knows how to run a workflow such as cancelling a subscription.
  • The Domain Layer: The business rules. “A subscription can only be canceled within 14 days” belongs here, as plain logic that does not depend on any framework.
  • The Repository Layer: Fetching and saving data. This is the only place a WP_Query or a global $wpdb is allowed to appear.
  • The Infrastructure Layer: Anything that talks to the outside world: sending email through SendGrid, logging to an external service, calling the Stripe API.

Bad code: everything crammed into one hook

I call this one the God Function. It does everything, which means that when it breaks you have no idea which part broke.

<?php
add_action( 'wp_ajax_bbioon_cancel_sub', function() {
    // 1. Validation mixed with logic
    if ( ! isset( $_POST['sub_id'] ) ) wp_die();
    
    // 2. Raw DB access mixed in
    global $wpdb;
    $sub = $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}subs WHERE id = " . intval($_POST['sub_id']) );
    
    // 3. Business logic mixed in
    if ( $sub->status === 'active' ) {
        $wpdb->update( "{$wpdb->prefix}subs", ['status' => 'cancelled'], ['id' => $sub->id] );
        
        // 4. Infrastructure mixed in
        wp_mail( $sub->email, 'Cancelled', 'Your sub is gone.' );
    }
    
    wp_send_json_success();
});

Good code: the same job, split into layers

The refactor pulls persistence into a Repository and the logic into a Service. The same work happens, but each piece can now be read on its own.

<?php
// Repository Layer: Only handles DB
class Bbioon_Sub_Repository {
    public function find( int $id ) {
        global $wpdb;
        return $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}subs WHERE id = $id" );
    }
    public function update_status( int $id, string $status ) {
        global $wpdb;
        return $wpdb->update( "{$wpdb->prefix}subs", ['status' => $status], ['id' => $id] );
    }
}

// Application Layer: Orchestrates the work
class Bbioon_Subscription_Service {
    private $repo;
    public function __construct( Bbioon_Sub_Repository $repo ) {
        $this->repo = $repo;
    }

    public function cancel_subscription( int $id ) {
        $sub = $this->repo->find( $id );
        if ( ! $sub || $sub->status !== 'active' ) {
            throw new Exception('Invalid sub');
        }
        $this->repo->update_status( $id, 'cancelled' );
        // Call infrastructure layer to send email...
    }
}

// Interface Layer: Entry point
add_action( 'wp_ajax_bbioon_cancel_sub', function() {
    $service = new Bbioon_Subscription_Service( new Bbioon_Sub_Repository() );
    try {
        $service->cancel_subscription( (int) $_POST['sub_id'] );
        wp_send_json_success();
    } catch ( Exception $e ) {
        wp_send_json_error( $e->getMessage() );
    }
});

The rules that keep the layers honest

Layered Architecture only works while the boundaries hold, and that means dependencies flow inward. Your Repository has no business knowing about wp_send_json_success(), and your Application layer has no business knowing SQL. I go further into structure in my write-up on WordPress AI Architecture.

It comes down to three rules:

  • Rule 1: A layer only talks to the layer directly below it.
  • Rule 2: Business logic (the domain) never calls WordPress functions (the infrastructure).
  • Rule 3: Return domain objects, a Subscription for example, rather than the raw stdClass rows the database hands back.

If untangling this is eating your billable hours, I can take it on. I have been working with WordPress since the 4.x days.

The payoff is cheaper maintenance

Setting this up feels like extra work at first, and it does ask you to think like an engineer rather than a hacker. The payback shows up the first time a client moves from local database storage to a third-party API. Because the logic sits on its own, you rewrite the Repository layer and nothing else has to move.

For a deeper read on clean PHP, there is a solid introduction to Clean Architecture in PHP. After that, open your worst functions.php and start pulling the database calls out of it.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.