Book Review of Object Design Style Guide by Matthias Noback
Look, the average Magento 2 codebase is a mess. You know it, I know it. We’ve all inherited modules where a Helper/Data class spans two thousand lines, doing everything from formatting prices to sending API requests. The service layer is a myth, often just a collection of static methods in a utility class. Entities are anemic, their behavior scattered across ten different resource models and plugins. This chaos makes debugging a nightmare and testing nearly impossible. Matthias Noback’s Object Design Style Guide provides a concrete, opinionated framework for cutting through that mess. It’s not about theory. It’s a set of enforceable rules for writing objects that behave predictably in a system as complex as Magento.
The book’s central argument is simple but radical: an object’s design should make its role and rules obvious at the point of use. Noback enforces this through strict stylistic constraints. He distinguishes between services (which perform tasks) and other objects (like entities, DTOs, value objects), giving each a specific design template. For a Magento developer drowning in legacy code, these templates are a lifeline.
The Service Contract, Actually Done Right
Magento talks a big game about service contracts. The official docs tell you to hide business logic behind interfaces. But they rarely tell you how to structure the implementing class. The result? You get a ProductRepositoryInterface implemented by a ProductRepository that’s 800 lines long, injecting ten dependencies and mixing SQL queries, cache logic, and event dispatch. It’s a service in name only.
Noback’s definition of a service is brutally strict. A service is stateless. Its methods define the tasks your application can perform. It can’t have public properties. It can only depend on other services or value objects. Its name should be a verb, like PlaceOrder or CalculateCatalogPrice. This forces you to think in terms of specific tasks, not god-object “managers.”
Here’s a terrible, typical Magento helper, and what Noback would tell you to do with it.
// BAD: A classic Magento "helper" that does everything.
class ProductHelper
{
protected $productRepository;
protected $imageHelper;
protected $scopeConfig;
public function __construct(
ProductRepositoryInterface $productRepository,
ImageHelper $imageHelper,
ScopeConfigInterface $scopeConfig
) {
$this->productRepository = $productRepository;
$this->imageHelper = $imageHelper;
$this->scopeConfig = $scopeConfig;
}
public function getProductData($sku)
{
$product = $this->productRepository->get($sku);
$data = [];
$data['name'] = $product->getName();
$data['price'] = $this->getFinalPrice($product);
$data['image'] = $this->imageHelper->getUrl($product);
$data['is_salable'] = $product->isSalable();
return $data;
}
public function getFinalPrice($product)
{
// Messy price logic with config checks
if ($this->scopeConfig->getValue('catalog/price/display_tax')) {
// ...
}
}
public function validateProduct($product) { /* ... */ }
public function getRelatedSkus($product) { /* ... */ }
}
This class is a dumping ground. It’s hard to test because getProductData does four unrelated things. It’s hard to reuse because it’s tightly coupled to concrete helpers and configuration.
According to Noback’s rules, you’d split this into focused services and proper value objects.
// A service with a single, clear task.
class ComposeProductDisplayData
{
private $productRepository;
private $priceResolver;
private $imageUrlProvider;
// Only inject other services.
public function __construct(
GetProductBySku $productRepository,
ResolveDisplayPrice $priceResolver,
ProvideProductImageUrl $imageUrlProvider
) {
$this->productRepository = $productRepository;
$this->priceResolver = $priceResolver;
$this->imageUrlProvider = $imageUrlProvider;
}
// One public method. It returns a value object, not an array.
public function forSku(string $sku): ProductDisplayData
{
$product = $this->productRepository->execute($sku);
return new ProductDisplayData(
$product->getName(),
$this->priceResolver->forProduct($product),
$this->imageUrlProvider->getForProduct($product),
$product->isSalable()
);
}
}
// A simple value object. It's immutable data with no dependencies.
final class ProductDisplayData
{
private string $name;
private Money $displayPrice;
private string $imageUrl;
private bool $isSalable;
public function __construct(string $name, Money $displayPrice, string $imageUrl, bool $isSalable)
{
$this->name = $name;
$this->displayPrice = $displayPrice;
$this->imageUrl = $imageUrl;
$this->isSalable = $isSalable;
}
// Only getters. No setters. The data is fixed after creation.
public function getName(): string { return $this->name; }
public function getDisplayPrice(): Money { return $this->displayPrice; }
public function getImageUrl(): string { return $this->imageUrl; }
public function isSalable(): bool { return $this->isSalable; }
}
The difference is night and day. ComposeProductDisplayData has one job. It’s easily testable by mocking its three dependencies. ProductDisplayData is a dumb, reliable data bag. You can’t mess it up. This pattern scales. Need a different format for the API? Write a new service, ComposeProductApiData, that uses the same core dependencies but outputs a different value object.
Entities vs. Value Objects: The Magento Blind Spot
Magento’s data model blurs the line between entities and value objects catastrophically. A Product model is a mutable, global singleton that also serves as a DTO for database storage, a parameter bag for form data, and a context object for business logic. It’s everything and nothing.
Noback’s book forces a clean separation. An entity has an identity (like a ProductId) and a lifecycle. Its state can change. A value object has no identity; it’s defined solely by its attributes (like Money or Address). It’s immutable.
Applying this to a custom Magento module is painful but transformative. Let’s say you’re building a subscription system. The lazy way is to add a subscription_data JSON column to the customer table and call it a day. The Noback way is to model the domain.
// Entity: Has an identity (SubscriptionId) and a lifecycle.
final class CustomerSubscription
{
private SubscriptionId $id;
private CustomerId $customerId;
private SubscriptionPlan $plan; // Value Object
private SubscriptionStatus $status; // Value Object
private ?DateTimeImmutable $pausedUntil;
private function __construct() {}
public static function enroll(CustomerId $customerId, SubscriptionPlan $plan): self
{
$subscription = new self();
$subscription->id = SubscriptionId::generate();
$subscription->customerId = $customerId;
$subscription->plan = $plan;
$subscription->status = SubscriptionStatus::active();
$subscription->pausedUntil = null;
return $subscription;
}
public function pause(DateTimeImmutable $until): void
{
if (!$this->status->canBePaused()) {
throw new \DomainException('Subscription cannot be paused in its current state.');
}
$this->pausedUntil = $until;
$this->status = SubscriptionStatus::paused();
}
// ... other behavior methods, NO public setters.
}
// Value Object: Defined by its attributes.
final class SubscriptionPlan
{
private string $code;
private Money $price;
private BillingInterval $interval;
public function __construct(string $code, Money $price, BillingInterval $interval)
{
// Validate on creation. It's immutable after.
if (empty($code)) { throw new \InvalidArgumentException('Plan code is required.'); }
$this->code = $code;
$this->price = $price;
$this->interval = $interval;
}
public function getCode(): string { return $this->code; }
public function getPrice(): Money { return $this->price; }
public function getInterval(): BillingInterval { return $this->interval; }
// Equality is based on all property values.
public function equals(self $other): bool
{
return $this->code === $other->code &&
$this->price->equals($other->price) &&
$this->interval->equals($other->interval);
}
}
This design puts business rules where they belong - inside the objects. The pause method enforces a rule (canBePaused). The SubscriptionPlan guarantees its own validity. Your service classes become orchestrators that retrieve entities, call their methods, and persist the changes. The model is boringly predictable. You can reason about it without tracing through fifteen plugin interceptors.
The Honest Verdict for Magento Devs
Is this style a silver bullet for Magento’s core? No. The platform’s legacy is too deep. But for your custom modules, it’s a game-changer. It makes your code resistant to the surrounding chaos. When you need to add a new feature, you extend the domain model, not hack a plugin onto a core class. Testing becomes trivial because your objects have clear boundaries and dependencies.
The book’s later sections on dividing responsibilities and changing behavior are basically a manual for writing clean plugins and preferences. It teaches you to wrap existing services cleanly, to use composition over inheritance - a concept Magento pays lip service to but rarely enforces.
We think this is the real value. The Object Design Style Guide gives you a vocabulary and a set of enforceable code review criteria. You stop asking “does this work?” and start asking “is this a service or an entity?”, “is this object immutable?”, “does this method name describe a task or a property?”. That shift in thinking saves more time than any fancy IDE plugin. It turns object-oriented programming from a syntax you use into a tool you actually control. For a Magento developer stuck maintaining someone else’s spaghetti, that’s not just helpful. It’s professional survival. Grab the book. Apply it to your next custom module. The difference will be obvious, and honestly, a little bit satisfying.