6 ms·
Build files are the best tool to represent software architecture
- flanked-evergl 1y agoDoubt it. The problem detailed at the start of the post can be solved by static analysis/linting. And no amount of new strange files will give you the same benefit as using some stock standard static analysis tools.
- jmmv 1y agoPlease elaborate on what those "some stock standard static analysis tools" are.
- flanked-evergl 1y agoFor python, use ruff. https://docs.astral.sh/ruff/rules/unused-import/ https://docs.astral.sh/ruff/rules/unused-import/
- jmmv 1y agoI don't think the article talks about unused dependencies anywhere, does it?
- flanked-evergl 1y agoVery first complaint is that one can't tell from imports what is being used or not. If you use a linter that removes unused imports then you can tell.
- jmmv 1y agoNo. The very first complaint is that if you don't know what is being used or not, you cannot tell whether a new "import" will add _another_ cross-module dependency that did not yet exist. As I already mentioned in another comment, you have: .../module1/__init__.py -> import module3 .../module1/foobar.py -> import module3 .../module1/baz.py -> import module2 Now you are editing or reviewing a change to foobar.py. How can you tell if depending on module2 is conceptually OK? You need to look at baz.py to prove to yourself that the dependency already exists. Or you need to know a priory that it's an OK thing to do.
- bschwindHN 1y agoI must live in some alternate reality where this just isn't a problem, or maybe the author has not described the problem well. But reading the article, it just feels like some software form of bureaucracy.
- Mesopropithecus 1y agoWas gonna write that it pays dividends only from a certain project size onwards, but in fact it could be true from a certain org size instead. So yes, it has some component of bureaucracy-as-code, and I agree with the author that that can be a good thing.
- jmmv 1y agoIf you work on a well-modularized codebase, with small and individual Go modules, or Rust crates or NPM packages or whatever you call them... you do not live in an alternate reality. You are doing essentially the same as I was describing because the package managers for those tools force you to express cross-dependencies explicitly in a separate "metadata file". You are just doing so via a different, language-specific tool. The problem appears when: 1. you have a significant number of people working on the project and 2. one of these "modules" has become too big internally where it's hard to make sense of its internal structure. Having a monorepo makes the problem even more likely. At that point, you'll probably want to start breaking up that gnarly module into pieces so that you can see its structure again, right?
- esafak 1y agoThen you might have titled the article "BUILD files help reveal architecture on poorly organized code bases".
- deleted 1y ago[deleted]
- CuriouslyC 1y agoI like to take artifacts people are guaranteed to need and use them to represent architecture via introspection. Package files and manifests are enough to get you 80% of the way there, you can add annotations to package files and docstrings/doc comments to go as deep as you need. That keeps your architecture in Git and coupled to the code.
- xyzzy_plugh 1y agoThis is only true when the dependency structure is not already apparent. Almost all modern languages solve for this in their import statements and/or via their own package manager, at which point pushing everything up into Bazel is indeed redundant. If anything this highlights the failure of languages solving for this themselves. I'm looking at you, C++. It's no surprise Bazel is a hard sell for Rust, Go, Node, etc. because for those languages/ecosystems Bazel BUILD files are not the best tool to represent software architecture.
- eej71 1y agoBazel is a hard sell overall.
- kyrra 1y agoBut there is a lot of C, C++, and Java in the world. It also helps in a mono-repo to help control access to packages. BAZEL makes it so you can't import packages that aren't visible to your package.
- jmmv 1y agoThe problem is that anything that's _apparent_ and not _enforced_ will be messed up over time. Maybe not in a project with few people where everyone is an expert on how "things are supposed to be", but it will inevitably happen when you add more and more people. And the whole point of the article is to say that import statements do actually _not_ solve this issue, because import statements are at the file level, not at the module level (whatever module means in your mind). In any case. As I mentioned in the article en passing, other languages _do_ provide similar features to Bazel's build files though, and I explicitly called out Rust as one of them. When you are defining crates and expressing dependencies via Cargo, you are _essentially doing the same_ as what I was describing in the article. Same with Go if you are breaking your code apart into multiple modules But then we all know that there are some huge repos out there that are just "one module" and you can't make anything out of their internal structure. Hence you start breaking them apart into Crates, Go modules, NPM packages, you name it or... you know, add Bazel and build files. They are the same tool -- and that's why I didn't write Bazel in the title, because I imagined "build files" more generically. I guess I needed to be clearer there.
- simpaticoder 1y agoMany words and few to the point. tldr: This person likes build-tools (Google's Bazel) that can constrain dependencies to be used only by certain packages, and says this documents the project's architecture. I've always thought of "architecture" as a high-level description of run-time behavior, not a set of compile-time dependency constraints.
- npodbielski 1y agoExactly. I thought that in modern languages and frameworks there are better tools to do that like 'ProjectReference' in .Net. Oh well..
- jmmv 1y agoI have worked with ProjectReference before. How is it different from expressing a cross-module dependency in a Bazel BUILD file? But as I already said in two other comments in this discussion, ProjectReference would be equivalent to what I'm describing in the article, just using language-specific tooling. If you are breaking your solution into various projects and keeping them separate with cross-references among them, you are doing exactly what I was describing already.
- kiitos 1y ago> the build graph -- the very thing that BUILD files define -- is the best place to encode [dependencies] in a programmatic manner. so the thing is that a BUILD file doesn't define the build graph, it approximates it -- the build graph is always defined by language-specific tooling and specifications it's fine that the BUILD file is an approximation! that's as good as you can do, if you want to try to model dep relationships between heterogeneous languages so when we're talking about the dep graph, "using language-specific tooling" isn't a detail you can brush aside, it's a core requirement for correctness, really
- npodbielski 1y agoSo this incomprehensible file at the end of article, that supposed to be "lean" is what the author is fighting for? And it supposed to show 'architecture'? Wow. I am happy that I never started working with Java. That is terrible.
- simpaticoder 1y agoThis article has nothing to do with Java, and the author explicitly states that.
- npodbielski 1y agoBut somehow all the examples involve it.
- jmmv 1y agoIn the article, yes. Look at the other comment threads in this discussion though: they touch upon many other languages.
- timcavel 1y ago[dead]
- matttproud 1y agoOne of the worst things a developer accustomed to Bazel (and its relatives) can do with a modern language (say Go or Rust) is to model code and organize it through the Bazel concept of a build target (https://bazel.build/concepts/build-ref https://bazel.build/concepts/build-ref) first and then second represent it with the language's local organization concepts versus the other way around. One should preferentially model the code with the language-local organizing concept in an idiomatic way (e.g., a Go package — https://go.dev/ref/spec#Package_clause https://go.dev/ref/spec#Package_clause) and THEN map that instance of organization to a build target (e.g., go_library). When you do this in the wrong order, you end up with very poorly laid out concepts from a code organization standpoint, which is why vagaries like this needed to be written: * https://google.github.io/styleguide/go/best-practices.html#package-size https://google.github.io/styleguide/go/best-practices.html#p... * https://matttproud.com/blog/posts/go-package-centricity.html https://matttproud.com/blog/posts/go-package-centricity.html In languages that operate on a flat namespace of compilable units (e.g., C++ or Java), build target sizing and grouping in Bazel (and its relatives) largely doesn't matter (from a naming the namespace and namespace findability+ergonomics perspective). But the moment Bazel starts interfacing with a language that has strict organization and namespacing concepts, this can get rather hairy. The flat namespace practice with Bazel has (IMO) led to code organization brain-rot: > Oh, I created another small feature; here, let me place it in another (microscopic) build target (without thinking about how my users will access the symbols, locate the namespace, or have an easy way of finding it). — — — Note: The above is not a critique of Bazel and such. More of a meta-comment on common mispractices I have seen in the wild. The build system can be very powerful for certain types of things (e.g., FFI dependency preparation and using Aspects as a form of meta-building and -programming).
- actionfromafar 1y agoNot having used Bazel in anger (or barely at all) I think I might understand what you mean, but this topic cries out for a blog post.
- kiitos 1y agoindeed, no idea why so many folks seem to think `bazel` is like some co-equal alternative to language-native build tooling/processes, it's a fine tool for certain (niche) use cases but in no way is it ubiquitous or anything approaching a common standard
- kiitos 1y ago> BUILD files give you a chance to encode the high-level architecture of your software project as a graph of dependencies that lives outside of the code. this is wild, the code literally defines dependency relationships at the mechanical level, it's the source of truth of course you can't derive dep graphs from source code via simple file-based grep analysis, but that's obvious? you need to use language-specific semantically-aware tools to do that. if the author believes otherwise that's their mistake
- joaonmatos 1y agoAnother fine day to be using Brazil
- commandersaki 1y agoAh not so good memories.
- cadamsdotcom 1y agoA great thing to put in your codebase is tooling-informed - indeed, tooling-enforced - protection of architectural constraints. Preventing your backend’s web-route handler functions from directly instantiating a database client, forcing the code to instead access the db via a logic or service layer, preserves separation of concerns as the codebase grows. It’s obvious to human software engineers and usually is institutional knowledge, and everyone hates having to tell someone in a code review that they broke a rule that “everyone” knows.. Instead, use tooling to enforce these separations. That lets both new employees and agents autonomously work without making a mess and without other humans “in the loop” informing them not to break rules that aren’t written down anywhere - because when you make tools, now the rules are written down. LLMs can quickly create scripts in your language of choice, that walk the AST of your code before you commit. They can check for violations of rules as arbitrary as you like. Put that little script in your codebase, run it every CI run, and it’ll keep paying dividends. It’s like linting for architecture.
- bloppe 1y agoAt Google, you can just run "buildifier" to auto-generate the build files from the source code import statements, so clearly the build files are redundant. That's not the point. Bazel is supposed to be fast, and keeping the analysis graph loaded in memory is part of that. I always thought of the build files as a sort of cache. It would usually take a few seconds for buildifier to "revalidate" that cache, which can be done infrequently, then the actual build itself, which happens much more frequently while iterating, is faster. You really start to notice the performance difference in larger repos.