• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to design a modular Domain-driven architecture (DDD) blocks architecture for enterprise-level custom plugins

How to design a modular Domain-driven architecture (DDD) blocks architecture for enterprise-level custom plugins

Deconstructing WordPress Plugin Complexity with DDD Building Blocks

Enterprise-level WordPress plugins often evolve into monolithic beasts, making maintenance, scalability, and feature expansion a significant challenge. This post outlines a strategy for designing modular plugin architectures using Domain-Driven Design (DDD) principles, focusing on practical implementation with PHP and WordPress hooks. We’ll break down a hypothetical e-commerce plugin into distinct, manageable “building blocks” or “bounded contexts.”

Defining Bounded Contexts: The Core of Modularity

The first step in a DDD approach is to identify the core business domains and delineate them into Bounded Contexts. For an e-commerce plugin, these might include:

  • Catalog Management: Handles products, categories, attributes, and variations.
  • Order Processing: Manages shopping carts, checkout, order creation, and status updates.
  • Customer Management: Deals with user accounts, profiles, addresses, and purchase history.
  • Payment Gateway Integration: Abstracts different payment providers.
  • Shipping & Fulfillment: Manages shipping methods, rates, and tracking.

Each of these contexts should be as independent as possible, communicating with others through well-defined interfaces (APIs or events). This isolation is key to achieving true modularity.

Structuring the Plugin Filesystem for DDD

A clear directory structure is crucial for maintaining DDD principles. We’ll adopt a structure where each Bounded Context has its own dedicated directory, containing its domain logic, application services, and infrastructure concerns.

Consider a plugin named `my-enterprise-ecommerce`. The proposed structure would look like this:

  • my-enterprise-ecommerce/
    • src/
      • Catalog/ (Bounded Context: Catalog Management)
        • Domain/
          • Entities/Product.php
          • ValueObjects/Price.php
          • Repositories/ProductRepository.php
          • Services/ProductService.php
        • Application/
          • Commands/CreateProductCommand.php
          • Queries/GetProductByIdQuery.php
          • Handlers/CreateProductCommandHandler.php
        • Infrastructure/
          • Persistence/WPDBProductRepository.php
        • CatalogServiceProvider.php (or similar for dependency injection)
      • Order/ (Bounded Context: Order Processing)
        • Domain/
          • Entities/Order.php
          • ValueObjects/OrderStatus.php
          • Repositories/OrderRepository.php
        • Application/
          • Commands/PlaceOrderCommand.php
          • Handlers/PlaceOrderCommandHandler.php
        • Infrastructure/
          • Persistence/WPDBOrderRepository.php
        • OrderServiceProvider.php
      • Customer/ (Bounded Context: Customer Management)
      • Payment/ (Bounded Context: Payment Gateway Integration)
      • Shipping/ (Bounded Context: Shipping & Fulfillment)
      • Common/ (Shared entities, value objects, or interfaces if absolutely necessary)
    • includes/ (WordPress specific bootstrapping, hooks, etc.)
    • my-enterprise-ecommerce.php (Main plugin file)

Implementing Domain Logic: The Heart of a Context

Within each Bounded Context, we focus on the core domain logic. This involves entities, value objects, aggregates, domain services, and repositories. Let’s take the `Catalog` context as an example.

Catalog Context: Domain Layer Example

We’ll define a `Product` entity and a `Price` value object.

Entities: Product.php

Entities have a unique identity that persists over time. In WordPress, this often maps to a post type or a custom table ID.

namespace MyEnterpriseEcommerce\Catalog\Domain\Entities;

use MyEnterpriseEcommerce\Catalog\Domain\ValueObjects\Price;
use InvalidArgumentException;

class Product {
    private int $id;
    private string $name;
    private Price $price;
    private array $attributes = []; // e.g., ['color' => 'red', 'size' => 'M']

    public function __construct(int $id, string $name, Price $price) {
        if (empty($name)) {
            throw new InvalidArgumentException('Product name cannot be empty.');
        }
        $this->id = $id;
        $this->name = $name;
        $this->price = $price;
    }

    public function getId(): int {
        return $this->id;
    }

    public function getName(): string {
        return $this->name;
    }

    public function setName(string $name): void {
        if (empty($name)) {
            throw new InvalidArgumentException('Product name cannot be empty.');
        }
        $this->name = $name;
    }

    public function getPrice(): Price {
        return $this->price;
    }

    public function setPrice(Price $price): void {
        $this->price = $price;
    }

    public function addAttribute(string $key, string $value): void {
        $this->attributes[$key] = $value;
    }

    public function getAttribute(string $key): ?string {
        return $this->attributes[$key] ?? null;
    }

    public function getAttributes(): array {
        return $this->attributes;
    }
}

Value Objects: Price.php

