typeset by Claude 4 Sonnet from this rough draft
The Philosophy
Code is a liability. The goal is to delete as much of it as we can. It’s hard enough to build as it is—don’t introduce artificial constraints that just make your life difficult and ours.
Don’t be lazy. Your laziness costs all of us time and development velocity. You are defecting against the commons by taking unwarranted shortcuts. Be mindful when taking shortcuts—if you are optimizing for speed or optimizing for laziness, if the latter then we are just shortchanging ourselves.
Always understand what you are doing. DO NOT LAND CODE YOU DO NOT UNDERSTAND. Think about the costs of what you are doing and how they will grow as you scale. If you don’t know what you are doing, figure it out first before doing it.
Think about the interfaces and abstractions before writing the implementation. What are we actually trying to achieve functionally? What kind of interaction do other engineers (your customers) want? Think about ergonomics and developer experience when doing feature development.
Embrace simplicity, abhor complexity. Don’t make things more complicated than they need to be. Don’t write code you wouldn’t be proud to put your name behind. Don’t shoot yourself in the foot.
Version Control
Linear Commit History
Do not git merge. Don’t bring feature branches up to date with main by merging main into the feature branch. Rebase the feature branch on main instead.
Similarly, PRs should be landed with Squash and Merge or Rebase and Merge, not with a merge commit.
Why? A linear commit history is easier to reason about and understand, as well as less complicated to introspect, reset, and revert. Merge commits introduce multiple parent branches which interfere with reverts. Squash and merge has a similar motivation—having a single commit to undo while not polluting the git history with checkpoint and cleanup commits, while still giving full flexibility at development time.
Meaningful Commit Messages
I try to use a simplified variant of conventional commits:
api/calls: update auth scheme to xyz
ci: make action do something else
Commits named “checkpoint” or “cleanup” have no semantic value for someone reading the git log. These names are for your benefit as much as they are for others.
Development Process
Avoid Massive PRs
Feature development and PRs should be incremental. Develop in parts that build on top of one another and merge piece by piece to enable review. Each PR should do exactly one thing.
Design First Implement Later
Write out what you’re trying to achieve and how you’re planning to achieve it before writing the code, if only for yourself (although you should put this in a doc and/or the commit description). It’s good for the models too.
Think about the interfaces and abstractions before writing the implementation. What are we actually trying to achieve functionally? What kind of interaction do other engineers (your customers) want? Measure twice, cut once.
Follow Standards and Specifications
In the backend, if we are implementing a REST API, we need to actually follow REST semantics. If you don’t know what that means, learn. Ask ChatGPT. Think about the categorical imperative - i.e. how your approach generalizes. HTTP, REST, etc. are all designed to make our lives easier. If we don’t follow them, it’s lose/lose for everyone involved.
Code Quality
Don’t Make Things More Complicated Than They Need to Be
Don’t use generics or complex indirection. Someone down the line will have to debug this code, and most likely it will be you.
Think about ergonomics and developer experience when doing feature development. Is what you’re doing going to make it easier for the next person working on this, or is it going to make their life more difficult?
Write Testable Code
Try to write meaningful tests for your code, and write it in the first place with testing in mind. Don’t push broken tests to master. If the test suite is always red, we’re going to learn to ignore it because it doesn’t give us differentiable signal.
Code that branches widely is extremely hard to test - keep it narrow to make it easier to verify. It takes forever to test a lot of branching logic, and makes any tests we write extremely complicated and fragile.
Be Judicious with Error Handling
Don’t overwrite errors from upstream! It impedes debugging if we don’t know what the actual error is.
Be intentional and deliberate with your try/catch blocks. Don’t catch an exception you don’t know how to handle. The traceback will contain the log data, don’t catch just to log and re-raise unless you know what you’re doing.
Performance
Design for flexibility, engineer for performance.
Read Jeff Dean’s classic note Latency Numbers All Programmers Should Know. If you don’t understand what this stuff means, ask us or the models.
Know Your Network Calls
Always know when we are making a network call in any operation we do. Each database call is a network call, and each network call can fail, timeout, introduce nondeterminism. In localdev the DB is running on your laptop and access latency is measured in microseconds. In prod the DB is running on AWS Aurora (disaggregated storage) and has access latency measured in milliseconds - 2-5ms per query, not including the query processing latency itself, which can be substantial if we’re not writing good queries or are processing a lot of data.
Database Performance Matters
Think about our indexes and data access patterns! Database writes take write locks on the table or index which block subsequent reads. Similarly, reads can block subsequent writes (think about serializability and MVCC from database class). If we are making multiple writes in the same logical operation but they are not wrapped in a/the same transaction, we can end up with inconsistent DB state if a write fails!
If we can do work in SQL, we should do it in SQL, not bring the data back to process it in Python. Python is slow for CPU-bound work, MySQL is written in C++ has been optimized by wizards who have forgotten more about performance optimization than you will likely ever have to learn. ChatGPT, Claude, and Gemini are great at writing optimized SQL! Describe the overall problem to them along with the schema and data model and let them help you write a good query.
Minimize Database Writes
The database is for long-lived information that we will need to keep around to process later. If data is temporary or changes rapidly with time, it should live in a cache. We should not be updating individual database rows very frequently! If we’re only going to use the data once (i.e., it’s an artifact), put it in S3, not an OLTP store.
Think About Scale
Think about how your code will run in production, not just on your laptop! Your laptop is extremely fast - the pinnacle of Apple Silicon’s design efforts. Prod is running on the cheapest x86 CPU AWS could negotiate from Intel/AMD.
The client is running on a cheap Windows device at a doctor’s office and is being operated by someone who doesn’t know anything about computers. Think about the real-world usage environment when building!
Transfer as little data over the network as possible. We pay for every byte of egress! Every byte we have to send or receive from the client, from the database, from the cache, from the PMS, from Inngest is time and money we are paying for and which is slowing everything down. Minimize what goes outside our network perimeter - heavy or compliance-sensitive data needs to stay inside our perimeter as much as possible.
Data Design: Schema Strategy
When Designing Data Models and Schemas
Don’t overnormalize things for the sake of normalization! If you’re storing config data indexed by a primary key, just store it as a JSON column. Don’t spread data that shares an index key across multiple tables - that just forces you to take an unnecessary and expensive join. Think about designing your schemas from the perspective of how you’re going to query the data.
Take Advantage of Validation
Pydantic on the Python side! Use the Pydantic/SQLAlchemy type adapter to map Pydantic types to JSON columns transparently. We shouldn’t use bare JSON anywhere that isn’t strictly necessary. Pydantic is your friend! Similarly, if we are serializing and deserializing JSON a lot, then our data model is wrong!
Code Organization and Conventions
Consistent Conventions
Name things consistently. Functions, classes, variables. Put related functionality in the same places. The rest of us should be able to expect and predict where things are going to be.
File Organization
Don’t splay logic across more files than is warranted, but don’t pack too much stuff into a single file or especially a single function either.
Functions shouldn’t have more than three levels of indentation/nesting - it’s a sign your implementation is too complex.
There Should Only Be One Way to Do Things
Don’t splay config and options across a lot of interfaces or methods. Pick one good way to express some kind of configuration and use it consistently. If it’s not ergonomic in some case, then reconsider the original design.
Team Collaboration
Communication Is Key
Post updates and information liberally to Slack. Post your PRs and explain them so the rest of us understand what you are doing and how it works. It is better to overshare than undershare. We cannot afford to be working against one another by accident! Each of our projects has to enable others - it can’t dig ourselves into a deeper hole.
Write Documentation and Comments
We need to understand what’s going on and how to work/interact with the things each other are building. If you are adding or changing behavior, document it and tell the rest of us where that documentation is!
Use Shared Code
Read our shared/library/util code! Don’t reinvent the wheel if someone has already invented a wheel you can use. If you are building a wheel, contribute it back to the commons.
Working with AI
Always Review AI-Written Code
The models are smarter than you. That means they can handle more complexity than you, and their code reflects that. Don’t blindly accept code you don’t understand just because it looks like it works! You are not working as remote hands for the models. As of yet the models are working for you, and you are responsible for what they produce on your behalf.
The models DO NOT KNOW WHAT GOOD CODE LOOKS LIKE and will lie to you about it out of total ignorance. If you can’t tell if some code is good or bad, read more code from other widely-used projects that are accepted as having good practices! Developing an aesthetic sense and good judgement for evaluating code and one’s work product in general is incredibly important.
Do not land code over review feedback. If Greptile has caught bugs in your code, fix them before landing. If Greptile has to leave dozens or hundreds of comments, your PRs are too big and you need to split them apart into smaller components.
Development Environment and Practices
Minimize Localdev/Prod Differences
Don’t add special casing for localdev or similar magic. Minimize the number of differences between localdev and prod, every difference makes it harder for us to understand what’s going on when debugging and increases production risk.
Think Carefully About Logging
If we dump too much data into the logs, it just pollutes our own debugging process and costs us more money on Datadog! Pick your logging levels carefully. Info, warning, error, and debug have well-defined meanings! Our logging level in prod should be set to INFO, not debug. If we need certain information for debugging in prod, think carefully about where that is exposed and how it will show up in DD!
Also, in our localdev environment, if we have a ton of logs polluting our own console outputs, it’s that much harder to debug during development.
The Refactoring Mindset
Refactor as You Go Along
Don’t get lost while doing this, but if you can fix or improve something in the process of implementing a feature, do it! And separate the logic into independent commits. Leave things in a better state than you found them in.
Handle Edge Cases Thoughtfully
The first time you need to support an edge case, special case it. The second time, refactor. Don’t let special cases and custom functionality for edge cases pile up. It’s a sign your abstractions are bad.
Process and Decision Making
Don’t Scar on the First Cut
Don’t overindex on something going wrong once. Adding process and formalities has a cost, don’t force yourself to incur it until the same thing has gone wrong multiple times and you know why your process change will stop it.
It’s Okay to Say No to the Customer
Customers will ask the entire universe of you. If we’re not frequently saying no, then we’re both spreading ourselves too thin and also we aren’t selling the customer a grand enough story. Do not introduce warts into the codebase to satisfy a customer and ink a deal if the consequences of that will cause us to lose more business down the line!
The Bottom Line
Design for flexibility, engineer for performance. Premature optimization is the root of all evil - but don’t miss the critical 3%!
Many don’t know the full Knuth quote:
Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.
Don’t be lazy. Be mindful when taking shortcuts of whether you are optimizing for speed or optimizing for laziness. If the latter then we are just shortchanging ourselves. Make it easier to make things better in the future, don’t make it harder. Don’t defect against your future self.