Pseudo-modularity - why your code will fall apart anyway

Pseudo-modularity - why your code will fall apart anyway

You're joining a new project or starting a greenfield project. Everything is shiny. The folders are neatly arranged: Products/Orders/Users/. The architect cried with joy when he approved the directory structure. It looks so clean, so logical.

After one year...

Suddenly, it turns out that a change in Products (because marketing wants new SEO tags) breaks the tests in the Inventory warehouse module, and adding returns handling blocks the deployment of three other teams for two days. What went wrong? It was so ‘modular’.

I recently came across some great material that mercilessly points out this problem. What we often take for modularity is just a facade. If you feel that your system, despite its beautiful structure on paper, is starting to rot from the inside, this text is for you. Let's talk about why thinking in nouns is a trap.

Facade modularity and product-godzilla

The biggest lie we believe is that if we divide the code according to nouns (business entities), we will automatically get loosely coupled modules. Products separately, shopping cart separately. Congratulations, you've just sorted your socks in the drawer, but you haven't designed the architecture.

The intuitive approach (‘we have a table of products, so we create a Product class’) leads to disaster. Imagine a typical online shop. What is a ‘Product’? For the marketing department, it is a description, photos, URL slug, and meta tags. For the warehouse manager, it's the physical dimensions of the box, weight, EAN barcode, and location on shelf B-12. For accounting, it's the VAT rate and commodity code.

If you try to put all of this into one Product class, you will create a monster. A Godzilla that has to be changed both when the graphic designer replaces a photo and when the warehouse replaces forklifts with narrower ones. Instead of modules, we have a monolithic ball of mud, only divided into nice subdirectories.

The myth of a single model

The cardinal sin is believing in the Holy Grail of architecture: the Canonical Data Model. The belief that there must be exactly one, definitive definition of what this unfortunate product is throughout the entire system.

This leads to ‘accidental complexity’ – complexity that does not stem from the business, but from our misguided technical decisions. Your ORM chokes on fetching 50 columns from the database just to display the product name on the wish list. We try to force the same object to be used for displaying the page (read-heavy, cacheable) and for reserving goods (write-heavy, transactionally strict).

Think in terms of roles, not data

Consider yourself. You have one personal identification number, you are one ‘entity’. But are ‘you’ at the tax office (taxpayer), “you” at the doctor's (patient) and ‘you’ at a party with friends the same entity?

Of course not. In each of these contexts, you play a different role. The tax office is not interested in your blood type, and the doctor is not interested in your PIT (usually). In each context, you perform different operations and need a different set of data.

So why do we try to use the same Product object in e-commerce to render the shop's HTML and to optimise the picking path in the warehouse?

True modularity (often synonymous with the Domain-Driven Design approach) is about modelling behaviours and roles, not data structures. The same physical row in the database can be represented by completely different models in different contexts of the system.

How does this work in practice?

Instead of a single Product model, let's divide the system into contexts: Storefront and Inventory.

In the Storefront context, we are interested in presentation. There is a lot of reading here, little business logic, and speed is what matters.

# Context: Storefront Catalog (Read Model)
# Optimized for display on the website. Ideally read from a cache or view.
class ProductDisplay < ActiveRecord::Base
  self.table_name = 'products' # Mapping to the same table, but...

  # We strictly limit what this model can do/see.
  # We DON'T care about stock location or supplier contracts here.

  scope :active, -> { where(published: true) }

  def seo_description
    "Buy #{name} for only #{price}!"
  end

  # No logic about stock reservation here!
end

And now the context of Magazine. Here, we don't care about SEO or photo colour. Here, hard business rules reign supreme: you can't sell something that isn't physically there (unless it's a backorder), you need to know if the goods will fit in a parcel locker. This is where ‘Essential Complexity’ lives.

We need an Aggregator that ensures consistency.

# Context: Inventory Management (DDD Aggregate)
# This model represents the physical aspect of the item in the warehouse.
module Inventory
  class StockItem
    attr_reader :sku, :dimensions, :stock_level

    def initialize(sku, dimensions, stock_level)
      @sku = sku
      @dimensions = dimensions # Value Object involving height, width, depth
      @stock_level = stock_level
    end

    # Behavior-focused method.
    # It enforces business rules: can we fulfill this order line?
    def reserve_stock!(quantity)
      if @stock_level < quantity
        raise InsufficientStockError, "Only #{@stock_level} items remaining"
      end

      # Logic to decrease stock, create reservation record etc.
      @stock_level -= quantity
      publish_event(:stock_reserved, sku: @sku, qty: quantity)
    end

    # We use raw SQL here to avoid loading unnecessary marketing descriptions
    # when we only need to check numbers and locks.
    def self.find_for_locking(product_id)
      # Locking row for update to prevent race conditions
      data = ActiveRecord::Base.connection.select_one(<<-SQL
        SELECT sku, width, height, depth, quantity_on_hand
        FROM product_inventory_data 
        WHERE product_id = #{product_id} 
        FOR UPDATE
      SQL
      )
      # Reconstruct the Aggregate...
    end
  end
end

Can you see the difference? ProductDisplay is a lightweight facade for reading. Inventory::StockItem is a rule enforcer that doesn't even know what the product is called on the website (because it operates on SKUs). They may use the same database, but they are separated by a wall in the code.

The cost of a shared database

Finally, the topic that hurts the most. If your ‘modules’ share the same tables in the database and everyone can mess with them, then you don't have modules. You have a distributed monolith.

This is where Conway's Law comes in. If the ‘Shop’ team and the ‘Warehouse’ team have to agree on every change to the products table, then separation dies. Sometimes it is worth accepting data redundancy (e.g. copying the product name to the warehouse table) in order to gain autonomy.

Summary

Let's stop designing systems by looking only at the database structure. It's a road to nowhere.

Start asking questions like ‘Why does this module exist?’ and ‘What behaviours does it implement?’ instead of ‘What data does it store?’. Break away from the dogma that table products = class Product. Allow the same business entity to have different representations. Your code will stop ‘falling apart’ and you will stop fearing Friday deployments.

Happy role-ing!