Adventures in the Modern Monolith: How do you keep a secret at Tilt?
When 50+ Azure App Service instances restart simultaneously and all reach for the same Key Vault, things break.
The boundaries between Object-Oriented Programming (OOP) and Functional Programming (FP) are increasingly blurring, creating exciting opportunities for developers. Languages traditionally rooted in functional paradigms are incorporating OOP patterns, while OOP languages like C# are embracing functional features. As a result, developers can leverage the strengths of both paradigms to write more efficient and maintainable code. This post delves into four functional concepts that have transformed the author’s C# coding practices, highlighting how immutability, state management, and LINQ can simplify complex logic and reduce bugs in your codebase.
The lines between OOP and Functional programming are becoming blurry— and that is a great thing for developers! Functional programming languages are adopting object oriented patterns, and object oriented languages like C# are embracing concepts that have made functional programming so powerful.
A significant amount of complexity has been abstracted away in languages like Scala and F# as they have matured, which has empowered their developer communities to distil wisdom and principles that have found their way into more object oriented languages such as C#. This has yielded enormous benefit in particular to OO developers. For example, here’s an excellent UC Davis study quantifying the reduction of bugs by using functional programming.
In this post, I’m going to share with you 4 ways functional concepts have changed my C# coding patterns for the better.
When I think of immutability, there are two categories that come to mind: Immutable Types, and Immutable Variable Assignment. The C# documentation covers Immutable Types very well, so I’ll leave those out of the scope of this blog. But if you’re keen, these include records, get-only properties, and immutable collections — I highly recommend reading up on and adopting these!
Lets focus on Immutable Variable Assignment.
Many languages provide at least one type of keyword that specifies that an assigned value can not be changed once it is set. In other words, once the variable is assigned - it becomes immutable. For example, in JS you can define const and let variables. const is immutable - you cannot reassign its value. In Scala you have val and var.
In C#, we simply have var. And although you can define const assignments, these assignments must be compile-time constant - and that’s not what we’re after here. We’re really talking about variables whose value cannot be changed after assignment at runtime.
At Empower, we’re doing our best to bring fair credit to everyone. This means we run a lot of experiments to see what works best for our customers. Recently we ran an experiment to determine which of our Thrive eligible customers who are also eligible for our Cash Advance product, would be most likely to actually sign up for the Cash Advance product.
Below is how I would have written the code for this before incorporating the concept of immutability daily design practice:
var showCashAdvanceBeforeThrive = false;
var cards = new List<Card>();
if (_featureFlagService.IsFeatureEnabled("ShowCashAdvanceFirst"))
{
showCashAdvanceBeforeThrive = true;
cards = dto.Cards
.OrderByDescending(x => x.CardType == CardType.CashAdvance ? 0 : 1)
.ThenBy(x=> x.CardType == CardType.Thrive ? 0 : 1)
.ToList();
} else {
cards = dto.Cards;
}
_eventService.Fire(new MarketingUserEvent(
userPublicKey,
"OnboardingOffersReturned",
new Dictionary<string, string>
{
{ nameof(showCashAdvanceBeforeThrive), showCashAdvanceBeforeThrive }
}
));
return cards;
In the above code, consider the cognitive load we’re adding to the person who comes next by reassignment of showCashAdvanceBeforeThrive so we can fire the correct marketing Event as well as cards based on the feature flag / ordering.
Now consider the code below, where we utilize C#’s ternary conditional operator to have a single point of assignment, as well as our (now proactive) declaration of the feature flag variable.
var showCashAdvanceBeforeThrive = _featureFlagService.IsFeatureEnabled("ShowCashAdvanceFirst");
var cards = showCashAdvanceBeforeThrive
? dto.Cards
.OrderByDescending(x => x.CardType == CardType.CashAdvance ? 0 : 1)
.ThenBy(x => x.CardType == CardType.Thrive ? 0 : 1)
.ToList()
: dto.Cards;
_eventService.Fire(new MarketingUserEvent(
userPublicKey,
"OnboardingOffersReturned",
new Dictionary<string, string>
{
{ nameof(showCashAdvanceBeforeThrive), showCashAdvanceBeforeThrive }
}
));
return cards;
In an ideal world, var could be replaced with val, making any reassignment of showCashAdvanceBeforeThrive a compile time failure, but we’re able to make do here by structuring our code so that this value is never reassigned - treating it as an immutable assignment.
In my opinion - having explicit keywords to enforce variable immutability encourages us to write our code more clearly. With constructs like this in place, it means we have to think less to fall into the pit of success - our modern IDEs can even offer us refactor suggestions to conform. This feature has actually even been requested of the C# dev team - but alas, it has not been adopted.
As I’ve progressed in my career, I’ve learned to let the person closest to the information have the loudest voice. I apply this same concept to programming— unless a reason to break the pattern exists, keep the code that makes similar decisions within it’s own domain. This is often discussed by referencing interfaces that mirror the business domain, but in this case let’s focus on allowing the object itself to contain the logic that creates the state in which it’s in instead of letting the calling code handle it.
One example is our server driven UI. By dynamically telling the client applications what to render when a user views the homepage, we can find ways to better serve our customers without having to release new versions of our Android & iOS applications.

