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:

  1. Testing: Each component can be tested in isolation
  2. Debugging: Problems are localized to specific engines
  3. Maintenance: Changes map directly to domain concepts
  4. Documentation: The code structure mirrors Islamic jurisprudence
  5. 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.