From intern frustration to an open-source production DSL: turning days of pain into minutes of freedom
Nov 10, 2025 — 17 min read

There's a moment in every programmer's career when they realize the tool everyone uses is the wrong abstraction. Not buggy. Not slow. Just... wrong. The fundamental model doesn't match how you need to think about the problem. For me, that moment came while building spreadsheets programmatically.
I was an intern at AngelList in summer 2023, working on the backend systems that handle accounting for venture capital funds. The task at hand seemed straightforward: automate support for complex carried interest structures in our quarterly financial statements. Carried interest (or "carry") is how venture fund managers can get paid: typically 20% of a fund’s profits. But it can get complicated. Hurdle rates. Catchup provisions. Preferred returns. Fund managers negotiate these complex terms carefully, and our accountants wanted to eliminate the manual work modifying financial statements to reflect these accurately.
I thought deriving broadly applicable formulas would be the hard part. It took deep thought and a couple white-boarding sessions, but I was able to break through. The trough of sorrow had been crossed…or so I thought. Implementing these financial models using caxlsx, the most common Ruby library for generating Excel files, was the final step.
Unfortunately, that's when I discovered what "wrong abstraction" really means.
Here's what building a very simple profit calculation looks like in caxlsx:
# caxlsx - the old way
Looks simple? Now try to:
B3 returns zero (is it row 3 or the third data row? Don’t get 1-indexed Excel mixed up with 0-indexed Ruby!)I mean, to capture complex carry structures spanning multiple sheets with conditional logic, I was constructing things like:
IF(SUM(SUM(F4:K4)-SUM (E4*2*0.2/0.2, E4*1*0.3/0.3, E4*0.5*0.4/0.4), E4*2*0.2, E4*1*0.3, E4*0.5*0.4)-SUM (F4:K4)*0.5>0,0, MIN(0, MIN(SUM(F4:K4)-SUM(S4*-1/0.1, T4*-1/0.2,U4*-1/0.3), E4*0.5)*0.4*-1))
Unsurprisingly, I walked into a maelstrom of issues. Off-by-one errors everywhere. Formula strings that broke with every structural change. Mental coordinate tracking that made adding features feel archaeological. When I reached out asking "there must be a better way," the consensus was: "Nope, building spreadsheets is just brutal."
I wasn’t satisfied with the answer. But with the next cycle of quarterly reporting looming for our accountants, I shipped my changes, which would work effectively in the medium term, even if not scalable for the long term.

