Miraas: Taming Complexity with Domain-Driven Design
Part 3 of a series exploring the technical challenges behind building Miraas, an Islamic inheritance calculator
Taming Complexity with Domain-Driven Design
After choosing our tech stack, the real challenge began: how do you model one of the most complex rule-based systems in existence? Islamic inheritance law isn’t just complicated - it’s systematically complicated, with layers of interdependent rules that would make any software architect lose sleep.
This is where Domain-Driven Design (DDD) saved us. Not the heavyweight, enterprise-y version with repositories and aggregates everywhere, but the core principle: make your code mirror the domain.
The Architecture That Emerged
Looking at our domain folder, you’ll see a clean separation that directly reflects how Islamic inheritance actually works:
Domain/
├── InheritanceCase.cs // The aggregate root
├── DeceasedPerson.cs // Core entity
├── Heir.cs // Value objects and hierarchy
├── BlockingEngine.cs // Who blocks whom
├── ShareEngine.cs // How shares are calculated
├── CalculationEngine.cs // Orchestrates everything
└── DomainExtensions.cs // Domain-specific queries
This isn’t accidental. Each file represents a distinct concept in Islamic jurisprudence, and the relationships between them mirror the actual legal relationships.
The Aggregate Root: InheritanceCase
In DDD terms, InheritanceCase is our aggregate root - the single entry point that maintains consistency across all related entities:
public class InheritanceCase
{
List<Heir> heirs = new();
public DeceasedPerson Deceased { get; }
public IEnumerable<Heir> Heirs => heirs;
public decimal? EstateValue { get; set; }
public void AddHeir(Heir heir) { /* validation and consistency */ }
public bool HasHeir(RelationType relationType) { /* domain queries */ }
public int GetHeirCount(RelationType relationType) { /* business logic */ }
}
Notice what’s happening here. This isn’t just a data container - it’s a domain concept. An inheritance case in Islamic law is exactly this: a deceased person, their heirs, and the estate to be distributed.
Why Not Just a DTO?
Coming from Java or Python, you might think: “Why not just use a simple data class or dictionary?” Here’s why that fails:
# Python approach - looks simpler but hides complexity
inheritance_case = {
'deceased': {'gender': 'male'},
'heirs': [
{'type': 'son', 'count': 2},
{'type': 'wife', 'count': 1}
],
'estate_value': 100000
}
# But now every function needs to validate this structure
def calculate_shares(case_dict):
# Is 'deceased' present? Is 'gender' valid?
# Are 'heirs' in the right format?
# This validation logic gets repeated everywhere
Compare to our C# approach:
var inheritanceCase = new InheritanceCase(new DeceasedPerson(Gender.Male));
inheritanceCase.AddHeir(new Son(2));
inheritanceCase.AddHeir(new Wife(1));
inheritanceCase.EstateValue = 100000;
// Impossible to create invalid state
// Validation happens at construction time
// IntelliSense guides you to valid operations
The aggregate root enforces invariants. You can’t accidentally create an inheritance case with a husband and a male deceased person (impossible in Islamic law). The type system prevents these errors at compile time.
The Heir Hierarchy: When OOP Shines
One area where object-oriented programming truly excels is modeling hierarchical relationships. Islamic inheritance has clear hierarchies - different types of heirs with different rules:
public abstract class Heir
{
public abstract GenderType Gender { get; }
public abstract RelationType Relation { get; }
public int Count { get; set; } = 1;
public ShareResult Result => this.shareResult;
// Each heir knows how to represent itself
public override string ToString() => /* domain-specific formatting */
}
// Concrete implementations
public class Son : Heir
{
public override GenderType Gender => GenderType.Male;
public override RelationType Relation => RelationType.Son;
}
public class Daughter : Heir
{
public override GenderType Gender => GenderType.Female;
public override RelationType Relation => RelationType.Daughter;
}
The Power of Polymorphism
This hierarchy enables powerful polymorphic behavior. When we iterate through heirs, each one knows its own identity and rules:
foreach (var heir in inheritanceCase.Heirs)
{
// Each heir polymorphically provides its relation type
var share = shareEngine.GetShare(heir.Relation, inheritanceCase);
heir.AddShare(new ShareResult(share.ShareFraction));
}
In Python, you’d need explicit type checking:
for heir in inheritance_case['heirs']:
if heir['type'] == 'son':
# Handle son logic
elif heir['type'] == 'daughter':
# Handle daughter logic
# ... 15+ more elif statements
In Java, you’d have similar polymorphism, but with more boilerplate:
public abstract class Heir {
public abstract GenderType getGender();
public abstract RelationType getRelation();
// Getters, setters, constructors...
}
public class Son extends Heir {
@Override
public GenderType getGender() { return GenderType.MALE; }
@Override
public RelationType getRelation() { return RelationType.SON; }
// More boilerplate...
}
C#’s properties and expression-bodied members make this much cleaner while maintaining the same type safety.
The Three Engines: Separation of Concerns
The heart of our architecture is the separation into three distinct engines, each handling a specific aspect of Islamic inheritance:
1. BlockingEngine: The Gatekeeper
public class BlockingEngine
{
readonly List<BlockingRule> rules;
public List<RelationType> GetBlockedHeirs(InheritanceCase inheritanceCase)
{
var blocked = new HashSet<RelationType>();
foreach (var rule in rules)
if (inheritanceCase.HasHeir(rule.Blocker))
blocked.Add(rule.Blocked);
// Complex conditional blocking logic...
return blocked.ToList();
}
}
This engine answers one question: “Who is prevented from inheriting?” In Islamic law, certain heirs block others completely. Sons block uncles, fathers block many siblings, etc.
2. ShareEngine: The Calculator
public class ShareEngine
{
readonly List<ShareRule> rules;
public (Fraction ShareFraction, string ShareExplanation) GetShare(
RelationType heirType, InheritanceCase inheritanceCase)
{
var rule = rules.FirstOrDefault(r => r.Heir == heirType);
return rule?.Rule(inheritanceCase) ?? (Fraction.Zero, "No rule found");
}
}
This engine handles the mathematical calculations. Each heir type has rules that determine their share based on the family structure.
3. CalculationEngine: The Orchestrator
public class CalculationEngine
{
readonly BlockingEngine blockingEngine = new();
readonly ShareEngine shareEngine = new();
public CalculationResult Calculate(InheritanceCase inheritanceCase)
{
// 1. Validate input
// 2. Apply blocking rules
// 3. Calculate fixed shares
// 4. Handle Awl (overflow) scenarios
// 5. Distribute residue
// 6. Apply Radd (underflow) distribution
}
}
The calculation engine orchestrates the entire process, handling the complex interactions between blocking and share calculation.
Why This Separation Matters
Single Responsibility Principle
Each engine has one job:
- BlockingEngine: Determines eligibility
- ShareEngine: Calculates individual shares
- CalculationEngine: Orchestrates the process
This makes testing much easier. You can test blocking logic independently of share calculations, and share calculations independently of the orchestration logic.
Testability
[Test]
public void Son_Blocks_Grandfather()
{
var case = new InheritanceCase(new DeceasedPerson(Gender.Male));
case.AddHeir(new Son());
case.AddHeir(new Grandfather());
var blocked = blockingEngine.GetBlockedHeirs(case);
Assert.That(blocked, Contains.Item(RelationType.Grandfather));
}
Clean, focused tests that verify specific business rules without the complexity of the full calculation process.
Maintainability
When Islamic scholars review our calculations and suggest changes, we know exactly where to look:
- Blocking rule changes →
BlockingEngine - Share calculation changes →
ShareEngine - Process flow changes →
CalculationEngine
The Fraction Abstraction: Domain-Specific Types
One of the most important design decisions was creating a custom Fraction type instead of using decimals or floats:
public struct Fraction : IEquatable<Fraction>, IComparable<Fraction>
{
public int Numerator { get; private set; }
public int Denominator { get; private set; }
// Exact arithmetic - no floating point errors
public static Fraction operator +(Fraction a, Fraction b)
{
int numerator = a.Numerator * b.Denominator + b.Numerator * a.Denominator;
int denominator = a.Denominator * b.Denominator;
return new Fraction(numerator, denominator);
}
// Factory methods for Islamic inheritance fractions
public static Fraction Half => new Fraction(1, 2);
public static Fraction Third => new Fraction(1, 3);
public static Fraction Quarter => new Fraction(1, 4);
public static Fraction Sixth => new Fraction(1, 6);
public static Fraction Eighth => new Fraction(1, 8);
public static Fraction TwoThirds => new Fraction(2, 3);
}
Why Not Decimal?
// ❌ Decimal precision issues
decimal third = 1.0m / 3.0m; // 0.333333...
decimal sixth = 1.0m / 6.0m; // 0.166666...
decimal sum = third + sixth; // 0.499999... ≠ 0.5
// ✅ Exact fraction arithmetic
Fraction third = Fraction.Third; // 1/3
Fraction sixth = Fraction.Sixth; // 1/6
Fraction sum = third + sixth; // Exactly 1/2
In religious calculations, precision isn’t just nice to have - it’s required. The Quran specifies exact fractions, and our calculations must preserve that exactness.
Domain-Specific Language
The factory methods create a domain-specific language:
// This reads like Islamic jurisprudence
if (inheritanceCase.DeceasedHasDescendants())
return Fraction.Quarter; // Husband gets 1/4 with children
else
return Fraction.Half; // Husband gets 1/2 without children
Compare to generic decimal arithmetic:
// This reads like generic math
if (inheritanceCase.DeceasedHasDescendants())
return 0.25m; // What does 0.25 mean in this context?
else
return 0.5m; // Is this precise enough?
C# Features That Made This Possible
Expression-Bodied Members
public abstract class Heir
{
public abstract GenderType Gender { get; }
public abstract RelationType Relation { get; }
// Clean, concise property definitions
public ShareResult Result => this.shareResult;
}
In Java, this would require explicit getter methods with more boilerplate.
Collection Initializers
rules = [
new BlockingRule(RelationType.Son, RelationType.Grandfather),
new BlockingRule(RelationType.Father, RelationType.Grandfather),
new BlockingRule(RelationType.Son, RelationType.SonOfSon),
// ... more rules
];
This declarative syntax makes the blocking rules read like a configuration file, which is exactly what they are - a configuration of Islamic inheritance law.
Tuples for Multiple Return Values
public (Fraction ShareFraction, string ShareExplanation) GetShare(
RelationType heirType, InheritanceCase inheritanceCase)
{
// Return both the calculation and explanation
return (Fraction.Third, "Mother: 1/3 of estate (no children or siblings)");
}
This is cleaner than creating a custom result class for every method that needs to return multiple values.
Pattern Matching (C# 8+)
string generateExplanation(RelationType relation) => relation switch
{
RelationType.UterineBrother or RelationType.UterineSister =>
"Uterine sibling (same mother, different father)",
RelationType.FullBrother or RelationType.FullSister =>
"Full sibling (same parents)",
RelationType.ConsanguineBrother or RelationType.ConsanguineSister =>
"Consanguine sibling (same father, different mother)",
_ => $"Heir: {relation}"
};
This is more concise and safer than traditional switch statements.
Domain Extensions: Readable Business Logic
One of the most underrated aspects of our design is the DomainExtensions class:
static class DomainExtensions
{
public static bool DeceasedHasChildren(this InheritanceCase i) =>
i.HasHeir(RelationType.Son) || i.HasHeir(RelationType.Daughter);
public static bool DeceasedHasDescendants(this InheritanceCase i) =>
DeceasedHasChildren(i) || DeceasedHasGrandChildren(i);
public static bool DeceasedHasMaleDescendantsOrAscendants(this InheritanceCase i) =>
DeceasedHasMaleDescendants(i) || DeceasedHasMaleAscendants(i);
}
These extension methods transform complex boolean logic into readable domain language:
// Before: Hard to understand
if (inheritanceCase.HasHeir(RelationType.Son) ||
inheritanceCase.HasHeir(RelationType.Daughter) ||
inheritanceCase.HasHeir(RelationType.SonOfSon) ||
inheritanceCase.HasHeir(RelationType.DaughterOfSon))
// After: Reads like Islamic jurisprudence
if (inheritanceCase.DeceasedHasDescendants())
This is domain-driven design in action - making the code speak the language of the domain experts.
Error Handling: When to Fail Fast vs. When to Warn
One crucial aspect of our design is how we handle different types of errors:
public class CalculationResult
{
public bool IsSuccessful { get; set; }
public string? ErrorMessage { get; set; }
public bool RequiresScholarlyReview { get; set; }
public List<string> Warnings { get; set; } = new();
}
We distinguish between:
- Hard errors: Invalid input that prevents calculation
- Warnings: Unusual scenarios that need attention
- Scholarly review flags: Edge cases requiring human judgment
// Hard error - fail fast
if (inheritanceCase.Heirs.Count() == 0)
return CalculationResult.Failure("No heirs specified.");
// Warning - calculate but flag
if (totalFixed != Fraction.One)
{
result.RequiresScholarlyReview = true;
result.Warnings.Add($"Total fraction is {totalFixed}, not 1");
}
This reflects the reality of Islamic inheritance law - some scenarios are clearly invalid, while others are valid but require scholarly interpretation.
Lessons for Java and Python Developers
Type Safety Matters
Coming from Python’s dynamic typing, C#’s compile-time checks might seem restrictive. But in complex domains, they’re a lifesaver:
# Python - runtime error waiting to happen
def calculate_share(heir_type, inheritance_case):
if heir_type == "son": # Typo: should be "Son"
return 0.5
# This bug only surfaces when this code path executes
// C# - compile-time safety
public Fraction GetShare(RelationType heirType, InheritanceCase inheritanceCase)
{
if (heirType == RelationType.Son) // Enum prevents typos
return Fraction.Half;
// Impossible to have typos in enum values
}
Properties vs. Getters/Setters
Java developers often write:
public class Heir {
private RelationType relation;
public RelationType getRelation() {
return relation;
}
public void setRelation(RelationType relation) {
this.relation = relation;
}
}
C# properties are much cleaner:
public abstract class Heir
{
public abstract RelationType Relation { get; } // Read-only property
public int Count { get; set; } = 1; // Read-write with default
}
Extension Methods vs. Utility Classes
Instead of static utility classes:
public class InheritanceUtils {
public static boolean hasDescendants(InheritanceCase case) {
// implementation
}
}
// Usage
if (InheritanceUtils.hasDescendants(inheritanceCase))
C# extension methods feel more natural:
// Usage reads like a method call on the object
if (inheritanceCase.DeceasedHasDescendants())
The Architecture’s Payoff
This domain-driven architecture paid dividends throughout development:
- Testing: Each component can be tested in isolation
- Debugging: Problems are localized to specific engines
- Maintenance: Changes map directly to domain concepts
- Documentation: The code structure mirrors Islamic jurisprudence
- Validation: Domain experts can review code that matches their mental models
When an Islamic scholar reviews our blocking rules, they see:
new BlockingRule(RelationType.Son, RelationType.Grandfather),
new BlockingRule(RelationType.Father, RelationType.Grandfather),
This directly corresponds to their understanding: “Sons block grandfathers, fathers block grandfathers.”
What’s Next
In our next post, we’ll dive deep into the BlockingEngine - the most complex part of our system. We’ll explore how we modeled the intricate relationships that determine which heirs can inherit and which are blocked, and how we optimized the blocking calculations to avoid exponential complexity.
The foundation we built with domain-driven design gave us the structure to tackle these complex problems systematically.
When your code mirrors your domain, both become easier to understand.
Miraas is available at https://miraas.azurewebsites.net - but remember, it’s a tool to assist understanding, not replace scholarly consultation.
More from Miraas Inheritance Calculator
- Miraas: A Software Engineering Perspective on Islamic Inheritance Calculation
- Miraas: Choosing Boring Over Shiny
- Miraas: Taming Complexity with Domain-Driven Design (Current)
- Miraas: A Blast from the Past - Logic Programming Meets Modern C#