How We Write Software
We PR every line of code that makes it into a shared branch of a Betterment codebase because it ensures we’re on the same page about what our code does, what it should do, and how it should do it, and code review serves as a crucial knowledge-sharing mechanism.
We do design reviews whenever introducing a new pattern or a novel application of a pattern, or interacting with a new external app or an existing app in a new way because design reviews ensure alignment of all stakeholders, source expertise, grow and mature our shared principles, and leave behind a durable artifact of what was built, and why.
We document new patterns or changes to our patterns with lightweight Architecture Decision Records so that we can refer back to the reasoning behind our decisions.
We write our software with inclusivity in mind because we want a work environment that is psychologically safe and welcoming to all people at Betterment. Our approach for word choice and language within our codebase and tools is informed by but not limited to Google’s principles on inclusive documentation.
We practice blameless post mortems and take action on follow-ups when we have bad outcomes because not only are root causes a myth but making an example of somebody is never the right mitigation for system failure. An honest, unconflicted accounting of an adverse event enables us to convert each incident into durable institutional learnings and makes our software better.
We don’t write code comments except for public API documentation in reusable libraries or exceptionally gnarly code that hasn’t yielded to concerted attempts at refactoring for clarity because the code’s intention must be clear to the reader from its source code and tests. Code comments tend to rot, drift away from the lines they comment on, and can tempt us to leave code that’s less intrinsically revealing than it should be.
We aggressively flatten out abstraction layers that don’t add value to our use cases because meaningful software has too much irreducible complexity to expend energy climbing a needlessly tall abstraction ladder.
We practice malleable design rather than future-proofing because every joint we create that doesn’t prove to be exactly what we needed or wasn’t used leaves scar tissue.
We build skinny controllers and rich domain models because model objects are easier to test than controllers (of any kind: CLIs, REST endpoints, scheduled tasks, etc.) and tend toward malleable software.
We use the open source model for reusable library ownership - each new library gets a repository and a self-organized core team of committers - because it frees us to create the tooling we need in a low-friction way, avoids tragedy of the commons, and sets us up to easily open source the libs when they mature.
We write software that fails safe intrinsically against unacceptable outcomes because the effectiveness of urging vigilance when operating unsafe software does not scale with line count or team size, and it promotes priesthoods and heroism instead of empowering us to move fast, safely.
We take on tech debt only strategically by tying debt reduction into the product roadmap and explicitly planning for and resourcing finishing the jump when needed because “we’ll fix it eventually” is wishful thinking.
Barring a strong reason to do otherwise, we use fixed point decimals, not floats, for arithmetic because rounding errors and implicit loss of precision cause surprises and can lead to unacceptable downstream effects when we’re writing financial software.
We use strings to represent decimals in JSON because JSON doesn’t provide a fixed-precision decimal type.
We use _ratio
(1.0 == 100%), _percent
(100 == 100%), and _bps
suffixes (10,000 == 100%) to disambiguate differently-scaled ratio
variables and columns because they help us avoid ambiguity and simple
arithmetic errors.
For data products that do not have a real time or transactional requirement, we leverage ETL and provide online data API services (e.g. dataservice) because it’s more efficient to push work into processes and technologies that are purpose-built for crunching data in aggregate, and APIs provide malleable contracts with consuming apps.
We organize our data warehouse into facts and dimensions that are crafted to serve the use cases of its primary consumers - product, marketing, finance, and analytics - because being customer oriented doesn’t stop at the data layer.