I remember when reuse was the Holy Grail. First it was object-oriented languages, then object modeling, then components, then services. While none of these has lived up to the promise of rampant reuse, one thing has – language. We have learned to overload and reuse terms to the point that most are no more descriptive than using the word “thing”. “Service” is one of these.
The OASIS Reference Model for Service Oriented Architecture 1.0 defines a service as “…a mechanism to enable access to one or more capabilities, where the access is provided using a prescribed interface and is exercised consistent with constraints and policies as specified by the service description”. This definition includes nothing about protocol, media types, messaging patterns, etc. Essentially, this definition applies to anything that exposes something to consumers over in a defined interface in a defined manner.
Well, that’s helpful. It doesn’t even resolve whether it’s referring to the application providing services or the specific endpoints exposed by that application. In the context of Domain-Driven Design, a service may even be share the same process with its client.
Some services are meant to be interactive. A request is made and a response is returned. Such a service is extremely useful when the response is being presented to an actual human user in response to some action of theirs. This immediate gratification provides an excellent user experience provided latency is minimized. When interactive services are composed within another interactive service, this latency burden quickly increases as the remote client can’t regain control until the service regains it from each remote service it’s called (barring a timeout).
Some services are meant to work asynchronously. These work well particularly well in system to system scenarios when there’s no need to for temporal coupling. A significant trade-off for this model is the additional complexity involved, particularly around error condition feedback.
Some services (typically SOAP-based) are meant to receive and return highly structured and well-defined messages invoking tasks to be performed. This works extremely well for many system to system communications. Others (typically RESTful services) provide more of a variation of media types which are dealt with using CRUD operations. These work particularly well when the client is mainly presenting the response (perhaps with some minimal transformation) to an end user. If the messages are less well-defined, then the level of client complexity increases. Services using well-defined messages that are exposed externally will typically have different versioning requirements than services meant for consumption by internal clients (where “internal” is defined as built and deployed contemporaneously with the service).
The points behind this litany of service definitions and usage patterns? Different styles are appropriate to different situations and precision in communication is important to avoid conflating those styles.
Providing functionality via a service, regardless of the type of service, is not sufficient to meet a need. Functionality should be exposed via the flavor of service that corresponds to the needs of the client. Otherwise, the client may incur more effort and complexity dealing with the mismatch than the functionality is worth.
The concept of a message-oriented API for the back-end allows it to meet all of these needs without violating the DRY principle. Just like UI components, service endpoints (SOAP or REST style) should not contain domain logic, but delegate to domain services. As such, this provides a thin layer primarily concerned with translation between the format and messaging pattern required externally and that required by the domain services. This allows internal consumers (again, where “internal” is defined as built and deployed contemporaneously with the back-end) to work either directly in-process with domain services or remotely via a very thin shim that retains the same message structure and solely provides the network communication capabilities. External consumers should have their own endpoints (perhaps even separate sites) that cater to their needs.
Having the thinnest possible service layer not only prevents duplication of logic but also lessens the burden of standing up new endpoints. Lowering the cost of an endpoint makes it easier to justify providing tailored APIs. APIs that are tailored to specific client types are simpler to consume, further reducing costs.
In my last post, I asked “do you need a service layer?” The answer remains, “it depends”. But now there’s a new wrinkle, “if you need a service layer, does it need to be a remote one?”
One of the perils of popularity (at least in technology) is that some people equate popularity with efficacy. Everyone’s using this everywhere, so it must be great for everything!
Actually, not so much.
No one, to my knowledge, has created the secret sauce that makes everything better with no trade-offs or side effects. Context still rules. Martin Fowler recently published “Microservices and the First Law of Distributed Objects”, which touches on this principle. In it, Fowler refers to what he calls the First Law of Distributed Object Design: “don’t distribute your objects”. To make a long story short, distributed objects are a very bad idea because object interfaces tend to be fine-grained and fine-grained interfaces perform poorly in a distributed environment. “Chunkier” interfaces are more appropriate to distributed scenarios due to the difference in context between in-process and out of process communication (particularly when out of process also crosses machine, even network boundaries). Additionally, distributed architectures introduce complexity, adding another contextual element to be accounted for when evaluating whether to use them.
In his post, under “Further Readings”, Fowler references an older post of his on Dr. Dobb’s titled “Errant Architectures” (a re-packaging of Chapter 7 of his book Patterns of Enterprise Application Architecture). In that, he discusses encountering what was a common architectural anti-pattern of the time (early 2000s):
There’s a recurring presentation I used to see two or three times a year during design reviews. Proudly, the system architect of a new OO system lays out his plan for a new distributed object system—let’s pretend it’s some kind of ordering system. He shows me a design that looks rather like “Architect’s Dream, Developer’s Nightmare” with separate remote objects for customers, orders, products and deliveries. Each one is a separate component that can be placed in a separate processing node.
I ask, “Why do you do this?”
“Performance, of course,” the architect replies, looking at me a little oddly. “We can run each component on a separate box. If one component gets too busy, we add extra boxes for it so we can load-balance our application.” The look is now curious, as if he wonders if I really know anything about real distributed object stuff at all.
The quintessential example of this style was the web site presentation layer with its business and data layer behind a set of web services. Particularly in the .Net world, web services were the cutting edge and everyone needed them. Of course the system architect in the story was confusing scalability for performance (as were most who jumped onto the web service layer bandwagon). In order to increase the former, a hit is taken to the latter. In most cases, this hit is entirely unnecessary. A web application with an in-process back-end can be load-balanced much more simply than separate UI and service sites, without suffering the performance penalty of remote communication.
There are valid use cases for a service layer, such as when an application provides a both an interactive user interface and is service-enabled. When there are multiple user interfaces, some front ends will benefit from physical separation from the back-end (native mobile apps, desktop clients, SharePoint web parts, etc.). Applications can be structured to accommodate the same back-end either in-process or out of process. This structure can be particularly important for applications that have both internal (where internal is defined as built and deployed simultaneously with the back-end) and external service clients as these have different needs in terms of versioning. Wrapping the back-end with a service layer rather than allowing it to invade a particular service implementation can allow one back-end to serve internal and external customers either directly or via services (RESTful and/or SOAP) without violating the DRY principle.
Many of the same caveats that apply to composite applications formed from microservices (network latency, serialization costs, etc.) apply to these types of applications. It makes little sense to take on these downsides in the absence of a clear demonstrable need. Doing so looks more like falling prey to a fad than creating a well thought out design.
A recent post on The Daily WTF highlighted a system that “…throws the fewest errors of any of our code, so it should be very stable”. The punchline, of course, was that the system threw so few errors because it was catching and suppressing almost all the errors that were occurring. Once the “no news is good news” code was removed, the dysfunctional nature of the system was revealed.
See the full post on the Iasa Global Blog (a re-post, originally published here).
A comment from Tom Cagley was the catalyst for my post “Design Communicates the Solution to a Problem”, and he’s done it again. I recently re-posted “Technical Debt – What it is and what to do about it” to the Iasa Global blog, and Tom was kind enough to tweet a link to it. In doing so, he added a little (friendly) barb: “Why not just do the work better?” My reply was “It’s certainly the best strategy, assuming you can“.
Obviously, we have a great deal of control over intentional coding shortcuts. It’s not absolute control; business considerations can intrude, despite our best advice to the contrary. There are a host of other factors that we have less control over: aging platforms, issues in dependencies, changing runtime circumstances (changes in load, usage patterns, etc.), and increased complexity due to organic growth are all factors that can change a “good” design into a “poor” one. As Tom has noted, some have extended the metaphor to refer to these as “Quality Debt, Design Debt, Configuration Management Debt, User Experience Debt, Architectural Debt”, etc., but in essence, they’re all technical deficits affecting user satisfaction.
Unlike coherent architectural design, these other forms of technical debt emerge quite easily. They can emerge singly or in concert. In many cases, they can emerge without your doing anything “wrong”.
Compromise is often a source of technical debt, both in terms of the traditional variety (code shortcuts) and in terms of those I listed above. While it’s easy to say “no compromises”, reality is far different. While I’m no fan of YAGNI, at least the knee-jerk kind, over-engineering is not the answer either. Not every application needs to be built for hundreds of thousands of concurrent users. This is compromise. Having insufficient documentation is a form of technical debt that can come back to haunt you if personnel changes result in loss of knowledge. How many have made a compromise in this respect? While it is a fundamental form of documentation, code is insufficient by itself.
A compromise that I’ve had to make twice in my career is the use of the shared database EAI pattern when an integration had to be in place and the target application did not have a better mechanism in place. While I am absolutely in agreement that this method is sub-standard, the business need was too great (i.e. revenue was at stake). The risk was identified and the product owner was able to make an informed decision with the understanding that the integration would be revised as soon as possible. Under the circumstances, both compromises were the right decision.
In my opinion, taking the widest possible view of technical debt is critical. Identification and management of all forms of technical debt is a key component of sustainable evolution of a product over its lifetime. Maintaining a focus on the product as a whole, rather than projects, provides a long-term focus should help make better compromises – those that are tailored to getting the optimal value out of the product. Having tunnel vision on just the code leaves too much room for surprises.
Making anything unambiguous means finding a way for others to understand which gets us to the knotty problem of how we communicate the method we have taken to create unambiguousness. (probably not even close to being a real word).
Tom Cagley’s comment referred to the last few sentences of the “Hatin’ on Nulls” post:
Coherence and consistency should be considered hallmarks of an API. As Erik Dietrich noted in “Notes on Writing Discoverable Framework Code”, a good API should “make screwing up impossible”. Ambiguity makes screwing up very possible.
Ambiguity and uncertainty are facts of life. Architects must work with less than perfect certainty to resolve ambiguous concerns and design an optimal solution for the problem(s) at hand. An important goal in achieving that optimal design must be to create a design that is comprehensible to the user. The design communicates how to achieve the solution to a problem. In doing so, we must deal with the ambiguity and uncertainty so that our clients don’t have to.
This, in my opinion, is a key defense of the phrase “form follows function”. As Tom Graves’ post “Form follows non-function” pointed out, perception of what is “good” design depends on quality of service attributes (AKA non-functional requirements). The functional attributes are necessary, but far from sufficient.
Interfaces, whether aimed at humans or machines, should be comprehensible (APIs are first consumed by humans before they are consumed by applications). In both cases, a lack of clarity will make them hard to use. It doesn’t matter if the design is technically clever, if functionality is hard to discover, in many cases it may as well be absent. It’s certainly likely to be perceived that way.
If the customer can’t accomplish a desired function, they’re going to feel constrained. Constrained customers tend to be unhappy customers. Unhappy customers tend to convert to ex-customers
As I was reading Roger Sessions’ latest white paper, “The Thirteen Laws of Highly Complex IT Systems”, Laws 1 and 2 immediately caught my eye:
Law 1. There are three categories of complexity: business, architectural and implementation.
Law 2. The three categories of complexity are largely independent of each other.
That complexity in these categories can vary independently (e.g. complex business processes can be designed and implemented simply just as simple processes can be designed and implemented in an extremely complex manner) is important to the understanding of complexity in IT.
See the full post on the Iasa Global Blog (a re-post, originally published here).
When I first read Christian Neumanns’ “Why We Should Love ‘null'”, I found myself agreeing with his position. Yes, null references have “…led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage…” per Sir C. A. R. Hoare. Yes, many people heartily dislike null references and will go to great lengths to work around the problem. Finally, yes, these workarounds may be more detrimental than the problem they are intended to solve. While I agreed with the position that null references are a necessary inconvenience (the ill effects are ultimately the result of failure to check for null, not the null condition itself), I didn’t initially see the issue as being particularly “architectural”.
Further on in the article, however, Christian covered why null references and the various workarounds, become architecturally significant. The concept of null, nothing, is semantically important. A price of zero dollars is not intrinsically the same as a missing price. A date several millenia into the future does not universally convey “unknown” or “to be determined”. Using the null object pattern may eliminate errors due to unchecked references, but it’s far from “safe”. According to Wikipedia, “…a Null Object is very predictable and has no side effects: it does nothing“. That, however, is untrue. A Null Object masks a potential error condition and allows the user to continue on in ignorance. That, in my opinion, is very much doing something.
A person commenting on Christian’s post stated that “…a crash is the worst kind of experience a user can have”. That person argued that masking a null reference error may not be as bad for the user as a crash. There’s a kernel of truth there, but it’s a matter of risk. If an application continues on and the result is a misunderstanding of what’s been done or worse, corrupted data, how bad is that? If the application in question is a game, there’s little real harm. What if the application in question is dealing with health information? I stand by the position that where there is an error, no news is bad news.
As more and more applications become platforms via being service enabled, semantic issues gain importance. Versioning strategies can ensure structural compatibility, but semantic issues can still break clients. Coherence and consistency should be considered hallmarks of an API. As Erik Dietrich noted in “Notes on Writing Discoverable Framework Code”, a good API should “make screwing up impossible”. Ambiguity makes screwing up very possible.