The Cost of Architectural Symmetry
Why different responsibilities deserve different amounts of structure. There is something reassuring about opening a codebase and recognizing its structure. The controllers are where we expect them to be. The services follow a familiar convention. Before we understand the details, we already have a sense of how to move through the system. I think that predictability explains a lot of our attachment to consistency. But familiar structure can also make us less likely to examine what each layer contributes. A controller calls a service, which calls another service, which calls a repository. The dependencies point in the correct direction. Everything has a place. And yet a small operation can pass through an impressive amount of architecture before anything happens beyond passing the request along. In my previous post, I wrote about boundaries that change the physical conditions under which work happens. Moving an operation between processes introduces communication, data movement, and uncertainty. Layers inside a process usually have a much smaller execution cost. But some of their cost is paid by the person trying to understand the system. Somebody still has to follow the operation, find the business rule, and determine where a change belongs. When several components pass the same arguments along, understanding the behavior means carrying that entire chain in your head. The machine might move through those calls cheaply. The engineer still has to make sense of them. There are reasonable reasons to introduce layers. A controller can translate an HTTP request into an application operation. A service can coordinate business behavior. A repository can isolate persistence details. Even a thin layer can establish a useful contract or contain a dependency that changes. But the responsibility should explain the layer. I feel like we often start with the layers and then try to find responsibilities to put inside them. Consider an endpoint that reads a customer’s preferred language. To follow that read, we open a controller, a service interface, a service implementation, a repository interface, and a repository. The service simply forwards the customer ID and returns the result. When the lookup needs an account ID as well, that extra argument travels through every signature and forwarding call. Only the query uses it to do anything. Several files change to support a decision made in one place, because the simple read inherited the structure of a more complicated operation. And I think a lot of this comes from our attachment to symmetry. Once one feature has a controller, a service, and a repository, a feature that only needs two of those starts to look incomplete. A straightforward read suddenly needs a place for business rules it does not have. So we create a service that passes the request along. The absence of a layer feels like an inconsistency, even when there is no responsibility for it to own. We are creating code to preserve the appearance of the architecture. The architecture of a house offers an interesting comparison. A bathroom needs plumbing and waterproofing. A kitchen needs ventilation and space for equipment. A bedroom has different requirements around privacy, light, and comfort. These rooms follow shared construction standards, but their structures differ because their purposes differ. Nobody expects a bedroom to contain the same fixtures as a bathroom to make the building more consistent. Software should allow that kind of variation too. Reading a value and coordinating inventory, payment, and order creation are different operations. Their structures can reflect those differences while following understandable conventions. Forcing both through the same sequence of components makes the simple operation harder to follow without necessarily making the complicated one clearer. The problem becomes more significant when the same expectation extends across entire systems. An organization creates a template, defines the approved layers, and expects every application to follow it. A small internal tool, a payment system, and a high-volume data pipeline inherit the same architecture before their requirements have been understood. This is where physics, engineering, and architecture become especially important. One system might spend most of its time waiting for external responses. Another might be constrained by memory bandwidth or contention around shared state. These physical constraints call for different engineering decisions, and those decisions should shape the architecture. The teams and operational requirements differ too. A boundary that helps several independent teams work can be excessive for an application maintained by two people. But when the template comes first, architecture becomes policy. Engineering becomes the work of fitting the problem into it. Physics becomes something we encounter when the approved structure performs differently from what we expected. The order has been inverted again. Templates can still preserve useful knowledge. Shared approaches to logging, configuration, deployment, and observability remove repeated work. Common conventions can reduce the burden of maintaining several systems. Those are real engineering benefits. But they need to justify the choices in the template. A standard does not give every component a useful role in every application. There is also a recurring argument behind many of these layers: we might need them later. The operation might acquire business rules. The dependency might need to be replaced. Sometimes that preparation is sensible, especially when a likely change would be expensive to accommodate. But the extra components already need to be understood and maintained. We are paying for flexibility before we know which kind we will need. Engineering has to distinguish between a change we have reason to expect and a possibility that is merely easy to imagine. The more I work on systems, the more I feel that useful architecture makes the important decisions easier to see. It gives a responsibility a clear home and makes complicated behavior understandable. Its value comes from what people can do with the system because that structure exists. Different parts of a codebase can need different amounts of structure. Different systems can need different architectures. Understanding those differences is part of the engineering work. The rooms belong to the same house. Their purposes justify their differences. A layer should exist because the system needs what it provides. Symmetry alone is a surprisingly expensive reason to write code. Top comments (0)
Comments
No comments yet. Start the discussion.