Friday, January 29, 2016

The D in SOLID or how thought we refactored to FE and BE servers in a single day

Warning

Although this post does touch on the SOLID principles and on dependency inversion in particular, it is in fact not really a post on software design and best practices, but rather a lessons learned kind of post. 

My goal here is to teach you that our job as a software consultant, or even as a software developer in general, is more than just knowing how to program. I'm not talking about general people's skills or even basic project management skills. These are no doubt traits that are good to have as a software developer, I even believe that understanding or having these skills will make you a better software developer in general. But ultimately, you could still hand them over to a non-technical person. Like I said: these are undoubtedly great skills for anyone, but technically, they are not required for a software developer.

So what am I talking about then? Well, sometimes we need to provide information to others, usually our PM or a client, and that information is 100% non-technical. It has, in essence, nothing to do with software, but still: we are the only ones that can supply this information accurately! 
This type of information is almost never asked for explicitly and to make matters worse: this is usually the type of information that, if you fail to provide it, may turn an otherwise healty project into a budgeting- and planning hell!

"So why doesn't anyone explicitly ask for that information then" I hear you think.
Well: because they don't know that they have to ask this 
It is our job as a developer to detect missing information in our PM or client's head and to fill that void. Seems hard? That's because it is hard but nevertheless it is an extremely important skill for every developer to have, especially if you're in the consulting business.

The main topic of this post is actually an example of where we failed to deliver this information.
Sit back, relax and learn (and laugh... a little... go ahead, I can take it)

The Problem At Hand

We had started out with the basics for a new project. You know: setting up the environment, creating the solution, configuring mvc, you know, the works. We decided to go with a standard three layer architecture.

  • Application layer was just the UI
  • Business Logic Layer contained our services, which implemented the business logic
  • Data Access Layer contained our repositories to handle database access.
After a couple of sprints, the client thought it would be a good idea to let us know that they're not really fond of single-tier applications. They insisted that we changed the current deployment architecture to use front-end and back-end servers. They wanted us to use publicly accessible front-end server which contained only UI logic. All business logic and data access and everything had to reside on a different back-end server. That back-end server had to be completely shielded from outside access. Only the front-end server was allowed to access the back-end server.

The Proposed Solution

The original design

Luckily, we had been smart from the start. We had made interfaces for each and every logic-class.
So the controller depended only on interfaces, the services depended only on interfaces, and so on
This is what the simplified design looked like.



This is of course just an example of one of the services: managing installations. As you can see, the application layer is an mvc controller: InstallationController which depends on an interface: IInstallationService. This is our business layer, implemented in the DefaultInstallationService, which once again depends on an interface for it's data access: IInstallationRepository. It's implementation, DbInstallationRepository, was then the concrete implementation of the data layer.

The reworked design

Here's how we designed the solution: We created a new project for the back-end, with the same architecture as the original project but, without the application layer. So there were no more mvc controllers and we had the services derive from ApiController. So our services, the business layer, would be accessible via WebApi. The Data layer stayed the same.


Next, we stripped everything from the original project, except the application layer.
We then made new implementations of all service interfaces. This time they were empty boxes only capable of calling a remote service over WebApi.


All we had to do was deploy both back-end and front-end, connect them up and TADAAAAA done.
Cool right? This is truly dependency inversion in action!

The estimates

We now had our design ready so the next question was: "How long will it take to make this change?" To which we happily replied: one day.... (three developers, so technically: three days)
Let that sink in for a while.

At this point I encourage you to take a look at the designs above again and read the text again.
Do you think you could do this all by yourself in three days? Be honest!
We believed we could. You know what? I still believe I could! really!

Where we went wrong

So where did we go wrong? Well, we kind of forgot to look ahead, because the change actually doesn't stop at implementing our new design. It also has huge consequences for each and every decision for the rest of the project, as well as for all estimates that were already done in the past. Why? Well consider these everyday tasks:

Calling a function in the business layer

In a regular application we just call the function, done. But now there's a lot more to it, because now your function call becomes a network call! it will take longer, so you should think about if you really want or need to do it. Also, it can fail: you'll have to handle that too. Next up is versioning, of both your domain objects and your methods, otherwise you'll need to take your entire system down until both front-end and back-end are updated.
You see, performing a simple function call is now suddenly subject to a lot of thinking and possibly concern.

Lazy loading of one-to-many relations

In a regular application, you just decide whether you want to postpone the loading of related objects until usage or not. Now suddenly, your lazy-loading is multi layered! you might want to lazily load relations in your back-end, but what about the front end? Will you also lazy-load relations? If so, you'll need to provide a separate function for it!

Caching

What will you cache and where? You'll have to think about lifetime on multiple levels. Maybe you'll even need to invalidate the back-end cache from the front-end, but then again, maybe not. Point is: you'll have to spend time thinking about these kind of things.

Read-modify-write in a single transaction

Distributed transactions over webservice calls and database access, that ought to be fun.

Conclusion

So there it is. This whole post serves, not only to be a source of information from the trenches,
but also as a warning. The SOLID principles are great to design and write good software 
and I would evangelize them avidly every day, but they are just a base, just the foundation of good software. If you use them every day, you will write better software and your software will be better prepared for crazy changes or other obstacles that may arise. But don't you thing for a minute that they are all you need to save your ass. Don't think that, because the design you have allows you to easily handle a huge change, just like ours here, that you're out of the woods. Being able to easily adapt is just the beginning, then comes the real hard work of bearing the consequences of your change throughout the rest of your implementation. Don't forget that, like we did, when you do an estimation for a design change, don't forget to think about how this design change will impact the rest of the project. Sometimes the best decision is not to do the change even if your design allows it. Luckily for us, a more experienced colleague had spotted the flaws in our estimates and made us aware of the consequences it would have on the rest of the project.