Value objects are immutable and defined by their attributes, not an identity. They are useful for representing concepts like currency, dates, or measurements.

namespace MyEnterpriseEcommerce\Catalog\Domain\ValueObjects;

use InvalidArgumentException;

class Price {
    private float $amount;
    private string $currency; // e.g., 'USD', 'EUR'

    public function __construct(float $amount, string $currency) {
        if ($amount < 0) {
            throw new InvalidArgumentException('Price amount cannot be negative.');
        }
        if (empty($currency)) {
            throw new InvalidArgumentException('Currency cannot be empty.');
        }
        $this->amount = $amount;
        $this->currency = strtoupper($currency);
    }

    public function getAmount(): float {
        return $this->amount;
    }

    public function getCurrency(): string {
        return $this->currency;
    }

    // Example of immutability: methods return new instances
    public function add(Price $other): Price {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException('Cannot add prices with different currencies.');
        }
        return new Price($this->amount + $other->amount, $this->currency);
    }

    public function equals(Price $other): bool {
        return $this->amount === $other->amount && $this->currency === $other->currency;
    }
}

Repositories: Abstracting Data Persistence

Repositories provide an abstraction layer for data access, hiding the underlying storage mechanism (e.g., `wpdb`, custom tables, external APIs). This allows the domain layer to remain independent of infrastructure details.

Catalog Repository Interface: ProductRepository.php

namespace MyEnterpriseEcommerce\Catalog\Domain\Repositories;

use MyEnterpriseEcommerce\Catalog\Domain\Entities\Product;

interface ProductRepository {
    public function findById(int $id): ?Product;
    public function save(Product $product): bool;
    public function delete(int $id): bool;
    public function findAll(): array;
    // Add other methods as needed, e.g., findByCategory(int $categoryId)
}

Concrete Implementation: WPDBProductRepository.php

This implementation uses WordPress’s `wpdb` global object to interact with the database. We’ll assume a custom table `wp_ecommerce_products` for simplicity.

namespace MyEnterpriseEcommerce\Catalog\Infrastructure\Persistence;

use MyEnterpriseEcommerce\Catalog\Domain\Entities\Product;
use MyEnterpriseEcommerce\Catalog\Domain\Repositories\ProductRepository;
use MyEnterpriseEcommerce\Catalog\Domain\ValueObjects\Price;
use wpdb;
use Exception;

class WPDBProductRepository implements ProductRepository {
    private wpdb $db;
    private string $tableName;

    public function __construct(wpdb $db) {
        $this->db = $db;
        $this->tableName = $db->prefix . 'ecommerce_products';
    }

    public function findById(int $id): ?Product {
        $result = $this->db->get_row(
            $this->db->prepare(
                "SELECT * FROM {$this->tableName} WHERE id = %d",
                $id
            ),
            ARRAY_A
        );

        if (!$result) {
            return null;
        }

        try {
            $price = new Price((float) $result['price_amount'], $result['price_currency']);
            $product = new Product($result['id'], $result['name'], $price);
            // Load attributes if stored separately or serialized
            if (!empty($result['attributes'])) {
                $attributes = json_decode($result['attributes'], true);
                if (is_array($attributes)) {
                    foreach ($attributes as $key => $value) {
                        $product->addAttribute($key, $value);
                    }
                }
            }
            return $product;
        } catch (Exception $e) {
            // Log error, handle gracefully
            error_log("Error creating Product entity from DB row: " . $e->getMessage());
            return null;
        }
    }

    public function save(Product $product): bool {
        $data = [
            'name' => $product->getName(),
            'price_amount' => $product->getPrice()->getAmount(),
            'price_currency' => $product->getPrice()->getCurrency(),
            'attributes' => json_encode($product->getAttributes()),
        ];

        $format = ['%s', '%f', '%s', '%s'];

        if ($product->getId() > 0) { // Update existing
            $where = ['id' => $product->getId()];
            $where_format = ['%d'];
            $updated = $this->db->update($this->tableName, $data, $where, $format, $where_format);
            return $updated !== false;
        } else { // Insert new
            $inserted = $this->db->insert($this->tableName, $data, $format);
            if ($inserted) {
                $product->__set('id', $this->db->insert_id); // Hacky way to set ID after insert, consider a better approach
            }
            return $inserted !== false;
        }
    }

    public function delete(int $id): bool {
        $deleted = $this->db->delete($this->tableName, ['id' => $id], ['%d']);
        return $deleted !== false;
    }

    public function findAll(): array {
        $results = $this->db->get_results("SELECT id FROM {$this->tableName}", ARRAY_A);
        $products = [];
        if ($results) {
            foreach ($results as $row) {
                $product = $this->findById($row['id']);
                if ($product) {
                    $products[] = $product;
                }
            }
        }
        return $products;
    }
}

