Miraas: Choosing Boring Over Shiny
Part 2 of a series exploring the technical challenges behind building Miraas, an Islamic inheritance calculator
Tech Stack Choices: Why We Chose Boring Over Shiny
The Temptation of the Shiny
When starting Miraas, the temptation was real. React with TypeScript? Angular with RxJS? Vue with Composition API? Maybe throw in some microservices, Docker containers, and a NoSQL database for good measure? After all, what’s a modern web application without at least three JavaScript frameworks and a distributed architecture?
But here’s the thing about building software for sensitive domains: boring wins.
The Safe Choice: C# and ASP.NET Core
After years of experience with C#, I made what some might call a “boring” choice. But when you’re dealing with religious calculations that could affect someone’s spiritual standing and family relationships, boring becomes a feature, not a bug.
Why Not the JavaScript Ecosystem?
Don’t get me wrong - React, Angular, and Vue are fantastic technologies. But they bring complexity that we simply didn’t need:
- Build toolchain complexity: Webpack configurations, Babel transforms, TypeScript compilation
- State management overhead: Redux, MobX, Vuex - all solving problems we don’t have
- Dependency hell: NPM packages with transitive dependencies that change weekly
- Runtime errors: JavaScript’s dynamic nature means errors surface at runtime, not compile time
When you’re implementing complex business rules like Islamic inheritance law, the last thing you want is to debug whether your calculation is wrong or your build pipeline is broken.
// This looks innocent but can fail in so many ways
const motherShare = hasChildren ? 1/6 : 1/3;
// What if hasChildren is undefined? null? "false"?
// JavaScript will happily calculate with these values
Compare this to C#:
// Compile-time safety and explicit types
public Fraction GetMotherShare(bool hasChildren)
{
return hasChildren ? Fraction.Sixth : Fraction.Third;
}
// If hasChildren isn't a bool, this won't compile
C#: The Multi-Paradigm Advantage
C# gets unfairly dismissed as “just another OOP language,” but it’s evolved into something much more powerful. It offers the perfect blend of object-oriented structure and functional programming capabilities that made our domain modeling both expressive and safe.
Object-Oriented Domain Modeling
Our Heir hierarchy showcases how OOP can elegantly represent real-world concepts:
public abstract class Heir
{
public abstract RelationType Relation { get; }
public int Count { get; set; } = 1;
public ShareResult Result => this.shareResult;
// Each heir type knows its own identity
}
public class Son : Heir
{
public override RelationType Relation => RelationType.Son;
}
public class Daughter : Heir
{
public override RelationType Relation => RelationType.Daughter;
}
This isn’t just code organization - it’s domain modeling. Each heir type encapsulates its own behavior and identity. When Islamic law says “sons inherit differently than daughters,” our code structure mirrors that reality.
Functional programming purists might argue for a more data-centric approach, but sometimes the real world is hierarchical. Inheritance relationships in Islamic law map naturally to class inheritance in code.
The Power of Custom Abstractions
The Fraction abstraction demonstrates how C#’s infrastructure capabilities enable rich domain modeling:
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);
}
This isn’t just a data structure - it’s a domain-specific language. When we write Fraction.Third + Fraction.Sixth, we’re expressing Islamic inheritance concepts in code that’s both mathematically precise and semantically clear.
Functional Programming: Functions as First-Class Citizens
Here’s where C#’s functional capabilities shine. Our rule engines demonstrate how thinking in functions can solve complex problems elegantly.
The ShareEngine: Rules as Functions
class ShareRule
{
public RelationType Heir { get; }
public Func<InheritanceCase, (Fraction, string)> Rule { get; }
public ShareRule(RelationType heir, Func<InheritanceCase, (Fraction, string)> rule)
{
Heir = heir;
Rule = rule ?? throw new ArgumentNullException(nameof(rule));
}
}
Each inheritance rule is a function that takes an InheritanceCase and returns a share calculation. This functional approach allows us to express complex conditional logic naturally:
new ShareRule(RelationType.Mother, inheritanceCase =>
{
if (inheritanceCase.DeceasedHasDescendants())
return (Fraction.Sixth, "Mother: 1/6 of estate (with children or siblings)");
else if (inheritanceCase.DeceasedHasSiblings())
return (Fraction.Sixth, "Mother: 1/6 of estate (with children or siblings)");
else if (!inheritanceCase.HasSpouse() && !inheritanceCase.HasHeir(RelationType.Father))
return (Fraction.Third, "Mother: 1/3 of estate (no children or siblings)");
else
return (Fraction.Zero, "Mother: 1/3 of residue (no children/sibling/spouse/father");
})
This is functional programming in action - rules are data, functions are values, and complex logic becomes composable and testable.
The BlockingEngine: Declarative Rules
public class BlockingEngine
{
readonly List<BlockingRule> rules;
public BlockingEngine()
{
rules =
[
new BlockingRule(RelationType.Son, RelationType.Grandfather),
new BlockingRule(RelationType.Father, RelationType.Grandfather),
new BlockingRule(RelationType.Son, RelationType.SonOfSon),
// ... more rules
];
}
}
Instead of complex if-else chains, we declare blocking relationships as data. The engine processes these rules functionally, making the system both readable and maintainable.
The OOP Mindset Trap
Years of OOP programming can create mental constraints. When faced with complex business rules, many developers (especially Java programmers) immediately think: “I need a Rules Engine!”
Suddenly you’re evaluating Drools, jBPM, or other heavyweight frameworks. You’re learning new DSLs, dealing with rule compilation, and debugging rule execution engines. What started as a domain problem becomes a technology problem.
But look at our solution: under 500 lines of code for the entire rule system. No external dependencies, no complex frameworks, just functions and data structures.
// This is our entire "rules engine"
public (Fraction ShareFaction, string ShareExplaination) GetShare(RelationType heirType, InheritanceCase inheritanceCase)
{
var rule = rules.FirstOrDefault(r => r.Heir == heirType);
if (rule == null)
return (Fraction.Zero, "No rule found");
else
{
(var s, var e) = rule.Rule(inheritanceCase);
return (s, string.IsNullOrWhiteSpace(e) ? generateExplanation(heirType) : e);
}
}
Sometimes the simplest solution is the best solution.
Modern Alternatives: Go, Rust, and the Functional Renaissance
You might wonder: “What about newer languages like Go or Rust? Or purely functional languages like F# or Haskell?”
These are excellent choices, each with their strengths:
Go offers simplicity and excellent concurrency, but lacks the rich type system that makes domain modeling expressive. Our Fraction type would be more verbose, and we’d lose compile-time guarantees that make inheritance calculations safe.
Rust provides memory safety and powerful pattern matching, but the learning curve is steep. When you’re racing to solve domain problems, fighting the borrow checker isn’t where you want to spend your time.
F# would actually be fantastic for this problem - it’s functional-first, has excellent pattern matching, and runs on .NET. But team familiarity matters, and C# gave us 80% of F#’s benefits with 20% of the learning curve.
Haskell would make functional programming purists happy, but try explaining monads to a team when you need to ship inheritance calculations.
C#’s Multiple Personality Disorder
C# has what I like to call “multiple personality disorder” - and that’s actually a feature. In a single codebase, you can:
- Write object-oriented domain models (
Heirhierarchy) - Use functional programming for business rules (
ShareRulefunctions) - Leverage imperative programming for algorithms (Awl calculations)
- Apply declarative approaches for configuration (blocking rules)
// OOP: Polymorphic behavior
public abstract class Heir { ... }
// Functional: Rules as functions
Func<InheritanceCase, (Fraction, string)> rule = case => ...;
// Imperative: Step-by-step calculation
if (totalFixed > Fraction.One) {
var awlDenominator = totalFixed;
foreach (var heir in fixedShareHeirs) {
// ... step by step logic
}
}
// Declarative: Rules as data
rules = [
new BlockingRule(RelationType.Son, RelationType.Grandfather),
new BlockingRule(RelationType.Father, RelationType.Grandfather),
];
This flexibility lets you choose the right paradigm for each problem, rather than forcing everything into a single approach.
The Decades-Old Advantage
C# being “decades old” isn’t a weakness - it’s a strength. The language has evolved through real-world usage, incorporating the best ideas from functional programming while maintaining its OOP foundation.
Unlike newer languages that start with strong opinions, C# has learned from experience. It added:
- LINQ (functional query syntax)
- Pattern matching (from functional languages)
- Immutable records (from functional programming)
- Async/await (from concurrent programming)
All while maintaining backward compatibility and a massive ecosystem.
Why This Matters for Domain-Complex Applications
When building software for sensitive domains like religious calculations, legal systems, or financial applications, your technology choices should minimize risk, not add it.
Boring technology choices mean:
- More time solving domain problems, less time fighting tools
- Easier debugging when (not if) issues arise
- Better long-term maintainability
- Reduced cognitive load for team members
- Fewer moving parts that can break
The domain is complex enough - Islamic inheritance law has thousands of edge cases. The last thing you need is technology that adds more complexity.
Lessons for Technology Selection
- Match complexity to necessity - Use complex technology only when it solves real problems
- Favor compile-time safety - Catch errors before they reach users
- Choose familiar over fashionable - Team productivity matters more than resume-driven development
- Embrace multi-paradigm approaches - Different problems need different solutions
- Value ecosystem maturity - Boring technologies have better tooling and documentation
The Path Forward
In our next post, we’ll dive deep into the blocking engine - how we modeled the complex relationships that determine which heirs can inherit and which are blocked. We’ll explore how functional programming concepts helped us create a system that’s both powerful and maintainable.
But the foundation we built with “boring” C# gave us the stability to focus on what really matters: getting the inheritance calculations right.
Sometimes the most innovative thing you can do is choose the most boring technology that works.
Miraas is available at https://miraas.azurewebsites.net - but remember, it’s a tool to assist understanding, not replace scholarly consultation.