DEV Community

Discussion on: Architectural decisions that you regret...

Collapse
 
kspeakman profile image
Kasey Speakman • Edited

These are the largest 2 regrets I've had in making architecture over the years.

Regret: making "magic" architecture

For instance, opting into being part of the architecture by implementing an interface or abstract class.

// the infrastructure runs some reflection code on startup
// to find all IHandleCommands classes and make sure
// messages are delivered to Handle methods inside them
public class SomeHandler : IHandleCommands
{
    public void Handle(SomeCommand command)
    {
        ...
    }
}

The reflection code is not shown so your eyes don't glaze over.

This is nice and clever, because you don't have to write wiring code. But especially for developers it can be really hard to accept "Don't worry about how it works. Just put this interface on it and it will work." Magic. It is great as long as you don't run into any special cases which break it.

This is the same reason I avoid attributes (aka annotations) where possible. I'll use them for some compiler optimizations or literally for extra information like [Obsolete], but I avoid using them to control logic. Logic around these is not directly called from the code it adorns, so it's hard to track down when things go wrong. And it's not obvious how things work. Recently I looked around for a while on ASP.NET Core's source code for the exact code that is run by the [Authorize] attribute. I never found it. I found code that I suspect is called, but I can't prove it because I was not able to trace a direct call chain into that code.

Regret: making opinionated abstractions required

Changes to architecture are very costly since arch is typically used by a lot of different feature code. Any required architectural abstractions should be as course-grained as possible.

I've made the mistake of thinking that I'm going to make it super easy to just plug in new feature code and my arch framework will handle all the infrastructure details. Usually this involves requiring feature code to take on my arch abstractions. That works fine until next month when the customer requests a different kind of feature like exporting to CSV. Where the feature needs to handle some of the infrastructure itself, like writing directly to the response stream. Otherwise it can run out of memory reading a large data set. So I have to backup and rethink my whole architectural abstraction. And probably change every place where it was already used. And that's just the beginning of the fights with the required abstraction. You'll have to keep going back and refactoring to add handling for all the various cases you run into.

Instead, it's best to keep the architecture as course-grained and simple as possible. In most web frameworks you are given the Request and Response objects (although unfortunately most of the time they are just DTOs with getters and setters), sometimes packaged together in a Context object. This is a good example of a course-grained interface. Let feature code handle what they want at nearly this level. Then if there are common cases which use the same steps (e.g. a Load-Edit-Save workflow), make a helper abstraction to simplify that. Then feature code can choose to opt into the helper if it fits what they are doing or handle everything themselves. That way it should be really rare to need to change the architecture code in a way that breaks feature code. But you still have the opportunity to write very little code for really common features by using helpers.

HTH

Collapse
 
jvanbruegge profile image
Jan van Brügge

I absolutely agree with "magic". Thats the main reason why I'm not using Cycle.js instead of Angular for my projects. Everything is explicit and traceable.