Application Layer: Orchestrating Use Cases

The application layer orchestrates the domain objects to fulfill specific use cases. It doesn’t contain business logic itself but rather coordinates the domain and infrastructure layers. This is where commands and queries are typically handled.

Catalog Context: Application Layer Example

Let’s define a command to create a new product.

Command: CreateProductCommand.php

namespace MyEnterpriseEcommerce\Catalog\Application\Commands;

class CreateProductCommand {
    public string $name;
    public float $priceAmount;
    public string $currency;
    public array $attributes;

    public function __construct(string $name, float $priceAmount, string $currency, array $attributes = []) {
        $this->name = $name;
        $this->priceAmount = $priceAmount;
        $this->currency = $currency;
        $this->attributes = $attributes;
    }
}

Command Handler: CreateProductCommandHandler.php

The handler takes the command, uses the repository to create a domain entity, and saves it.

namespace MyEnterpriseEcommerce\Catalog\Application\Handlers;

use MyEnterpriseEcommerce\Catalog\Domain\Entities\Product;
use MyEnterpriseEcommerce\Catalog\Domain\Repositories\ProductRepository;
use MyEnterpriseEcommerce\Catalog\Domain\ValueObjects\Price;
use MyEnterpriseEcommerce\Catalog\Application\Commands\CreateProductCommand;

class CreateProductCommandHandler {
    private ProductRepository $productRepository;

    public function __construct(ProductRepository $productRepository) {
        $this->productRepository = $productRepository;
    }

    public function handle(CreateProductCommand $command): Product {
        // Create domain objects
        $price = new Price($command->priceAmount, $command->currency);
        $product = new Product(0, $command->name, $price); // ID 0 for new product

        // Add attributes
        foreach ($command->attributes as $key => $value) {
            $product->addAttribute($key, $value);
        }

        // Persist using the repository
        if (!$this->productRepository->save($product)) {
            throw new \RuntimeException('Failed to save product.');
        }

        return $product;
    }
}

Dependency Injection and Service Providers

To manage dependencies between different parts of your application (e.g., injecting `ProductRepository` into `CreateProductCommandHandler`), a Dependency Injection Container (DIC) is highly recommended. For WordPress plugins, you can implement a simple service locator pattern or use a lightweight DIC library. Each Bounded Context can have its own “Service Provider” to register its services.

Example: CatalogServiceProvider.php (Conceptual)

namespace MyEnterpriseEcommerce\Catalog;

use MyEnterpriseEcommerce\Catalog\Domain\Repositories\ProductRepository;
use MyEnterpriseEcommerce\Catalog\Infrastructure\Persistence\WPDBProductRepository;
use MyEnterpriseEcommerce\Catalog\Application\Handlers\CreateProductCommandHandler;
use MyEnterpriseEcommerce\Common\DI\Container; // Assuming a simple DIC

class CatalogServiceProvider {
    public function register(Container $container) {
        // Register the repository implementation
        $container->singleton(ProductRepository::class, function(Container $c) {
            global $wpdb; // Accessing global $wpdb
            return new WPDBProductRepository($wpdb);
        });

        // Register the command handler, it will automatically resolve its dependencies
        $container->register(CreateProductCommandHandler::class, function(Container $c) {
            $productRepository = $c->get(ProductRepository::class);
            return new CreateProductCommandHandler($productRepository);
        });

        // Register other services for the Catalog context...
    }
}

Integrating with WordPress Hooks

The WordPress-specific integration layer (often in `includes/` or within each context’s `Infrastructure` or a dedicated `Presentation` layer) is responsible for hooking into WordPress actions and filters. This layer translates WordPress events into commands or queries for the application layer and formats the results for display.

Example: Hooking into `save_post` for Product Creation

This example assumes you’re using WordPress posts for products, which is a common scenario. A more robust solution might use custom tables or CPTs managed by the `Catalog` context itself.

namespace MyEnterpriseEcommerce\Includes;

use MyEnterpriseEcommerce\Catalog\Application\Commands\CreateProductCommand;
use MyEnterpriseEcommerce\Catalog\Application\Handlers\CreateProductCommandHandler;
use MyEnterpriseEcommerce\Common\DI\Container; // Assuming a simple DIC

class HookManager {
    private Container $container;

    public function __construct(Container $container) {
        $this->container = $container;
    }

    public function registerHooks() {
        // Example: Hooking into post save for a custom product post type
        add_action('save_post_product', [$this, 'handleProductSave']);
    }

    public function handleProductSave(int $postId) {
        // Prevent infinite loops and double saves
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return;
        }
        if (wp_is_post_revision($postId)) {
            return;
        }

