Skip to main content

Systems

Everything up to this chapter was about keeping a line, a function, or a class clean. This chapter zooms out to the level of the whole application: how do the pieces get built, and how does the system stay easy to change as it grows past what anyone planned for on day one?

Separate constructing a system from using it

Building something and using it are different activities with different concerns. A building under construction is full of cranes, scaffolding, and workers in hard hats; the same building a year later is quiet, finished, and occupied. Software should draw the same line: the startup process, where objects get created and wired together, is a different concern from the runtime logic that uses those objects once they exist, and mixing the two together tangles both.

A common way this tangling shows up is lazy initialization - a method that checks whether its dependency exists yet and constructs it inline if not. That single if block quietly takes on two responsibilities: it decides whether to build something and it decides how to use it, so any function containing it is no longer doing one thing. It also hardcodes a concrete dependency at the exact place the abstraction was supposed to be used, which is exactly the kind of coupling that makes testing painful - swapping in a fake for a test now means reaching into that method instead of just passing one in.

# The application reaches out and builds its own dependency.
# Every test of OrderApplication now needs a real database reachable.
class OrderApplication:
def __init__(self):
self.repository = SqlOrderRepository(connect_to_prod_db())
 
def run(self, order):
self.repository.save(order)
# The application only knows the shape of a repository, not
# how one gets built. Main is the only place that ever
# constructs a concrete SqlOrderRepository.
class OrderApplication:
def __init__(self, repository):
self.repository = repository
 
def run(self, order):
self.repository.save(order)
 
# main.py - the one place construction happens
def main():
repository = SqlOrderRepository(connect_to_prod_db())
app = OrderApplication(repository)
app.run(next_order())

The cleanest fix is to push all construction into one place - typically a main function or a small set of modules it calls - and let the rest of the application assume its dependencies simply arrive, fully built. Dependencies should only ever point away from main: the application code has no idea main exists, it just uses whatever it was handed.

OrderApplication+ run()«interface»OrderRepository+ save(order)SqlOrderRepository+ save(order)Main+ main()BUILDS
implementsusesassociation
Main is the only place that knows the concrete repository exists.

Sometimes the application itself needs to control when an object gets built, not just receive it once at startup - an order-processing system might create a new line item every time a customer adds one. A factory is the tool for that: it lets the application decide the timing while the factory (built and injected by main) hides the concrete construction details.

Dependency injection and inversion of control

Dependency Injection is the general version of the pattern above. Instead of a class resolving its own dependencies, it stays completely passive: it declares what it needs through constructor parameters (or setters), and something else - a container, a framework, or just main - is responsible for supplying the real objects at startup. This is Inversion of Control applied specifically to dependency management: the class no longer controls how its dependencies come into existence, that responsibility moves to a dedicated authority. Frameworks like Spring formalize this with a configuration file or set of annotations that describes which concrete classes satisfy which interfaces, so the wiring lives in one declared place instead of scattered across constructors.

Cross-cutting concerns and scaling up

Some concerns don't respect the tidy boundaries of your domain classes. Persistence, transactions, logging, and security all need to touch nearly every object in a system, in a consistent way, even though none of those concerns are "about" any single class. Sprinkling persistence code into every business class defeats the whole point of decomposing the system into small, single-responsibility pieces - the persistence logic gets duplicated everywhere it's needed, instead of living in one place.

The fix is to keep business logic in plain objects that know nothing about persistence, transactions, or the framework running them, and to layer those cross-cutting concerns on from the outside - through a decorator or proxy that wraps the plain object, or through a framework's declarative configuration that wires the wrapping up automatically. The business object stays a "Plain Old Object" focused entirely on its own domain, easy to test in isolation because it depends on nothing but the abstractions it actually needs. Whatever persistence or transaction behavior gets added later is added around it, not into it.

This same separation is what allows a system's architecture to grow incrementally instead of needing to be right on day one. A building can't be redesigned once the foundation is poured, but software isn't bound by that constraint - if the domain logic stays decoupled from infrastructure concerns, a naively simple architecture can scale up by adding infrastructure later, without a rewrite. Deferring an architectural decision until you have real information about what's actually needed is not laziness, it's postponing a costly choice until you can make it with more evidence.

Domain-specific vocabulary

The same POJO discipline extends to the language the code is written in. A small domain-specific language - a fluent API or a purpose-built mini-language layered on top of the host language - lets policy-level code read close to how a domain expert would describe it, instead of as a stack of framework calls. That closes the gap between what the business asked for and what the code says, and reduces the chance that a translation from one to the other introduces a bug.

Conclusion

A system stays clean the same way a class does: by keeping each concern in its own place and making the seams between those concerns explicit. Separate construction from use, push cross-cutting concerns to the edges instead of weaving them through the domain logic, and resist the urge to lock in an architecture before the system has told you what it actually needs.