💭 Principles of Pactio Engineering

[Author: Will]

Whilst we don't follow any of these rules as gospel, I thought it would be fitting to have these as our first blog post, unfiltered and unchanged, as they were originally devised before I joined Pactio. They stress a very important thing: what really makes a startup, and any group of individual, is their culture. Being very explicit about your culture is the only way to create one that you will like 5 years down the line.

Whilst it's tempting to allow such long term planning in a busy startup to slip, or I might have personally not chosen some of the principles, I was super impressed by the intentionality and vision that the company had when I joined. Everyone was fully expecting to be here long term, and that felt exhilarating.


These are the basic principles that we use to drive Pactio engineering culture. These are: generally technical rather than social; opinionated; and subject to discussion and revision. They're also rules, so are made to be broken where appropriately and artfully done.

1. Create a technology monoculture

Where at all possible, use TypeScript. Where at all possible, put sharable code in reusable packages. Where at all possible, rely on existing tooling, infrastructure, and design patterns.

Technology monocultures can feel a little intellectually unsatisfying, since you're often dissuaded from using the ideal tool for the job. But the benefits of having a reliable, predictable, and shared set of technical norms and frameworks are huge. It helps minimise the number of things that we have to keep in our heads, and a fortiori the problem space over which we need to search. Change to the monoculture should be embraced, but slowly, cautiously, and with an eye on the hard-won lessons.

2. Good writing is good thinking

Pactio is a writing-driven culture. When designing abstractions, APIs, or complex parts of systems, think through the decisions in prose. Consider consequences, articulate trade-offs. Share, read, and write widely.

Documentation is overrated in the minutiae and underrated in the grand; it should be used much more for larger pieces of work and much less to describe specific pieces of code. (The same applies to code comments.) Design documents are useful as artifacts, but they're much more useful as organisms: they should grow and change and be revisited.

3. Write small, composable functions

Functions are cheap. Write lots of them. Test them independently. Use dependency injection to keep them independent.

"It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures." – Alan J. Perlis

4. Functional core, imperative shell

Put business logic in pure functions. Rely on immutable data structures (or rely on ES6's destructuring syntax to emulate them.) Push side-effects to the edge of the program.

If the core business logic is comprised of small, independently testable pure functions, we can massively reduce the likelihood of an entire class of bugs and end up with very readable, clear, and reusable code. Only use mutable values when there are clear performance benefits.

5. Data is data

Data is data. Data is ontologically simple. JS arrays and scalars are a pure and good representation of simple data. JS objects – a.k.a. maps, dictionaries, associative arrays – are a pure and good representation of structured data. Avoid classes wherever possible; don't intermingle state and behaviour.

Inheritance isn't the only way to do polymorphism. TypeScript gives us interfaces the shape of which get validated at compile-time, which is often enough. Run-time validation of structured data can often be avoided by explicit object construction and thoughtful validation at the external boundary of the system. See also 3., especially the quote from Perlis.

6. A test is worth 1,000 words

Write lots of unit tests. Write some integration tests. Consider edge cases: use tests to better align the desired input space and the actual input space. Where it feels useful, use tests to help drive design.

A small set of end-to-end / integration tests are very beneficial, and important to ensure that the imperative shell behaves as expected and is glued together properly. Lots of very fast unit tests can provide wide and deep coverage over the functional core. If your code is hard to test with basic jest matchers, it's probably too interwoven with other pieces of code (see 3. and 4.)

7. Write visible code: profile and log what you can

Log extensively. Use log levels to select out of verbosity where needed. Err on the side of more logging. Instrument services in production, and find ways to trace the flow of requests between services. Use notifications and triggers to inform us – and the user – when things go wrong.

The @pactio/logger package, and other shared tools that we will build, are designed to make the system as transparent as possible. As per 9., if you can help build a tool to make the system more transparent, you should!

8. Defense is the best offence

Assume things break. Assume that your networked services can be inaccessible. Assume that inputs will be typed incorrectly – especially numbers. Minimise the probability of cascading problems. Return sensible defaults. Make code like Chumbawumba; it gets knocked down, but it gets up again.

If it can possibly break, it will. If it has a public interface, its input can't be trusted. Use packages to minimise the surface area of what needs to be checked.

9. Care about developer experience as much as user experience

Technical debt is to a codebase as rust is to a boat's hull. Invest in good tooling. Share useful snippets with the team. Leave lots of comments explaining why, and lots of tests describing what. Automate where you can.

Use tools like eslint and prettier to validate and fix syntax errors. Have shared defaults that get applied automatically. Use yarn scripts to speed up common development tasks. Build tooling and dashboards, either from scratch or using GUIs like Retool.

10. Show, don't tell

Features are there to be used. Minimise distance to use in production. Measure what you can. Move quickly, deploy, test, iterate.

Feature flag everything, and deploy. Design our infrastructure and processes around speed of deployment. Try not to break things, but opt for moving fast.

Finally

Leave the world in a better state than you found it.

Fix bugs when you come across them. Leave comments for future Pactitioners. Refactor regularly. Extract out code into reusable packages.

Be a good steward. Be kind.