345 private links
Your deployment strategy
What you need:
• A single server
• Postgres
• Maybe Redis for caching
• That's it
Building up means starting with a tiny core and gradually adding functionality. Sanding down means starting with a very rough idea and refining it over time.
There are a few rules I try to follow when building up:
- Focus on atomic building blocks that are easily composable and testable.
- Build up powerful guarantees from simple, verifiable properties.
- Focus on correctness, not performance.
- Write the documentation along with the code to test your reasoning.
- Nail the abstractions before moving on to the next layer.
The alternative approach, which I found to work equally well, is “sanding down.” In this approach, you start with a rough prototype (or vertical slice) and refine it over time. You “sand down” the rough edges over and over again, until you are happy with the result. [...] I find that this approach works well when working on creative projects which require experimentation and quick iteration.
When using this approach, I try to follow these rules:
- Switch off your inner perfectionist.
- Don’t edit while writing the first draft.
- Code duplication is strictly allowed.
- Refactor, refactor, refactor.
- Defer testing until after the first draft is done.
- Focus on the outermost API first; nail that, then polish the internals.
Both variants can lead to correct, maintainable, and efficient systems. There is no better or worse approach. However, it helps to be familiar with both approaches and know when to apply which mode. Choose wisely, because switching between the two approaches is quite tricky as you start from different ends of the problem
The term was coined to explain a deliberate process where you write software quickly to gain knowledge
"If you discover a better way to do things, the old way of doing it that is embedded in your code base is now “debt”:
- you can either live with the debt, “paying interest” in the form of all the ways that it makes your code harder to work with;
- or you can “pay down” the debt by fixing all the code in light of your new knowledge, which takes up front resources which could have been spent on something else, but hopefully will make sense in the long term.
Technical debt isn’t just bad code—it represents the lessons you’ve learned about how to build software better. Refactoring isn’t “paying off a debt,” but investing in applying that knowledge. Ignoring it wastes what you’ve learned and, over time, leads to lost value and competitive disadvantage compared to those who actively improve their code.
Can I really say “we now know” that the existing code is inferior? Is it true that fixing the code is “investing my knowledge”?
A collection of antipatterns and how to avoid them
The more you want to calculate at query time, the more you want views, calculated columns and stored or user routines. The more you want to calculate at normalized base update time, the more you want cascades and triggers. The more you want to calculate at some other (scheduled or ad hoc) time, the more you use snapshots aka materialized views and updated denormalized bases. You can combine these. Any time the database is accessed it can be enabled by and restricted by stored routines or other api.
Until you can show that they are in adequate, views and calculated columns are the simplest.
The whole idea of a DBMS is to store a representation of your application state as the database (which normalization reduces the redundancy of) and then you query and let the DBMS implement and optimize calculation of the answer. You haven't presented a reason for not doing that in the most straightforward way possible.
The method to build software feature by iteration is a mistake long-term.
Carrying over this approach past the learning phase was a mistake.
It is possible to dramatically cut the amount of bugs you introduce in the first place, if you focus on optimizing that (and not just the iteration time)
One super power is bugs can be found while reading the code.
The key is careful, slow reading. What you actually are doing is building the mental model of a program inside your head.
If you are reviewing a PR, don’t review just the diff, review the entire subsystem.
Follow the control flow or stare at the state
Rule 1: Restrict all code to very simple control flow constructs
Rule 2: Give all loops a fixed upper bound.
Rule 3: Do not use dynamic memory allocation after initialization.
Rule 4: No function should be longer than what can be printed on a single sheet of paper in a standard format with one line per statement and one line per declaration. Typically, this means no more than about 60 lines of code per function.
Rule 5: The code's assertion density should average to minimally two assertions per function. Assertions must be used to check for anomalous conditions that should never happen in real-life executions. Assertions must be side-effect free and should be defined as Boolean tests. When an assertion fails, an explicit recovery action must be taken such as returning an error condition to the caller of the function that executes the failing assertion. Any assertion for which a static checking tool can prove that it can never fail or never hold violates this rule.
Rule 6: Declare all data objects at the smallest possible level of scope.
Rule 7: Each calling function must check the return value of nonvoid functions, and each called function must check the validity of all parameters provided by the caller
Rule 8: preprocessor must be limited to the inclusion of header files
Rule 9: The use of pointers must be restricted. Specifically, no more than one level of dereferencing should be used.
Rule 10: All code must be compiled, from the first day of development, with all compiler warnings enabled at the most pedantic setting available. All code must compile without warnings. All code must also be checked daily with at least one, but preferably more than one, strong static source code analyzer and should pass all analyses with zero warnings.
Native libraries are hard in Rust; the compiler offers no guarantees about the memory representation of structs; or these structs needs to be FFI-Friendly with unsafe extern "C". There is no sandboxing, so a malicious code could promise the machine; or corrupt the memory silently.
Finally, dynamic library plugins are distributed as compiled code.It's easier to hide backdoors in compiled code. It's also harder to share the code than simple scripts.
There are two main ways to embed JavaScript in a Rust program. The first one is with bindings for the lightweight QuickJS engine, such as rquickjs. Take a look at AWS' LLRT (Low Latency Runtime) for an advanced integration of QuickJS in Rust.
Ho, and did I mention that QuickJS is lightweight? Around 210 KiB of code vs around 40 MiB of code for V8, all while being fast enough for most situations.
My vision of programming is to limit ourselves to two programming languages. A very powerful, secure and fast compiled language for the lower levels of the computing stack, Rust, and a less powerful and slower scripting language for high-level scripting and user interfaces, JavaScript.
Another alternative is WASM. The provided code is already compiled; and the author judges the ecosystem too immature.
The last method is the less powerful: expression engine. It allows to specify a language with some rules, even if it is not turing complete and the result always evaluate to an expression.
So to rank them:
- scripting language
- expression engine
- WASM
- at the end native libraries
Rule 1: Only assert properties you are 100% confident about
Rule 2: Do not assert statements about the external world
Rule 3: Assert properties that your code relies on
Rule 4: Pair with coarse-grained recovery for online systems
Rule 5: Make them as easy to deploy as possible
I agree: if we want to build complex products, we have to hide implementation details. It means for example at some level, the type or structure should abstract an empty value for me: 0, null, undefined, false, NULL, etc...
I have the same idea for a node js backend serving a fancy UI :)
It would be better to split the UI and the server while developing to benefit from hot reloading.
Example: https://git.sr.ht/~pyrossh/rust-embed/tree/master/item/examples/axum-spa/main.rs
How We Structure It – No Guesswork Needed:
- One frontend framework (Astro) for performance and design flexibility
- One headless CMS (DatoCMS) structured for multi-brand, multi-region content
- One hosting platform (Vercel) that scales automatically
- One monorepo for all brands and sites
- One dev team managing the whole thing
However, there’s already something that I’ve singled out to use with my own and my teams’ work—to aim for 80% utilization.
Simple rules to handle load
A model
(via https://nicolas-delsaux.hd.free.fr/Shaarli/shaare/1L8NcQ)
Hydro is a high-level distributed programming framework for Rust. Hydro can help you quickly write scalable distributed services that are correct by construction. Much like Rust helps with memory safety, Hydro helps with distributed safety.
CGP makes use of Rust's trait system to define generic component interfaces that decouple code that consumes an interface from code that implements an interface. This is done by having provider traits that are used for implementing a component interface, in addition to consumer traits which are used for consuming a component interface.
Either all fields are public or all fields are private.
A dependency in software can be summarized to a checksum, a location, a name and a version.
The checksum checks the integrity, the location defines how to retrieve the dependency, the name identify the dependency and the version its substitutability (major with breaking changes, minor if it has new features or bug fixes).