DRY in the Enterprise: How to Eliminate Domain Logic Duplication Across Legacy and Modern Systems
Every enterprise software engineer has faced that sinking feeling: you are debugging a data discrepancy or a validation bug, only to realize that the exact same business rule—say, validating a Brazilian CNPJ—is implemented in three different places, using three different snippets of code, across different modules or legacy stacks (like Progress ABL, PHP, or C#).
When the legal department updates its compliance rules or the central tax authority changes formatting requirements, updating one place leaves the others silently failing. This is the classic trap of domain logic duplication.
Let’s look at why this happens, the top functionalities that fall victim to it, and a step-by-step architectural playbook to centralize your code using Domain-Driven Design (DDD) patterns like Value Objects.
The Root Cause: Primitive Obsession and Siloed Teams
When systems evolve organically—especially in large enterprises modernizing legacy systems (like migrating Progress ABL monoliths to microservices)—teams often duplicate code because:
- Primitive Obsession: Treating domain concepts (like a CNPJ, a monetary amount, or an address) as primitive strings or numbers rather than rich domain objects.
- Siloed Development: Different squads working on distinct business domains (Legal vs. Customer Support) reinvent the wheel instead of sharing a common core.
- Legacy Boundaries: Bridging old technologies (Progress ABL, COBOL, legacy PHP) with modern stacks makes direct code sharing non-trivial.
Top 10 Most Common Functionalities Occasionally Duplicated in Systems
- Tax ID & Document Validation (CNPJ, CPF, SSN, VAT): The classic example where checksum algorithms and formatting rules get copy-pasted across registration, billing, and legal modules.
- Monetary & Currency Calculations: Rounding rules, currency conversions, and tax additions scattered across checkout, invoicing, and reporting services.
- Address Normalization & Postal Code (CEP/Zip) Lookups: Formatting strings, validating postal codes, and parsing street/number data independently in shipping and CRM tools.
- Date, Time & Business Day Calculations: Calculating due dates, skipping holidays, or handling timezone shifts across scheduling, billing, and SLA tracking components.
- Contact Information Formatting & Validation (Email & Phone Numbers): Regex checks and normalization logic scattered across user onboarding, marketing, and support portals.
- Data Masking & PII Sanitization: Hiding sensitive information (like credit card numbers or passwords) for logs and UI display, implemented differently per service.
- Status/State Machine Transitions: Checking if an order, contract, or ticket can transition from Status A to Status B, duplicated in UI validators and backend workflows.
- JWT Token Parsing & Claims Extraction: Manually decoding and validating auth tokens across multiple microservices instead of using a shared middleware/gateway.
- Encryption, Hashing, and Checksums: Implementing custom or inconsistent hashing algorithms for data integrity checks or token generation.
- Pagination, Sorting, and Query Filtering Builders: Re-implementing sorting and filtering logic across multiple administrative list endpoints.
Step-by-Step Process to Eliminate and Prevent Duplication
Step 1: Identify and Audit (The Domain Audit)
Run a code audit or static analysis to locate duplicated logic. Look for identical validation regexes, math formulas, or algorithm implementations across different classes and packages.
- Action: Group these findings by business domain (e.g., “Taxation,” “Identity,” “Billing”).
Step 2: Introduce Rich Domain Concepts via Value Objects
Instead of passing raw string or int variables across your system (Primitive Obsession), encapsulate the data and its validation rules inside an immutable Value Object.
For example, instead of validating a CNPJ string in both a Legal class and a Customer Support class, create a CNPJ Value Object:
// PHP Example: CNPJ Value Object enforcing immutability and self-validation
final class CNPJ
{
private string $value;
public function __construct(string $value)
{
$cleaned = preg_replace('/\D/', '', $value);
if (!$this->isValid($cleaned)) {
throw new InvalidArgumentException("Invalid CNPJ format or checksum.");
}
$this->value = $cleaned;
}
private function isValid(string $cnpj): bool
{
// Centralized validation algorithm lives here and ONLY here
if (strlen($cnpj) !== 14) return false;
// ... checksum verification logic ...
return true;
}
public function getValue(): string
{
return $this->value;
}
public function format(): string
{
// Centralized formatting (XX.XXX.XXX/XXXX-XX)
return vsprintf('%s.%s.%s/%s-%s', str_split($this->value, [2, 3, 3, 4, 2]));
}
}
Step 3: Establish a Single Source of Truth (SSOT)
- For Monoliths: Move the Value Object into a shared
DomainorCommoncore kernel package within the same repository. - For Microservices / Distributed Systems: Extract the logic into a shared internal library (e.g., a private Composer package, NuGet package, Maven artifact, or npm module) imported by all dependent services.
- For Cross-Language Barriers (e.g., Progress ABL to Modern C#/PHP): If you cannot share code libraries directly due to legacy runtime constraints, expose the rule via a lightweight internal micro-service or domain API, or strictly document and codify the algorithm in a shared specification repository.
Step 4: Enforce via Architecture Tests and Linting
To ensure developers don’t re-introduce duplicated inline checks:
- Use architecture testing tools (like ArchUnit for Java/C#, Deptrac for PHP, or custom ESLint rules for TypeScript) to enforce dependency rules.
- Set up CI/CD pipeline gates that fail builds if domain rules bypass the designated Value Objects or shared packages.
Key Recommendations & Architectural Best Practices
- Embrace Domain-Driven Design (DDD): Remember that Value Objects are immutable and have no conceptual identity—they are defined entirely by their attributes. This makes them exceptionally safe to share and reuse.
- Keep Core Logic Free of Infrastructure Concerns: Your validation Value Objects should not care about databases, HTTP requests, or frameworks. They should be pure, testable domain code.
- Refactor Opportunistically: Don’t stop everything to rewrite working legacy code, but apply the Boy Scout Rule: “Leave the codebase cleaner than you found it.” Whenever you touch a legacy module containing duplicated logic, extract it into the new shared standard.
Explaining the meaning of DRY
DRY stands for Don’t Repeat Yourself. It is one of the most fundamental principles of software engineering, popularized by Andy Hunt and Dave Thomas in their book The Pragmatic Programmer (1999).
At its core, the DRY principle states:
“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.”
What DRY Means in Real-World Software Engineering
DRY is often misunderstood as simply “never copy-pasting code.” While avoiding copy-pasted code is a big part of it, DRY goes deeper: it is about centralizing business logic and knowledge.
In the context of the HDI example from the blog post:
- The Problem: The Legal module and the Customer Support module (written in Progress ABL) both contained the logic for verifying whether a CNPJ checksum was valid.
- The Violation: The “knowledge” of what makes a valid CNPJ existed in two places.
- The Risk: If the Brazilian Federal Revenue (Receita Federal) changes how CNPJs are formatted or validated, or if one team fixes an edge-case bug in their checksum algorithm, the system is now out of sync. One part accepts a document while another rejects it.
When you adhere to DRY, you isolate that validation logic so that it lives in one place (e.g., inside a single CNPJ Value Object or shared module). If the logic needs to change, you update it once, and every consumer in the enterprise automatically inherits the change.
The Three Types of Duplication DRY Fights
- Logic Duplication: Writing the exact same algorithm, validation, or calculation in multiple classes or services.
- Data/Schema Duplication: Defining the structure of a customer or document in separate databases or DTOs without a single source of truth.
- Documentation/Developer Duplication: Documenting a rule in a wiki that conflicts with the code implementation, requiring manual synchronization.
The Fine Print: DRY vs. WET (Write Everything Twice)
While DRY is a core goal, engineers also watch out for over-abstracting too early:
- WET (“Write Everything Twice”): A common rule of thumb is to allow duplication up to two times before creating a shared abstraction. Premature abstraction can create tight coupling between two modules that coincidentally look similar today but serve different business intents tomorrow.
- Accidental Duplication vs. Knowledge Duplication: Two pieces of code might look identical (e.g., both check if a string length is 14), but if they represent different business rules that evolve independently, consolidating them can create unwanted coupling.
In enterprise architecture, DRY applied to true domain knowledge (like tax IDs, monetary rounding, or business rules) ensures system integrity and reduces cognitive load when maintaining legacy and modern systems side by side.

