Avoid mini-frameworks
25 points by jeremiahlee
25 points by jeremiahlee
The real and only difference between a library and a framework, is whether it introduces new concepts.
I'm not sure I really agree with that definition.
The definition I've read (a long time ago, I forget where) that gave the most succinct description was:
A library is code your code invokes/calls.
A framework is code that invokes/calls your code.
What happens when it’s both? And what happens when a framework is built on a framework?
Both is still a framework, «main loop out of your hands» takes priority.
Frameworks surely can be nested, the intermediate-framework defines callbacks for top-level-framework, those callbacks do some of the work but call your callbacks for fine-grained choces.
I wrote the code in my angular webapp that calls the framework, then in a stunning soviet style reversal the framework calls me.
A lot of this comes down to it's really hard to design good APIs. A widely used official framework has probably had a lot of work put into its API design. Something quickly built probably hasn’t. Plus, as soon as you start creating new abstractions there's going to be impedance mismatch with the underlying ones.
One of the benefits of categories / class extensions — the ability to define new methods on external classes — is that they let you add functionality to existing frameworks, solving pain points without having to wrap everything in a new layer.
This! I did find Ousterhout's Philosophy of Software Design quite good at articulating what makes a good API/framework. Abstractions can simplify, but simplicity is one of the hardest thing to achieve.
Strong agree. I don't even like internal company projects to wrap libraries across services. At the last business I worked for we (5-6 engineers) used yt-dlp in a few places and there were a couple suggestions over the years to wrap it in an internal library. My counter-suggestion was always that the uses were either different enough that combining them into a library doesn't reduce the amount of work but increases the maintenance burden in various ways, or the uses are similar enough that we should roll out a service (we were using microservices) that would do the video downloading for us. Maybe with a monorepo it's a good idea but in my experience trying to over-apply DRY makes life harder.
Time is a flat circle. We had this discussion 15-20 years ago with PHP and then Rails came and then Symfony and other PHP frameworks that established themselves as the standard came.
And now we have things like React and I would take any mini framework of a random person over it.
Not sure if other companies are like this, but at Google, I've never actually seen a code migration fully complete, which is kinda hilarious.
I think it's actually pretty rare that migrations are ever fully complete. Often, there's a strong business case to move part of the system off the legacy framework/library, but a much weaker business case to move all of it. The cost of migrating the "long tail" of legacy code is often higher than the very minimal maintenance cost, since it doesn't need to change very often.
I think the article is directionally right, but I don't think it locates the distinction in the right place. I would ask two questions:
What do I mean by "fuck around and find out?" Questions arise about what an operation should do, whether something is a bug, etc, and there's no clear answer, or if there is an answer, it's a matter of taste more than anything that can be justified to others. Decisions are often made by fiat.
If the answer is yes/yes, then it's probably safe to use the higher level abstraction. There's still a question about whether it pays its way, but at least it should not be too hard to move back and forth between the levels.
If it's no/yes, I think you'll have a bad time--it takes a lot of work to build a stable foundation on top of something poorly defined. If it's yes/no, that's also quite bad, you're losing a lot for the higher level framework, and the lack of clear semantics often leads to the "only one person knows how this works" problem.
If it's no/no, it's probably a complicated case by case scenario, you may not have a lot of great options.
I agree a lot with this article, having dealt with teams that built dsls over existing languages that forced people into bad practices and made it hell to change the foundation.
I think it's related to inner platform effect, and oil shell introduced this term or interior vs exterior extendability
https://www.oilshell.org/blog/2023/06/narrow-waist.html
I think it's better to try to build exterior extensible tooling.
I've met a similar situation as the author described, where the mini-framework is feature-incomplete, with nonexisting documentation, and you pretty much need to constantly talk to the author to figure out how to use it to do certain things
Frustrating boilerplate and repetitive code can be easily solved with shared snippets, served by e.g. Simple Completion Language Server for cross-editor completions. Our team adopted such solution few months ago, and it reduces context switching during the review process (no need to dig through the layers of abstractions).
At $FORMER_JOB, rather than using a framework to reduce boilerplate, we just used a template. Building a new service was a simple matter of cloning the template repo.
This solved a number of problems: every service started the same way, it was easy to follow the code, upgrades were decoupled, and (most important) you could design the service based on the requirements rather than the foibles of the framework.
We did supplement this with a library but our attitude was the same. It was just a library.
We're doing the same at $JOB and I agree with the general sentiment, but it makes upgrading or standardising some things after the fact very painful.
When somebody fixes something they tend to do it in the repos they usually work on and maybe in the template, but there's a long tail of services that cloned the template repo at various stages of development and it's hard to understand why the way some services do some things are different: was it because of business requirements or just because the service was developed before the template was updated? We do have libraries but they are mostly about data that is shared (events, models) rather than standardizing things.
I'm working on a service in a different language from the rest of the stack which I hope we're gonna use in new services as well, and I'm taking care to move the boilerplate that I see copied in every service into a library. I'm talking stuff like transaction management helpers, setup for logging and telemetry, various middlewares... These things have more or less the same implementation in every service and there's little reason why they should deviate from the norm.
I think using these helpers should not be required in case they don't make sense, but being able to fix something in one repository and have it applied everywhere by upgrading the version (which could be automated) rather than searching and fixing the offending code in every repo is much better.
We also do this at $JOB and I must say I have warmed up to it or at least accepted that it works. There are no internal libraries, just relentless copying/stealing/using as a template. Of course the disadvantages are well-known but if everyone is in the habit of checking for things that should be fixed it's ingrained to quickly fix it in 5 projects and not just one. Like yes, of course sometimes you wish you didn't have to do this, but not having "ugh, the other team doesn't cut their release of library X for our weird special case" is also fine. Guess it needs to fit the domain and I'm not talking about things where we have teams constantly reinvent everything.
But on the other hand, maybe I just have really bad experience with another team not fixing stuff and you end up just not fixing it, or having your own vendored fork and then throwing away your fix once they release and add 10 other things. I guess other orgs solve exactly this with a monorepo. You change it, you need to make sure all other teams' tests don't fail - but this is the asynchronous version.
That also works, but for us it was tricky to do with Erlang where everything is tightly integrated into “distributed monolith”. Hence why snippets were nicer way to deal with templates, similarly in SwiftUI with snippet-based component and styling templates. Libraries that we write are meant to be “sane defaults” like custom protocol, in-house implementations of thrift etc. from which you can derive entirely new projects.
I should have said this up front, but snippets are a great idea, and I meant to imply that templates are just an example of that. I'm definitely going to look at using snippets more widely in my new projects. I typically embed them in documentation, but I like the idea of formalising them a little.
I think the last 5 decades of DRY has been very bad for the industry. Like all things, there is a middle ground, and often a template or pattern is a better fit than avoiding repetition on principle alone. Some things should be templates, some things should be snippets, and of course some things should be libraries.
Though - as a refugee from 2 decades of Java - I am not convinced that anything should ever be a framework.
Makes sense. I'm recovering from 7 years of Rust, where everything had to become a library due to the compiler design. A crate is a single compilation unit, leading to awfully long compile times if you don't split it all up.