Yet I could feel the number of sheets and references between them were only growing over time (and this prediction came true—just look at that PCB-esque diagram to see how complex our workbooks are now!). A need for a more scalable approach was looming on the horizon. We engineers thought we found a silver bullet in named ranges, a feature in Excel which allows you to provide a name for a range of cells and reference that name rather than their coordinates. But we soon discovered both our in-house accountants and external auditors were not fans, as they didn’t provide the level of transparency that using traditional cell coordinate references do.
Alas, I accepted reality, powered through, and shipped working code. But I had a lingering thought that things could and should be better. Then, our quarterly hack week arrived.
During that hack week, my colleague Zach approached me with a question that changed everything:
What if we could build spreadsheets like a finance professional would? Let's treat the flow of data as a directed graph rather than a 3D array.
With the visual feedback they get in Excel as they build out a workbook, finance professionals don't have to think in cell coordinates. They just think: "Revenue minus Expenses equals Profit. If Profit is positive, calculate Carry as 20% of Profit." For them, these are relationships between named concepts, the coordinate arithmetic is just a means to an end.
Well, what if our code looked like this instead?
member_row = sheet.add_row!(:member_row).add!(:name, value: "John Doe").add!(:net_profit,Zaxcel::Functions.sum(Zaxcel::Lang.range(row.ref(:revenue), row.ref(:mgmt_expense))))profit = member_row.ref(:net_profit)carry = sheet.add!(:carry,Zaxcel::Lang.if(Zaxcel::BinaryExpressions.greater_than(profit, 0)).then(Zaxcel::Functions.max(profit * 0.2, 0)).else(0))
The difference is night and day. Instead of string coordinates, we have Ruby objects. Instead of cell positions, we have named references. Instead of praying our formula strings are correct, we have well-defined abstractions that handle all the gnarly string manipulation under the hood.
Zach came into the week with a rough proof-of-concept with several competing ideas on solving our Excel pains. By the end of hack week, we had a clear vision and a plan to execute collaboratively. By the end of the quarter, we were producing financial statements in production with our brand-new DSL, which we’d aptly named Zaxcel (Zach + Excel! wasn’t my idea but a catchy portmanteau nonetheless). By the next quarter-end, with some elbow grease from a principal engineer, all of our old financial statement generation logic had been ripped out and Zaxcel was being used elsewhere (e.g. exportable expense schedules). Success!
Through building Zaxcel, we developed a core conviction:
Figuring out the business logic is meant to be the hard part. Displaying it in a spreadsheet should be easy.
In the world of financial reporting, Excel is inevitable. Customers demand it. Investors expect it. Auditors require it. The solution isn't to avoid Excel—it's to build better abstractions for generating it.
This meant designing a DSL that mirrors how finance professionals actually think and communicate:
carry_row.add!(carry_on(net_profit.ref(:amount)))Zaxcel::Lang.if(Zaxcel::BinaryExpressions.greater_than(profit, hurdle)).then(...)Zaxcel::Functions.sum(Zaxcel::Lang.range(income_statement.column_ref(time_range.name)))The code should read like the specification. If a finance professional can understand the logic in English, an engineer should be able to trace it in code without mentally translating cell coordinates.
Zaxcel's effectiveness comes from three key engineering decisions:
When you write profit.ref(:amount), you're not calculating a cell position—you're creating an edge in a directed graph. The graph tracks relationships between named entities.
# Building the graphrevenue = sheet.add_row!(:revenue).add!(:amount, value: 100_000)expenses = sheet.add_row!(:expenses).add!(:amount, value: 60_000)# Creating relationships (graph edges)profit = sheet.add_row!(:profit).add!(:amount, value: revenue.ref(:amount) - expenses.ref(:amount))
The magic happens at “compile time”. Zaxcel:
Insert a row? Graph updates automatically. Restyle a column? System catches every reference. Add a new sheet? Cross-references just work. Accidentally add a circular reference? Graph traversal detects it automatically.
This is why refactoring is safe—the graph structure is independent of physical positions.
Zaxcel’s DSL abstractions guarantee the production of well-formatted formulas, regardless of complexity. The flexibility of Ruby is what enabled three critical DSL patterns to facilitate that production idiomatically:
Operator Overloading via Numeric#coerce:
module Zaxcel::Arithmeticdef coerce(other)[CoercedValue.new(value: other), self]enddef +(other)Zaxcel::BinaryExpressions::Addition.new(self, other)end# Similar for -, *, /end
This lets you write revenue.ref(:amount) + expenses.ref(:amount) and have it generate =B2+B3. The math feels natural because it is Ruby's native arithmetic.
Builder Patterns for Complex Logic:
Zaxcel::Lang.if(Zaxcel::BinaryExpressions.not_equal(revenue.ref(:amount), 0)).then((profit.ref(:amount) / revenue.ref(:amount)).round(precision: 4)).else(0)
This generates =IF(B2<>0,ROUND(B4/B2,4),0) but reads like a specification, not implementation.
Module Composition for Capabilities:
module Zaxcel::Roundabledef round(precision: 0)Zaxcel::Functions::Round.new(self, precision: precision)endend
Any arithmetic expression can be .round(precision: 4) because it mixes in Roundable. Adding capabilities feels natural and discoverable.
Type safety is crucial when your Excel DSL is so readable it stops looking like Excel.
Knowing What You're Working With:
# This reads like English, but what types are we actually dealing with?profit.ref(:amount) - expenses.ref(:amount)# Sorbet tells you:sig { returns(Zaxcel::Cell::Reference) } # profit.ref returns a referencesig { params(other: T.any(Numeric, Zaxcel::Arithmetic)).returns(Zaxcel::CellFormula) } # subtraction returns a formula
When code reads like a financial specification, type signatures become your map. Without them, you're guessing whether profit.ref(:amount) returns a cell, a reference, a value, or something else entirely.
IDE Autocomplete as Documentation:
profit.ref(...) # IDE shows: :amount, :description, :category# You don't need to remember column names - autocomplete shows valid optionsZaxcel::Functions. # IDE shows: sum, max, min, round, if_error, xirr...# Sorbet-powered discoverability without documentation diving
The DSL deliberately blurs the line between business logic and implementation. Sorbet keeps you oriented when carry_calculation.round(precision: 4) could just as easily be Ruby math or Excel formula generation.
With those decisions locked in, we live in a world where Zaxcel is more than a library, it's a domain-specific language designed around how finance professionals think.
:revenue, :profit_margin) not implementation details ("B2", "=C3/A3").This is Ruby's superpower: building DSLs that match your domain. Rails did it for web apps. RSpec did it for testing. Zaxcel aims to do it for financial reporting.
The best DSLs don't feel like you're using a library—they feel like the language was designed for your problem. That's the standard we aimed for.
Zaxcel now powers AngelList's entire financial statement generation pipeline, with each of these workbooks dynamically generated based on fund structure, investor classes, and accounting periods. A typical workbook has 15+ interconnected sheets with hundreds of cross-sheet references. Thanks to Zaxcel, we’re able to produce thousands of financial statements, saving our accountants tens of thousands of hours of manual work every quarter.
Built-in DSL abstractions and type safety allow us to execute refactors with a very high degree of confidence. Feature development that used to take days (or even weeks), now takes a single engineer, a matter of a couple hours, if not minutes.
Here's a simplified version of how we generate Capital Account Summaries with carry allocations:
# Pull profit from Income Statementprofit = income_statement_sheet.row_ref(:net_income).cell_ref(:current_period)# Calculate if hurdle is methurdle_met = Zaxcel::Lang.if(Zaxcel::BinaryExpressions.greater_than(profit, hurdle)).then(true).else(false)# Carry calculation with catchupcarry_cell = Zaxcel::Lang.if(hurdle_met).then(Zaxcel::Functions.min((profit_cell - hurdle_amount) * carry_rate,max_carry_amount)).else(0)# Add to capital account sheetcarry_row = sheet.add_row!(:carried_interest).add!(:description, value: 'Carried Interest (20%)').add!(:amount, value: carry_cell)
This generates formulas that:
Without Zaxcel, this would be 50+ lines of string manipulation prone to subtle bugs.
The impact is best measured in before-and-after timelines:
Pre-Zaxcel Era (2023)
Post-Zaxcel Launch (Late 2023-2024)
Present Day (2025)
That last point matters: good abstractions help everyone, both humans and machines. When your code reads like its specification, it's easier to understand, maintain, and extend.
Technical excellence doesn't matter if nobody uses it. Zaxcel's success came from tight collaboration between engineering and our fund administration team.
If this had existed out in the world, we would have certainly saved ourselves a lot of work with our Excel workbook generation functionality. The AngelList engineering team would love to save others from the pain we had building spreadsheets pre-Zaxcel.
So on that note, we’re excited to announce: Zaxcel is now open-source!
Check out our live launch from the SF Ruby meetup on Oct 30th 2025MIT-licensed. Production-tested at scale. Fully typed with Sorbet.
Our engineering team has built a lot of cool stuff over the past couple years, and over the coming months we’d love to share some more stories about how we’ve built:
The pattern is consistent: take complex financial problems, build high-leverage technical solutions, and build on what we learn.
If you're interested in solving problems like these, we're hiring. Reach out directly to me or other engineers on our team—we're always looking for great colleagues who appreciate good abstractions and hard problems.
Zaxcel wouldn't exist without the collaboration and conviction of several key people:
Zach Schnell conceived the core insight during hack week and architected the graph-based reference system. His willingness to bet engineering time on this problem fundamentally reshaped AngelList's financial infrastructure.
Alex Stathis delivered a big boost to driving full adoption of Zaxcel across our financial reporting surfaces after my internship ended. His efforts significantly improved the library ergonomics and ensured cross-functional buy-in.
The AngelList engineering team—particularly those maintaining our 15-year-old Rails monolith—provided the production environment and feedback loops that refined Zaxcel from prototype to foundation.
Our accounting and operations teams, especially those who patiently worked with early versions and helped us understand what "audit-ready" really means in practice.
And to the SF Ruby community for providing a venue to share this work—looking forward to the discussion.
