Intro
Here at Squarespace, we build and maintain Commerce, an all–in–one DIY platform that lets merchants run an online store within their website. Our merchants sell a huge variety of products in all places around the world. As diverse and widespread as they may be though, all of our merchants share one responsibility in common: they have to collect taxes on their sales, and they have to pass those taxes on to their local governments.
As a DIY platform, the approach Squarespace takes with taxes is to let merchants enter in the rates they need to collect, then provide a calculator that can apply those rates to a shopping cart. Squarespace’s role is thus not to know the exact details of a given tax system, but rather, to provide a tool that can effectively model the most common ones. Tax laws can get very complex, so creating a tool like this is not trivial.
Requirements
In its first iteration, Squarespace’s tax calculator only supported tax-exclusive prices, and the amount of accounting detail it outputted was not sufficient for rendering the sophisticated invoices that regions like the European Union require. In order to better support a wide range of merchants, we decided to rebuild our calculator from the ground up with the following goals in mind:
- Support both tax-exclusive and tax-inclusive prices
The United States and Canada typically add sales tax to product prices (i.e., a $10.00 product charged at 10% results in a $11.00 receipt). The rest of the world, however, tends to include tax in their product prices; in the European Union, a €10.00 product taxed at 10% still totals to €10.00, but it’s understood that €0.91 of that price was set aside for tax. Our calculator needs to handle both ways of calculating tax. - Support exemptions for certain types of products
In many countries and US states, certain types of services are exempt from taxation, so Squarespace allows merchants to specify whether a service should be taxed or not. Our calculator needs to support this. - Output a very high level of detail about the calculation
Other systems of ours, e.g. our invoice renderer, need to know very granular details like which exact products were taxed, how much a given discount affected the tax total, which jurisdictions taxed which products, etc.
Technical Goals
Like any good rebuild, we wanted to take this opportunity to lay some strong technical foundations behind all the new features. Our main engineering goals for this rebuild were:
- Decouple the calculator from any one source of product prices or tax rates. The engine shouldn’t care if the rates come from our database, from an external rates service, or wherever else.
- Make it testable! And follow through with a strong suite of unit tests.
- Be very deliberate about how we treat numerical precision and rounding.
- Use Java best practices:
- Make objects 100% immutable wherever possible
- Keep classes very small and focused
- Avoid methods with more than one argument
- Use functional programming patterns where appropriate: list transformations, filtering operations, etc. (yay Java 8!)
Implementation
In order to achieve all of these goals, we ended up creating a new package, commerce.taxes.calculator
, centered solely around tax calculation: no database I/O, no API interactions, and very little knowledge of types and concepts from elsewhere in our system. The classes in this package fit into four general areas: orchestrators, business objects, factories, and output database models.
Orchestrators
- CartTaxCalculator [immutable]
Accepts database models, supplied by calling code, for aShoppingCart
representing a basket of goods to be purchased, a list ofTaxRules
specifying rates a merchant intends to charge, and aStoreSettings
containing details about currency and preference for tax inclusive/exclusive pricing. Translates these into business objects for use byOrderTaxesCalculation
. - OrderTaxesCalculation
Owns all the business objects necessary to execute a tax calculation. Filters out anyJurisdictionSalesTaxRates
not applicable to the shopper’s location, then delegates toProductLineItemFactory
andShippingLineItemFactory
to apply the remaining rates to cart entries. UnlikeCartTaxCalculator
,OrderTaxesCalculation
is completely unaware of any database model.
Business Objects
- JurisdictionSalesTaxRates [immutable]
The set of tax rules governing a geopolitical region. Includes aCalculationJurisdiction
specifying the region itself, a base tax rate, and all of the exemptions for the jurisdiction. - CalculationJurisdiction [immutable]
A geopolitical region, representing an area where a set of tax rules may apply. ACalculationJurisdiction
can be asked if contains another one viaCalculationJurisdiction::legallyContains(other)
. This is how we filter rates by regional applicability. - TaxableCart [immutable]
A minimal representation of a shopping cart, containing only information that directly affects financial calculations. This information includes product prices, any discounts applied, shipping charges, and tax categories for each product. - CalculationLineItem [immutable]
A tax line item with detailed accounting information. Specifies which product was taxed, which jurisdiction levied the tax, and whether this line was derived from a subtotal, discount, or shipping charge. Tax percentages and monetary amounts are modeled as 128 bit BigDecimals.
Factories
- ProductLineItemFactory [immutable]
A factory that, when instantiated with a shopping cart entry and aPriceTaxInterpretation
, outputs one or moreCalculationLineItems
for eachJurisdictionSalesTaxRates
applicable. Depending on thePriceTaxInterpretation
, the amounts on the line items will be calculated either tax-inclusive or tax-exclusive. - ShippingLineItemFactory [immutable]
A factory that, when instantiated with a shipment charge and aPriceTaxInterpretation
, outputs one or moreCalculationLineItems
for eachJurisdictionSalesTaxRates
applicable to the charge. From a tax perspective, shipping charges are basically another “product” in the shopping cart.
Database Models
- OrderTaxesLineItem
The end result of a calculation is a list ofOrderTaxesLineItems
. Each one includes all the information necessary to trace a tax charge back to the product, order component, and jurisdiction that produced it. Monetary amounts are stored as fixed-point string representations.
In the diagram below you can see the general data flow that occurs; start with database models, translate into domain-specific business objects, use factories to produce business-level line items, then translate these line items into database entities. A reasonable question to ask may be “why all the translating steps”? Doing this is very helpful for a couple of reasons:
- It accomplishes our goal to keep the calculator isolated from any one source of tax rates or product prices. As long we can somehow provide
JurisdictionSalesTaxRates
and aTaxableCart
, it doesn’t matter where the actual data comes from. - The code is much more testable because it doesn’t have any extraneous data dependencies. A test fixture only needs to create the minimum amount of data necessary for a calculation: no product descriptions, websites, member accounts, etc.
- It allows us to have much clearer code paths. Once we’ve translated everything into business objects, we’re free to model our fields and logic in the best manner for calculation, not storage or output. We can use domain specific terms like “legallyContains”, add behaviors for manipulating line items, and combine pieces of input data in more convenient ways. This is OO at its best, and for a domain as complex as taxes, it makes all the difference between clear, self-documenting code and a pile of spaghetti.
- We can translate primitive values into more useful representations, like making
BigDecimals
out all our floating point tax rates. Every numerical value within the calculator is aBigDecimal
until the very end, when we convert to a fixed-point string representation forOrderTaxesLineItem
. This allows us to maintain high numerical precision and predictable, well-defined rounding behavior.
As far as Java best practices, we did pretty well on this front:
- By thinking very carefully about what state belonged in which objects, we were able to achieve immutability across the board.
- The longest class was
CalculationLineItem
, coming in at 207 lines of code. Half of this is just boilerplate for aBuilder
and properequals
andhashCode
implementations. - The majority of our methods were either 1 or 0 arguments. The only ones with more than that were convenience methods on static factories.
- With the strong focus on immutability, short argument lists, and a clear data pipeline, most of the logic could be expressed in a functionally–driven, fluent manner. This was great for conciseness and legibility.
So… was any fancy new technology used here? Aside from a bunch of Java 8, not really. What made building this a great challenge, though, was taking a messy problem area and using thoughtful naming, code organization, and data flows to make it into something workable. Our new tax calculator handles more cases than the original, and the code behind it is much easier to reason about. In my book, that’s a pretty big win!
Maybe you’re not into taxes (I don’t blame you), but if you like wrangling tough, real-world problems into elegant solutions, we’re hiring!