Traditionally we would think of an object oriented language handling the error state similar to this. Specifically notice how we’re checking for validation errors and - if we find them - are mutating the objects state:
var cashAdvanceTile = new CashAdvanceTile();
if (validationErrors.Any())
{
cashAdvanceTile.AddErrors(validationErrors);
return cashAdvanceTile;
}
cashAdvanceTile.AddData(data);
While there’s nothing inherently wrong with this, code reused in this manner results in code complexity that’s just not necessary. One way of keeping the state immutable is to introduce static methods that essentially act as constructors (sometime referred to as factory methods):
public CashAdvanceTile(HomeFeedData homeFeedData)
{
HomefeedData = homeFeedData;
}
public static CashAdvanceTile GenerateErrorEntity(ValidationErrors validationErrors)
{
return new CashAdvanceTile(HomeFeedTileStatus.Error, validationErrors);
}
This allows us to do the following:
if (validationErrors.Any())
{
return CashAdvanceTile.GenerateErrorEntity(validationErrors);
}
return new CashAdvanceTile(homeFeedData);
What I like about this is now you do not have to know anything about the object to know how to generate an Error state. No missing IsError property that you can forget to set, as an example.
LINQ is extremely functional in nature, stacking functions on top of themselves, often with the function taking in a function. When I left the C# ecosystem for a few years, it was one of the features I missed the most.
Let’s start off with an example of finding all users with account balances under $100 without adopting the higher order functions that LINQ provides us with.
var usersWithLowAcccountBalances = new List<User>();
foreach (var user in users)
{
if (user.IsActive && user.AccountBalance < 100.00d)
{
usersWithLowAcccountBalances.Add(user);
}
}
By utilizing LINQ’s ability to take in a function, we can make the code more concise and avoid adding to the list over and over, which makes tracing the code much harder and more bug prone in larger code bases.
var usersWithLowAcccountBalances = users
.Where(user => user.IsActive)
.Where(user => user.AccountBalance < 100.00d);
Taking this a step further, let’s say that we found a pattern of behavior where we wanted to filter out low account balances in multiple areas of the application. C# has fantastic support for extension methods that allow us to combine common operations into one place and then also easily extend those with LINQ’s syntax for both database and in memory collections.
public static IEnumerable<User> FilterLowAccountBalances(this List<User> source)
{
return source
.Where(user => user.IsActive)
.Where(user => user.AccountBalance < 100.00d);
}
// now we can easily find users with a low account balance by zip code
var usersWithLowAcccountBalances = users
.FilterLowAccountBalances()
.Where(user => user.ZipCode.Equals("12345"));
Another example is using LINQ to filter down the data that you act on, so you can simplify nested functions. For example, when a user reactivates an account that we had previously scrubbed sensitive information, we can do the following:
var accountsToUpdate = await _dbContext
.Accounts
.Where(x => x.RoutingNumber == institution.RoutingNumber)
.OrderByDescending(x=> x.AccountId)
.ToListAsync();
foreach (var accountToUpdate in accountsToUpdate)
{
var haveWePurgedAccountInformation =
string.IsNullOrEmpty(accountToUpdate.RoutingNumber)
|| string.IsNullOrEmpty(accountToUpdate.AccountNumber);
if (!haveWePurgedAccountInformationAfter90Days)
{
continue;
}
accountToUpdate.RoutingNumber = institution.RoutingNumber;
}
await _empowerMssqlDb.SaveChangesAsync();
This code relies on nested logic that is hard to read as it grows over time. Utilizing LINQ to apply the filtering we saw inside of the foreach dramatically reduces the cognitive complexity needed to read and understand this code.
var accountsToUpdate = await _dbContext
.Accounts
.Where(x =>
x.RoutingNumber == institutionAccount.RoutingNumber)
.Where(x =>
string.IsNullOrEmpty(x.RoutingNumber) || string.IsNullOrEmpty(x.AccountNumber))
.OrderByDescending(x=> x.AccountId)
.ToListAsync();
accountsToUpdate.ForEach(x=> x.RoutingNumber = institution.RoutingNumber);
await _dbContext.SaveChangesAsync();
Of course, functional purists will point out this is a side effect— but like any best practice, we adopt what works best for us!

👋 I’m James, a seasoned backend developer with interest in crafting solutions on the C#/dotnet stack, scalability and performance challenges are my favorite. My career in software development has been a combination of startups and Fortune 500’s with startup like cultures. Mid week you’ll find me coding away, but weekends are time for fishing, hiking, boating— anything outdoors!
In my toolbox, JetBrains Rider, macOS, The Azure Cloud platform and of course the best coding language (C#) are my preferred environment. Their intuitive interfaces and powerful features enhance my developer experience.
_While I love software development, I don’t find it particularly fun in a vacuum. The projects I’ve most enjoyed in my career are those where I get interact with a team to solve new challenges— that’s why I love it here at Empower. _
What keeps me excited to come to work everyday is the fact we have a strong team based culture. Almost daily I’ll hop on a call with fellow Engineers or Product Managers to pair up on something. Culture isn’t an accident here, it’s an investment. Twice a year we bring the team together in person to get to know one another, learn what different pods are up to and form relationships.
When 50+ Azure App Service instances restart simultaneously and all reach for the same Key Vault, things break.
In the era of blazing fast compute and memory, it’s easy for the performance characteristics of System objects to feel like a thing of the past.
At ~~Empower~~ Tilt, a data-driven fintech startup, our lifeblood is understanding our users and how they interact with our products.
More in Engineering