2025-03-22 10:34:35 -04:00
|
|
|
|
# Axioms
|
|
|
|
|
|
|
|
|
|
An `axiom` is a self-evident truth that serves as a foundational principle. We use axioms to guide us
|
|
|
|
|
in our design and implementation of the DSFX codebase. They are not just rules; they are the
|
|
|
|
|
philosophical underpinnings of our approach to software development. By following the axioms, we can
|
|
|
|
|
create a codebase that is not only functional but also elegant and a joy to maintain.
|
|
|
|
|
|
|
|
|
|
## Our Motivation
|
|
|
|
|
|
|
|
|
|
> "We are what we repeatedly do. Excellence, then, is not an act, but a habit." – Aristotle
|
|
|
|
|
|
|
|
|
|
We believe that excellence in software development occurs when the human element is in harmony with
|
|
|
|
|
the technical. There is a reason why the swing of analog recordings is often preferred over the
|
|
|
|
|
perfection of digital recordings. It’s the human touch, the imperfections, that make it feel alive.
|
|
|
|
|
This is the essence of our motivation: to create software that resonates with the human experience,
|
|
|
|
|
that is both functional and beautiful. We prioritize correctness, simplicity, and elegance in our
|
|
|
|
|
code, and we strive to make our codebase a place where developers can thrive.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Why Have Style?
|
|
|
|
|
|
|
|
|
|
Steve Jobs once said, "Design is not just what it looks like and feels like. Design is how it works."
|
|
|
|
|
In the world of software, this means that style is not just about making our code look pretty; it’s
|
|
|
|
|
about making it work well. It's not about producing a product that works, but about creating a
|
|
|
|
|
product that is a joy to work with. The axioms that we follow ripple through the project, shaping
|
|
|
|
|
everything from individual lines of code, to the overall philosophy of the project.
|
|
|
|
|
|
|
|
|
|
The priorities of this style are simple:
|
|
|
|
|
|
|
|
|
|
- **Safety** first,
|
|
|
|
|
- **Performance** next,
|
|
|
|
|
- and then **developer experience**.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Safety First
|
|
|
|
|
|
|
|
|
|
Even in Go, safety is non‐negotiable. We frequently embed explicit checks and assertions to ensure
|
|
|
|
|
our code behaves as expected. Use the `internal/lib/assert` package for assertions. While using
|
|
|
|
|
assertions is not idiomatic in golang, we believe that the safety they provide is worth the trade-off.
|
|
|
|
|
|
|
|
|
|
It is important to clarify _what_ should be asserted. Assertions should never be used as a crutch for
|
|
|
|
|
proper error handling. Rather, they should assert obvious bugs and developer errors, such as:
|
|
|
|
|
|
|
|
|
|
- **Preconditions**: Check that inputs are valid before processing.
|
|
|
|
|
- **Postconditions**: Ensure that outputs are as expected after processing.
|
|
|
|
|
- **Invariants**: Validate that the state of an object or system remains consistent throughout its lifecycle.
|
|
|
|
|
- **Critical states**: Assert that certain conditions are met at key points in the code.
|
|
|
|
|
|
|
|
|
|
Assertions cover our bases as developers. If we forget to initialize something, should we really be
|
|
|
|
|
reporting this error to the user? No! We should panic and fix the code. Assertions are our safety net.
|
|
|
|
|
In this case, the code should not be passing tests and we should not be releasing this code. It's
|
|
|
|
|
true that we could write an explicit unit test to check for this, but that would be redundant and
|
|
|
|
|
unnecessary. Again, these are **developer errors**, not user errors. We shouldn't be writing explicit
|
|
|
|
|
tests for developer errors. Instead, we should be asserting the positive and negative space of our
|
|
|
|
|
system to immediately crash the program if something goes wrong. This will fail the user level tests
|
|
|
|
|
by proxy and alert us to the issue before it reaches production, without needing to manage a complex
|
|
|
|
|
test harness just for these types of errors.
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
// Assert panics with a message if the condition is not met.
|
|
|
|
|
// Not idiomatic Go, but used here to enforce critical invariants.
|
|
|
|
|
func Assert(condition bool, msg string) {
|
|
|
|
|
if !condition {
|
|
|
|
|
panic(fmt.Sprintf("Assertion failed: %s", msg))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Key safety guidelines**:
|
|
|
|
|
|
|
|
|
|
- Use **simple, explicit control flow**. No recursion or function trampolines. Do not use `goto`.
|
|
|
|
|
- **Limit loops and queues**: every loop should have an explicit upper bound to avoid infinite cycles.
|
|
|
|
|
- Favor fixed-size types (e.g., use `uint32` or `int32` where appropriate) instead of
|
|
|
|
|
architecture-dependent sizes like `int` or `uint`. This helps avoid overflow and underflow issues.
|
|
|
|
|
- Use assertions liberally—both to check expected states _before_ and _after_ important operations.
|
|
|
|
|
Aim for at least two assertions per function.
|
|
|
|
|
- Keep function bodies short. In Go, we suggest striving for functions to be easily digestible
|
|
|
|
|
(roughly keeping functions around 70 lines or less) by breaking out helper functions where it makes
|
|
|
|
|
sense.
|
|
|
|
|
- Line lengths should be kept to a maximum of 100 columns to ensure readability and maintainability.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Performance
|
|
|
|
|
|
|
|
|
|
Performance starts in the design phase—even before profiling code.
|
|
|
|
|
|
|
|
|
|
- **Back-of-the-envelope sketches:** Before implementation, roughly gauge resource (network, disk,
|
|
|
|
|
memory, CPU) usage.
|
|
|
|
|
- Identify the slowest resource (network → disk → memory → CPU) and optimize that first.
|
|
|
|
|
- Use batching to reduce overhead and context switching. For example, aggregate database writes
|
|
|
|
|
rather than doing them one at a time.
|
|
|
|
|
- Separate the control plane from the data plane. For “hot loops” or performance‐critical code,
|
|
|
|
|
create small, stateless helper functions accepting primitive types. This helps both the compiler
|
|
|
|
|
and human reviewers spot redundant computations.
|
|
|
|
|
|
|
|
|
|
In a typical Go server, many things happen at once. The server is constantly reading from the network,
|
|
|
|
|
writing to the disk, and processing data in memory. This can lead to contention for resources, which
|
|
|
|
|
can slow down the server. To mitigate this, we recommend:
|
|
|
|
|
|
|
|
|
|
- **Don't use goroutines directly**: Instead, use a worker pool that you can dispatch work to. This
|
|
|
|
|
allows for better control over cpu usage, giving the program the ability to scale up or down
|
|
|
|
|
depending on the load. This can be done in a controlled manner, without flooding the scheduler with
|
|
|
|
|
goroutines. **Never use user input to determine the acceptable tolerance for load.**
|
|
|
|
|
|
|
|
|
|
- **Use channels for communication**: Channels are a powerful feature of Go that allow for safe
|
|
|
|
|
communication between goroutines. They can be used to pass messages between the worker pool and
|
|
|
|
|
the main program, allowing for better coordination and control over the flow of data.
|
|
|
|
|
|
|
|
|
|
- **Use context for cancellation**: The `context` package in Go provides a way to manage
|
|
|
|
|
cancellation and timeouts for goroutines. This is especially useful for long-running operations
|
|
|
|
|
that may need to be cancelled if they take too long or if the program is shutting down.
|
|
|
|
|
|
|
|
|
|
- **Use the `sync` package for synchronization**: The `sync` package provides a way to manage
|
|
|
|
|
synchronization between goroutines. This is important for ensuring that shared resources are
|
|
|
|
|
accessed safely and that data is not corrupted by concurrent access.
|
|
|
|
|
|
|
|
|
|
- **Use the `sync/atomic` package for atomic operations**: The `sync/atomic` package provides a way to
|
|
|
|
|
perform atomic operations on shared variables. This is important for ensuring that data is not
|
|
|
|
|
corrupted by concurrent access and that performance is not degraded by unnecessary locking.
|
|
|
|
|
|
|
|
|
|
- **Use the `sync.Pool` for object reuse**: The `sync.Pool` provides a way to reuse objects to
|
|
|
|
|
reduce memory allocation and garbage collection overhead. This is especially useful for
|
|
|
|
|
performance-critical code that creates and destroys many objects.
|
|
|
|
|
|
|
|
|
|
- **Use the `testing` package for benchmarking**: The `testing` package provides a way to
|
|
|
|
|
benchmark code to measure its performance. This is important for identifying performance
|
|
|
|
|
bottlenecks and for ensuring that changes to the code do not degrade performance.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Handling Errors
|
|
|
|
|
|
|
|
|
|
Errors must always be handled explicitly - they are never ignored. In a function that can produce
|
2025-04-01 10:57:37 -04:00
|
|
|
|
more than one error, wrap the errors using `fmt.Errorf("%w")` to provide additional debugging context
|
2025-03-22 10:34:35 -04:00
|
|
|
|
in a way that preserves the original error for use with `errors.Is` and `errors.As`. This allows
|
|
|
|
|
for better error handling and debugging, especially in complex systems where multiple errors can
|
|
|
|
|
occur.
|
|
|
|
|
|
|
|
|
|
Transient errors from external resources (like network or disk) should be retried with exponential
|
|
|
|
|
backoff. This is especially important for operations that can fail due to temporary issues, such as
|
|
|
|
|
network timeouts or disk I/O errors. Assume that there **will** be transient errors, and design your
|
|
|
|
|
code to handle them gracefully.
|
|
|
|
|
|
|
|
|
|
**A note on data corruption**
|
|
|
|
|
|
|
|
|
|
Data corruption is a serious issue that can occur in any system. It can happen due to hardware
|
|
|
|
|
failures, software bugs, or even malicious attacks. To mitigate the risk of data corruption, we
|
|
|
|
|
recommend the following:
|
|
|
|
|
|
|
|
|
|
- **Use checksums**: Always use checksums to verify the integrity of data before and after
|
|
|
|
|
processing. This helps ensure that the data has not been corrupted during transmission or storage.
|
|
|
|
|
|
|
|
|
|
- **Use versioning**: Use versioning to track changes to data and to ensure that the correct version
|
|
|
|
|
of the data is being used. This helps prevent data corruption due to software bugs or changes in
|
|
|
|
|
the data format.
|
|
|
|
|
|
|
|
|
|
- **Use backups**: Always keep backups of important data to prevent data loss due to corruption or
|
|
|
|
|
other issues. This is especially important for critical data that cannot be easily recreated.
|
|
|
|
|
|
|
|
|
|
Always write software with the assumption that things will go wrong. Packets will arrive out of order,
|
|
|
|
|
more than once, or not at all. Data that you wrote to the disk might have been overwritten or corrupted
|
|
|
|
|
by another process. Maybe the disk is failing. We need to be prepared for these scenarios and
|
|
|
|
|
design our code to handle them gracefully. This means using checksums, versioning, and backups to
|
|
|
|
|
ensure data integrity, as well as implementing robust error handling and retry logic for transient
|
|
|
|
|
errors. By doing so, we can minimize the risk of data corruption and ensure that our software is
|
|
|
|
|
resilient to failures.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Dependencies
|
|
|
|
|
|
|
|
|
|
We strive to maintain a **zero dependency** codebase. This means that we avoid using third-party
|
|
|
|
|
tools, even in development. This ensures that supply chain attacks are minimized exclusively to the
|
|
|
|
|
go toolchain.
|