        // Check user permissions (important!)
        if (!current_user_can('edit_post', $postId)) {
            return;
        }

        // Retrieve data from $_POST (this is a simplified example)
        $productName = $_POST['product_name'] ?? '';
        $priceAmount = filter_input(INPUT_POST, 'product_price_amount', FILTER_VALIDATE_FLOAT);
        $currency = $_POST['product_currency'] ?? 'USD';
        $attributes = $_POST['product_attributes'] ?? []; // Assume this is an array of key/value pairs

        if (empty($productName) || $priceAmount === false) {
            // Handle validation errors, maybe add_settings_error()
            return;
        }

        try {
            // Create the command
            $command = new CreateProductCommand($productName, $priceAmount, $currency, $attributes);

            // Get the handler from the container
            /** @var CreateProductCommandHandler $handler */
            $handler = $this->container->get(CreateProductCommandHandler::class);

            // Execute the command
            $product = $handler->handle($command);

            // Optionally, update post meta with the product ID or other info
            update_post_meta($postId, '_ecommerce_product_id', $product->getId());

        } catch (\Exception $e) {
            // Log the error and potentially display a user-friendly message
            error_log("Error saving product via hook: " . $e->getMessage());
            // add_settings_error('my_ecommerce_plugin', 'save_error', 'Could not save product: ' . $e->getMessage(), 'error');
        }
    }
}

// In your main plugin file (my-enterprise-ecommerce.php):
// $container = new Container(); // Initialize your DIC
// (new CatalogServiceProvider())->register($container);
// (new OrderServiceProvider())->register($container); // Register other providers
// (new HookManager($container))->registerHooks();

Cross-Context Communication: Events and APIs

When Bounded Contexts need to interact, it should be done through well-defined interfaces. For asynchronous communication or broadcasting state changes, an event-driven approach is powerful. For synchronous requests, a simple API or service call can be used.

Example: Order Context Reacting to Product Update

When a product’s price changes in the `Catalog` context, the `Order` context might need to be notified. This can be achieved by the `Catalog` context publishing an event (e.g., `ProductPriceUpdatedEvent`) and the `Order` context subscribing to it.

// In Catalog\Domain\Services\ProductService.php (or similar)
// ... after updating price ...
$eventBus->publish(new ProductPriceUpdatedEvent($product->getId(), $newPrice));

// In Order\Infrastructure\EventSubscribers\ProductEventListener.php
// ... when registering event listeners ...
$eventBus->subscribe(ProductPriceUpdatedEvent::class, [$this, 'onProductPriceUpdated']);

// In Order\Infrastructure\EventSubscribers\ProductEventListener.php
public function onProductPriceUpdated(ProductPriceUpdatedEvent $event) {
    // Find all carts/orders that contain this product and update their prices
    // This would involve Order context's repositories and domain logic
    $this->orderRepository->updatePricesForProductInCarts($event->getProductId(), $event->getNewPrice());
}

Benefits and Considerations

  • Modularity: Easier to develop, test, and maintain individual components.
  • Scalability: Individual contexts can be scaled independently.
  • Team Autonomy: Teams can own specific Bounded Contexts.
  • Testability: Unit and integration tests become more focused and manageable.
  • Flexibility: Easier to swap out implementations (e.g., change payment gateway) within a context.

Considerations:

  • Complexity: Initial setup can be more complex than a monolithic approach.
  • Learning Curve: Requires understanding DDD concepts.
  • Overhead: Managing inter-context communication adds overhead.
  • Context Mapping: Defining clear boundaries and relationships between contexts is critical and can be challenging.

By applying DDD principles and structuring your WordPress plugin into distinct, well-defined Bounded Contexts, you can build more robust, maintainable, and scalable enterprise-level solutions. This approach moves away from the traditional “everything in one file” mentality towards a more organized and domain-centric architecture.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Reducing database query bloat in Sage Roots modern environments layouts using custom lazy loaders
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Firebase Realtime DB handlers
  • Reducing Largest Contentful Paint (LCP) by optimizing custom script enqueuing structures in legacy plugins
  • How to implement native Redis caching layers for high-volume custom taxonomy queries in Carbon Fields custom wrappers
  • Building secure B2B pricing grids with custom REST API Controllers endpoints and role overrides

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (658)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (872)
  • PHP (5)
  • PHP Development (48)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (182)
  • WordPress Plugin Development (197)
  • WordPress Plugin Development (330)
  • WordPress Theme Development (357)

Recent Posts

  • Reducing database query bloat in Sage Roots modern environments layouts using custom lazy loaders
  • Performance Optimization: Tuning PHP-FPM and opcache pools for high-concurrency Firebase Realtime DB handlers
  • Reducing Largest Contentful Paint (LCP) by optimizing custom script enqueuing structures in legacy plugins

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala