This is what I call a “matrix problem”: we can structure our system along two or more axes. Here, we have one axis relating to features (book info, author details, publisher info), and an axis relating to different steps or layers (I/O to load info, assembling the table). Our actual code will fill the full matrix of features × steps combinations.
But textual programming languages are linear, so we have to somehow sort this 2D-matrix of code elements. On a fundamental level, both axes are equivalent and we can freely choose what to do. Both of your solutions are perfectly valid.
But there are trade-offs for organizing the code one way or another.
First, let's consider organizing by feature, which you call the “modular option”.
- This keeps all of the code relating to one feature together, making it easy to see the entire data flow for that feature. It is easy to add new features, or to change the behaviour of one feature since they are well isolated.
- However, it is difficult to make changes to cross-cutting concerns, such as changing how data is loaded and how the table is assembled. To perform such changes, we would have to touch all modules/features.
- While it is easy to perform tests for each feature in isolation, those tests will now generally have to be end-to-end tests. In your example, testing the table update would probably also involve I/O.
This strategy is somewhat common across the software stack, for example with microservices architectures, micro-frontends, or components in React/Vue. Compare also the concept of “bounded contexts” in domain-driven design.
Alternatively, let's consider organizing by layer, which you call the “pure option”.
We group code one the same level together, even if it relates to different features.
This completely flips the pros and cons.
- It is now easy to change things on one level, for example changing a database technology, changing the user interface, or in your case changing the table structure.
- However, it becomes more difficult to work on individual features. Adding a new feature or modifying an existing one will require changes across different layers.
- We can easily write unit tests that exercise each layer in isolation. However, it becomes more difficult to test features in and end-to-end manner, since it's not clear which functionality of the lower layers is depended upon.
This strategy is extremely common. We see this separation in the original MVC architecture, in many design patterns, in the classic layered architecture (presentation – business – persistence – database), in the Clean/Hexagonal/Onion architecture, and in concepts like “functional core, imperative shell”.
In general, you can choose by considering which changes are likely. Are you likely to change the UI without changing the business logic, or vice versa? Consider organizing by layer. Or are you more likely to keep the code in each layer stable, but you want to add more features easily? Then organizing by feature makes more sense.
But these approaches are not complete opposites – you should draw module boundaries wherever they are appropriate. For example, a large web application might separate the frontend/UI from the backend (organized by layer), but divide the backend into separate microservices (organized by feature).
In general: things that change together should be close to each other. The fancy word for this is “cohesion”, compare also some interpretations of the “single responsibility principle”.
Ultimately, the important point is not to follow some architecture for the sake of the architecture, but to look for seams in your codebase where it is appropriate to decouple modules.
The importance of these architectural principles also varies with the scale of the system.
In short examples, it just doesn't matter.
In larger programs, consider the above heuristics about the direction of change.
But architecture becomes more important when the software system is maintained by a team of people, or even by multiple teams. At scale, the software architecture and the organization's structure will likely mirror each other (→ Conway's Law). If individual teams should be able to deliver value independently, this will likely require some organization by features, for example by adopting a microservice architecture.