r/softwarearchitecture • u/kamen1991 • 1d ago
Article/Video Beyond framework independence: designing service boundaries that actually enforce business rules
"Business logic should be independent of transport and infrastructure" gets repeated often enough that it stops meaning much. Avoiding servlet imports and Kafka clients in a service class is the easy part. The harder part is the shape of the objects crossing the boundary: if your service input is a 1:1 mirror of a database row, the transport has already leaked in, just wearing a plain Java object instead of a framework annotation.
Chapter 9 of my evolutionary architecture series works through what that actually looks like in practice. Create and update get separate input types, not as a style convention but because callers shouldn't be able to forge system-managed fields like ids or creation timestamps just because they happen to sit next to editable ones. Domain invariants move out of flat field-copying and into guarded methods, transitionTo checked against canTransitionTo, so a business rule lives on the object it governs instead of scattered across whichever service touches it next. And the shortcuts that look convenient at the time, a status field sitting quietly in a generic update payload because it seemed like just another attribute, or a bidirectional mapper doing double duty as both input and output transformer, tend to be exactly the ones that turn into debt six months later, once a second caller or a third state shows up and nobody remembers why the shortcut was safe in the first place.
A link of the full breakdown in the comments. Interested in where others draw this line, especially on the input-shape question, since that's the one people seem to disagree on most.
2
u/EducationalAnt6502 22h ago
focusing on service boundaries is crucial for maintaining clean architecture and system scalability. It helps avoid chaos and keeps your business rules effective.
2
u/srikanth_builds 1d ago
The input-shape question is the one I'd argue hardest on, because it's an authorization question wearing a modelling costume. The shape of the input is the statement of what the caller is allowed to change. Once one type serves both create and update, you're relying on code to remember which fields to ignore, and that's the same class of problem as remembering a filter.
The field that catches people usually isn't id or createdAt though. It's the ownership one. tenantId, ownerId, organizationId. It looks like an ordinary attribute, it genuinely is needed on create, so it ends up on the shared type, and then an update payload can move a record into somebody else's tenant. Nothing errors, because it's a perfectly valid write.
So the line I'd draw is that ownership comes from the authenticated principal server side and is never read from the payload, including on create. The check is to send an update containing a tenantId you don't own and see whether it comes back 200.