A more common scenario I see is that people start with a monolith that ends up inheriting all the conceptual debt that accumulates as a project evolves. All this debt builds up a great desire for change in the maintaining team.
A champion will rise with a clean architecture and design in microservice form that addresses all high visibility pain points, attributing forecasted benefits to the perceived strengths of microservices. The team buys into the pitch and looks forwards to a happily-ever-after ending.
The reality though is that the team now has multiple problems, which include:
- Addressing conceptual debt that hasn't gone away.
- Discovering and migrating what the legacy system got right, which is often not documented and not obvious.
- Dealing with the overheads of microservices that were not advertised and not prominent at a proof-of-concept scale.
- Ensuring business continuity while this piece of work goes on.
I would propose alternative is to fix your monolith first. If the team can't rewrite their ball of mud as a new monolith, then what are the chances of successfully rewriting and changing architecture?
Once there is a good, well functioning monolith, shift a subset of responsibility that can be delegated to a dedicated team - the key point is to respect Conway's law - and either create a microservice from it or build a new independent monolith service, which aligns more to service oriented architecture than microservices.
When the same practices used to sell microservices get applied to modules, packages and libraries, there is no need to put a network in the middle to do the linker's job.
But too many are eager to jump into distributed computing without understanding what they are bring into their development workflow and debugging scenarios.
> there is no need to put a network in the middle to do the linker's job.
Absolutely. More people need to understand the binding hierarchy:
- "early binding": function A calls function B. A specific implementation is selected at compile+link time.
- "late binding": function A calls function B. The available implementations are linked in at build time, but the specific implementation is selected at runtime.
From this point on, code can select implementations that did not even exist at the time function A was written:
- early binding + dynamic linking: a specific function name is selected at compile+link time, and the runtime linker picks an implementation in a fairly deterministic manner
- late binding + dynamic linking: an implementation is selected at runtime in an extremely flexible way, but still within the same process
- (D)COM/CORBA: an implementation of an object is found .. somewhere. This may be in a different thread, process, or system. The system provides transparent marshalling.
- microservices: a function call involves marshalling an HTTP request to a piece of software potentially written by a different team and hosted in a datacenter somewhere on a different software lifecycle.
At each stage, your ability to predict and control what happens at the time of making a function call goes down. Beyond in-process functions, your ability to get proper backtraces and breakpoint code is impaired.
I think a trap that many software engineers fall into is interpreting "loose coupling" as "build things as far down the binding hierarchy as possible".
You see this under various guises in the web world - "event-driven", "microservices", "dependency injection", "module pattern". It's a very easy thing to see the appeal of, and seems to check a lot of "good architecture" boxes. There are a lot of upsides too - scaling, encapsulation, testability, modular updates.
Unfortunately, it also incurs a very high and non-obvious cost - that it's much more difficult to properly trace events through the system. Reasoning through any of these decoupled patterns frequently takes specialized constructs - additional debugging views, logging, or special instances with known state.
It is for this reason that I argue that lower-hierarchy bindings should be viewed with skepticism - if you _cannot_ manage to solve a problem with tight coupling, then resort to a looser coupling. Introduce a loose coupling when there is measurable downside to maintaining a tighter coupling. Even then, choose the next step down the heirarchy (i.e. a new file, class, or module rather than a new service or pubsub system).
Here, as everywhere, it is a tradeoff about how understandable versus flexible you build a system. I think it is very easy to lean towards flexibility to the detriment of progress.
This isn't really true though. It's not like you're suddenly adding consensus problems or something, you don't need to reinvent RAFT every time you add a new service.
Microservices put function calls behind a network call. This adds uncertainty to the call - but you could argue this is a good thing.
In the actor model, as implemented in Erlang, actors are implemented almost as isolated processes over a network. You can't accidentally share memory, you can't bind state through a function call - you have to send a message, and await a response.
And yet this model has led to extremely reliable systems, despite being extremely similar to service oriented architecture in many ways.
Why? Because putting things behind a network can, counter intuitively, lead to more resilient systems.
It's still a distributed problem if it's going over a network, even if you don't need consensus specifically.
I don't think you would get the same benefits as Erlang unless you either actually write in Erlang or replicate the whole fault tolerant culture and ecosystem that Erlang has created to deal with the fact that it is designed around unreliable networks. And while I haven't worked with multi-node BEAM, I bet single-node is still more reliable than multi-node. Removing a source of errors is still less errors.
If your argument is that we should in fact run everything on BEAM or equivalently powerful platforms, I'm all on board. My current project is on Elixir/Phoenix.
> replicate the whole fault tolerant culture and ecosystem
I think the idea is to move the general SaaS industry from the local monolith optimum to the better global distributed optimum that Erlang currently inhabits. Or rather, beyond the Erlang optimum insofar as we want the benefits of the Erlang operation model without restricting ourselves to the Erlang developer/package ecosystem. So yeah, the broader "micro service" culture hasn't yet caught up to Erlang because industry-wide culture changes don't happen over night, especially considering the constraints involved (compatibility with existing software ecosystems). This doesn't mean that the current state of the art of microservices is right for every application or even most applications, but it doesn't mean that they're fundamentally unworkable either.
At the language level, I think you probably need at least the lightweight, crashable processes. I don't think you can just casually just bring the rest of the industry forward to that standard without changing the language.
> putting things behind a network can, counter intuitively, lead to more resilient systems
Erlang without a network and distribution is going to be more resilient than Erlang with a network.
If you're talking about the challenges of distributed computing impacting the design of Erlang, then I agree. Erlang has a wonderful design for certain use cases. I'm not sure Erlang can replace all uses of microservices, however, because from what I understand and recall, Erlang is a fully connected network. The communication overhead of Erlang will be much greater than that of a microservice architecture that has a more deliberate design.
Playing a bit of devil's advocate, erlang is more reliable with more nodes by its design because it is expecting hardware to fail, and when that hardware does fail it migrates the functionality that was running on it to another node. With only a single node then you have a single point of failure, which is the antithesis of erlangs design.
> Erlang without a network and distribution is going to be more resilient than Erlang with a network.
I doubt that this is supposed to be true. Erlang is based on the idea of isolated processes and transactions - it's fundamental to Armstrong's thesis, which means being on a network shouldn't change how your code is built or designed.
Maybe it ends up being true, because you add more actual failures (but not more failure cases). That's fine. In Erlang that's the case. I wouldn't call that resiliency though, the resiliency is the same - uptime, sure, could be lower.
What about in other languages that don't model systems this way? Where mutable state can be shared? Where exceptions can crop up, and you don't know how to roll back state because it's been mutated?
In a system where you have a network boundary, and if you follow Microservice architecture you're given patterns to deal with many others.
It's not a silver bullet. Just splitting code across a network boundary won't magically make it better. But isolating state is a powerful way to improve resiliency if you leverage it properly (, which is what Microservice architecture intends).
You could also use immutable values and all sorts of other things to help get that isolation of state. There's lots of ways to write resilient software.
Distributed systems is more than just consensus protocols.
At minimum, you need to start dealing with things like service discovery and accounting for all of the edge cases where one part of your system is up while another part is down, or how to deal with all of the transitional states without losing work.
> Why? Because putting things behind a network can, counter intuitively, lead to more resilient systems
If you're creating resiliency to a set of problems that you've created by going to a distributed system, it's not necessarily a net win.
> At minimum, you need to start dealing with things like service discovery and accounting for all of the edge cases where one part of your system is up while another part is down, or how to deal with all of the transitional states without losing work.
I really don't get this phobia. You already have to deal with that everywhere, don't you? I mean, you run a database. You run a web app calling your backend. You run mobile clients calling your backend. You call with services. The distributed system is more often than not already there. Why are we fooling ourselves into believing that just because you choose to bundle everything in a mega-executable that you're not running a distributed system?
If anything,explicitly acknowledging that you already run a distributed system frames the problem ina way that you are forced to face failure modes you opt to ignore.
> You can't accidentally share memory, you can't bind state through a function call - you have to send a message, and await a response.
These are not strictly a "program on network vs program not on network" issue. "Accidentally sharing memory" can be solved by language design. It is correct that a system that is supposedly designed as inter-actor communication is not ideal when designed and written in a "function call-like" manner, but erlang only partially solves this phenomena by enforcing a type of architecture, which locks away the mentioned bad practice.
> ...despite being extremely similar to service oriented architecture in many ways. Why? Because putting things behind a network can, counter intuitively, lead to more resilient systems.
This is a strange logic. Resilient system is usually achieved by putting a service/module/functionality/whatever in a network of replications, meanwhile service oriented architecture talks about loosening the coupling between different computers that acts differently.
I do agree on microservice != turn function calls into distributed computing problems.
It MAY happen to systems eagerly designed with microservice architecture without a proper prior architectural validations, but it is not always the case.
You're probably right that Armstrong would be too humble to say it, but increased engineering effort is the only conceivable mechanism for unreliable networks to produce more reliable systems. I can't even tell what you think is going on.
In his initial thesis on Erlang, and in any talk I've seen him give, or anything I've seen him write, he attributes reliability to the model, not to BEAM.
You didn’t continue far enough down the hierarchy to find the places where microservices actually work. Honestly if you’re just calling a remote endpoint whose URL is embedded in your codebase and with the hard coded assumption that it will follow a certain contract you’re not moving down the binding hierarchy at all - you ‘re somewhere around ‘dynamic library loading’ in terms of indirection.
It gets much more interesting when you don’t call functions at all - you post messages. You have no idea which systems are going to handle them or where... and at that point, microservices are freeing.
All this focus on function call binding just seems so... small, compared to what distributed microservice architectures are actually for.
If you think of microservices as a modularization tool - a way to put reusable code in one place and call it from other places - then you are missing the point. Microservices don’t help solve DRY code problems.
Monoliths aren’t merely monolithic in terms of having a monolithic set of addressable functionality; they are also monolithic in terms of how they access resources, how they take dependencies, how they are built, tested and deployed, how they employ scarce hardware, and how they crash.
Microservices help solve problems that linkers fundamentally struggle with. Things like different parts of code wanting to use different versions of a dependency. Things like different parts of the codebase wanting to use different linking strategies.
Adding in network hops is a cost, true. But monoliths have costs too: resource contention; build and deploy times; version locking
Also, not all monolithic architectures are equal. If you’re talking about a monolithic web app with a big RDBMS behind it, that is likely going to have very different problems than a monolithic job-processing app with a big queue-based backend.
Adding to the theme of microservices overhead, I feel like the amount of almost-boilerplate code used to get and set the contents of serialized data in the service interfaces exceeds the code used to actually do useful work even in medium sized services, much less microservices.
It is possible that there are architectures other than ‘monolith’ and ‘microservices’. Component architectures are also a thing. If it doesn’t all deploy in one go, I don’t think it’s a monolith.
It is how a large majority of microservices gets implemented in practice.
What was originally a package, gets its own process and REST endpoint, sorry nowadays it should be gRPC, the network boilerplate gets wrapped in nice function calls, and gets used everywhere just like the original monolith code.
Just like almost no one does REST as it was originality intended, most microservices end up reflecting the monolith with an additional layer of unwanted complexity.
Okay, but if you’re allowed to submit ‘well structured modular componentized librarified monolith’ as an example proving how monoliths aren’t all bad, I’m not going to let you insist on holding up ‘cargo cult grpc mess’ as proof that microservices are terrible. Let’s compare the best both structures have to offer, or consider the worst both enable - but not just compare caricatures.
On the contrary, as the sales speech at conferences is that the mess only happens with monoliths and the only possible salvation is to rewrite the world as microservices.
Good programming practices to refactor monoliths never get touched upon, as otherwise the sale would lose its appeal.
An interview for me turned from going well to disaster because of a single mention I made that I was not a fan of seeing every single project with the microservice lens. That kind of rustled the interviewees who strongly believed that anything not written as microservice today is not worth building.
I thought that was a pretty inoffensive and practical statement I could support with points but who knew that would derail the entire interview.
It is much harder to enforce the discipline of those practices across modules boundaries than around network boundaries. So in theory yes you could have a well modularized monolith. In practice it is seldom the case.
The other advantage of the network boundary is that you can use different languages / technologies for each of your modules / services.
Don't get me wrong, sometimes it's worth it (I particularly like Spark's facilities for distributed statistical modelling), but I really don't get (and have never gotten) why you would want to inflict that pain upon yourself if you don't have to.
I’ve been developing for more years than dime if you have lived, and the best thing I’ve heard in years was that Google interviews were requiring developers to understand the overhead of requests.
In addition, they should require understanding of design complexity of asynchronous queues, needing and suffering from management overhead of dead letter, scaling by sharding queues if it makes more sense vs decentralizing and having to have non-transactionality unless it’s absolutely needed.
But not just Google- everyone. Thanks, Mr. Fowler for bringing this into the open.
Indeed! The "Latency Numbers Every Programmer Should Know" page from Peter Norvig builds a helpful intuition from a performance perspective but of course there's a lot larger cost in terms of complexity as well.
I mean, you can always deploy your microsevices on the same host, it would just be a service mesh.
Adding network is not a limitation. And frankly, I don't understand why you say things like understanding network. Like reliability is taken care of, routing is taken care of. The remaining problems of unboundedness and causal ordering are taken care of (by various frameworks and protocols).
For dlq management, you can simply use a persistent dead letter queue. I mean it's a good thing to have dlq because failures will always happen. About which order to procese queue etc. These are trivial questions.
You say things as if you have been doing software development for ages, but you're missing out on some very simple things.
Sounds like you're saying "Don't do distributed work" if possible (considering tradeoffs of course, but I guess people just don't even consider this option is your contention).
And secondly, if you do end up with q distributed systems, remember how many independently failing components there are because thag directly translates to complexity.
On both these counts I agree. Microservices is no silver bullet. Network partitions and failure happen almost every day where I work. But most people are not dealing with that level of problems, partly because of cloud providers.
Same kind of problems will be found on a single machine also. Like you'd need some sort of write ahead log, checkpointing, maybe optimize your kernel for faster boot up, heap size and gc rate.
All of these problems do happen, but most people don't need to think about it.
I'm not reading this as "Don't do distributed work". It's "distributed systems have nontrivial hidden costs". Sure, monoliths are often synonymous with single points of failure. In theory, distributed systems are built to mitigate this. But unfortunately, in reality, distributed systems often introduce many additional single points of failure, because building resilient systems takes extra effort, effort that oftentimes is a secondary priority to "just ship it".
Indeed. So with monolith usually we already have 3-4 (or more) somewhat reliable systems, and one non-reliable system which is your monolithic app. Why add other non reliable systems if you don't really need it?
Making a system to be reliable is really really hard and take many resources, which seldom companies pursuit.
I realized this one day when I was drawing some nice sequence diagrams and presenting it to a senior and he said "But who's ensuring the sequence?". You'll never ask this question in a single threaded system.
Having said that, these things are unavoidable. The expectations from a system are too great to not have distributed systems in picture.
Monoliths are so hard to deploy. It's even more problematic when you have code optimized for both sync cpu intensive stuff and async io in the same service. Figuring out the optimal fleet size is also harder.
I'd love to hear some ways to address this issue and also not to have microservice bloat.
What you tend to see, is the difficulty of crossing these network boundaries (which happens anyway) _and_ all the coupling and complexity of a distributed ball of mud.
Getting engineers who don't intuitively understand or maybe even care how to avoid coupling in monoliths to work on a distributed application can result in all the same class of problems plus chains of network calls and all the extra overhead you should be avoiding.
It seems like you tell people to respect the boundaries, and if that fails you can make the wall difficult to climb. The group of people that respect the boundaries whether virtual or not, will continue to respect the boundaries. The others will spend huge amounts of effort and energy getting really good at finding gaps in the wall and/or really good at climbing.
If you're using microservice to enforce stronger interface boundaries what you're really relying on are separate git repos with separate access control to make it difficult to refactor across codebases. A much simpler way to achieve that same benefit is to create libraries developed in separate repos.
Hum... People have been doing it since the 60's, when it became usual to mail tapes around.
If you take a look at dependency management at open source software, you'll see a mostly unified procedure, that scales to an "entirety of mankind" sized team without working too badly for single developers, so it can handle your team size too.
That problem that the "microservices bring better architectures" people are trying to solve isn't open by any measure. It was patently solved, decades ago, with stuff that work much better than microservices, in a way that is known to a large chunk of the developers and openly published all over the internet for anybody that wants to read about.
Microservices still have their use. It's just that "it makes people write better code" isn't true.
> So in theory yes you could have a well modularized monolith.
I've often wondered if this is a pattern sitting underneath our noses. I.e., Starting with a monolith with strong boundaries, and giving architects/developers a way to more gracefully break apart the monolith. Today it feels very manual, but it doesn't need to be.
What if we had frameworks that more gracefully scaled from monoliths to distributed systems? If we baked something like GRPC into the system from the beginning, we could more gracefully break the monolith apart. And the "seams" would be more apparent inside the monolith because the GRPC-style calls would be explicit.
(Please don't get too hung up on GRPC, I'm thinking it could be any number of methods; it's more about the pattern than the tooling).
The advantages to this style would be:
* Seeing the explicit boundaries, or potential boundaries, sooner.
* Faster refactoring: it's MUCH easier to refactor a monolith than refactor a distributed architecture.
* Simulating network overhead. For production, the intra-boundary calls would just feel like function calls, but in a develop or testing environment, could you simulate network conditions: lag, failures, etc.
Aren't you just describing traditional RPC calls? Many tools for this: DBus on Linux, Microsoft RCP on Windows, and more that I'm not aware of.
If you've only got a basic IPC system (say, Unix domain sockets), then you could stream a standard seriaization format across them (MessagePack, Protobuf, etc.).
To your idea of gracefully moving to network-distributed system: If nothing else, couldn't you just actually start with gRPC and connect to localhost?
When you start with gRPC and connect to localhost, usually the worst that can happen with a RPC call is that the process crashes, and your RPC call eventually times out.
But other than that everything else seems to work as a local function call.
Now when you move the server into another computer, maybe it didn't crash, it was just a network hiccup and now you are getting a message back that the calling process is no longer waiting, or you do two asynchronous calls, but due to the network latency and packet distribution, they get processed out of order.
Or eventually one server is not enough for the load, and you decide to add another one, so you get some kind of load mechanism in place, but also need to take care for unprocessed messages that one of the nodes took responsibility over, and so forth.
There is a reason why there are so many CS books and papers on distributed systems.
Using them as mitigation for teams that don't understand how to write modular code, only escalates the problem, you move from spaghetti calls in process, to spaghetti RPC calls and having to handle network failures in the process.
That is the selling theme of DCOM and MTS, CORBA and many other IPC mechanisms, until there is a network failure of some sort that the runtime alone cannot mitigate.
Well, we often use plain old HTTP for this purpose, hence the plethora of 3rd party API's one can make an HTTP call to...
(I side with the monolith, FWIW...I love Carl Hewitt's work and all, it just brings in a whole set of stuff a single actor doesn't need... I loved the comment on binding and RPC above, also the one in which an RPC call's failure modes were compared to the (smaller profile) method call's)
Sure but the converses of these statements are advantages for monoliths.
Most languages provide a way to separate the declaration of an interface from its implementation. And a common language makes it much easier to ensure that changes that break that interface are caught at compile time.
Not at all. That’s what libraries are for. Most applications already use lots of libraries to get anything done. Just use the same idea for your own organisations code.
I've been trying to get buy-in from colleagues to have stricter boundaries between modules but without much success, mainly because I don't fully understand how to do it myself.
Let's say we have 3 different modules, all domains: sales, work, and materials. A customer places an order, someone on the factory floor needs to process it, and they need materials to do it. Materials know what work they are for, and work knows what order it's for (there's probably a better way to do this. This is just an example).
On the frontend, users want to see all the materials for a specific order. You could have a single query in the materials module that joins tables across domains. Is that ok? I guess in this instance the materials module wouldn't be importing from other modules. It does have to know about sales though.
Here's another contrived example. We have a certain material and want to know all the orders that used this material. Since we want orders, it makes sense to me to add this in the sales module. Again, you can perform joins to get the answer, and again this doesn't necessarily involve importing from other modules. Conceptually, though, it just doesn't feel right.
It is a bit hard to just explain in a bunch of comments.
In your examples you need to add extra layers, just like you would do with the microservices.
There would be the DTOs that represent the actual data that gets across the models, the view models that package the data together as it makes sense for the views, the repository module that actually abstracts if the data is accessed via SQL, ORM, RPC or whatever.
You should look into something like:
"Domain-Driven Design: Tackling Complexity in the Heart of Software"
You need a thin layer on top that coordinates your many singular domains. We use graphql to do this. API Gateway or backend for frontend are similar concepts. Having many simple domains without any dependencies is great, they just need to be combined by a simple with many dependencies - a coordination layer.
Joining on the database layer is still adding a dependency between domains. The data models still need to come out of one domain. Dependencies add complexity. So joining is just like importing a module, but worse because it's hidden from the application.
If you really need a join or transaction, you need to think as if you had microservices. You'd need to denormalize data from one domain into another. Then the receiving domain can do whatever it wants.
Of course, you can always break these boundaries and add dependencies. But you end up with the complexity that comes with in, in the long run.
It seems like maybe your onion could use an additional layer or two.
If I understand your example, the usual solution is to separate your business objects from your business logic, and add a data access layer between them.
In terms of dependencies, you would want the data access layer module to depend on the business object modules. And your business logic modules would depend on both the data access layer and business object modules. You may find that it is ok to group business objects from multiple domains into a single module.
Note that this somewhat mirrors the structure you might expect to see in a microservices architecture.
More generous take on the meta: people do understand (some of) the problems involved but are less worried about debugging scenarios than resume padding. The most useful property of microservices is architecturing CVs.
Correct. What's best for the long term health of the business is not taken into consideration. The Board of Directors and the CEO only care about this quarter and this year, why would the foot soldiers take a long view?
As an engineer, the thought process goes:
I can use the same old tried and true patterns that will just get the job done. That would be safe and comfortable, but it won't add anything to my skillset/resume.
Or we could try out this sexy new tech that the internet is buzzing about, it will make my job more interesting, and better position me to move onto my next job. Or at least give me more options.
It's essentially the principal-agent problem. And by the way, I don't blame developers for taking this position.
I feel there is also a chicken-egg problem. Is IT hype driven because of RDD or vice versa? I also do not blame any party involved for acting like they do.
Yep. I am working on a Mongo system, started right about when Mongo was peak hype. This application should be using a relational database, but no resume padding for the original developer, and a ton of tech debt and a crappy database for me to work with.
How do you individually deploy and operate modules, packages, and libraries? The purported benefits of micro services have always been harmonizing with Conway's law. In particular, one of the nice benefits of microservices is that it's harder to "cheat" on the architecture because it likely involves changing things in multiple repos and getting signoff from another team (because again, the code organization resembles the org chart). The high-level architecture is more rigid with all of the pros and cons entailed.
I suppose if you work in an organization where everyone (including management) is very disciplined and respects the high level architecture then this isn't much of a benefit, but I've never had the pleasure of working in such an organization.
That said, I hear all the time about people making a mess with micro services, so I'm sure there's another side, and I haven't managed to figure out yet why these other experiences don't match my own. I've mostly seen micro service architecture as an improvement to my experiences with monoliths (in particular, it seems like it's really hard to do many small, frequent releases with monoliths because the releases inevitably involving collaborating across every team). Maybe I've just never been part of an organization that has done monoliths well.
> But too many are eager to jump into distributed computing without understanding what they are bring into their development workflow and debugging scenarios.
This fallacy is the distributed computing bogeyman.
Just because you peel off a responsibility out of a monolith that does not mean you suddenly have a complex mess. This is a false premise. Think about it: one of the first things to be peeled off a monolith are expensive fire-and-forget background tasks, which more often than not are already idempotent.
Once these expensive background tasks are peeled off, you gets far more manageable and sane system which is far easier to reason about and develop and maintain and run.
Hell, one of the basic principles of microservices is that you should peel off responsibilities that are totally isolated and independent. Why should you be more concerned about the possibility of 10% of your system being down if the alternative is having 100% of your system down? More often than not you don't even bat an eye if you get your frontend calling your backend and half a dozen third-party services. Why should it bother you if your front-end calls two of your own services instead of just one?
I get the concerns about microservicea, but this irrational monolith-mania has no rational basis either.
I'm really thinking this now, I'm responsible for design a reimplantation of a system and for some reason it's taken as a given it will be broken up into services on separate containers, which means network calls which aren't reliable. Which then brings a host of other problems to solve and a load of admin tasks. I'm really thinking I should re design as s monolith. It's a small system anyway.
When I started programming, linking was the only way of packaging software in mainstream computing, if it was so superior we wouldn't have moved away from it.
Microservices architectures are like the Navy using nuclear reactors to power aircraft carriers. It’s really compelling in certain circumstances. But it comes with staggering and hard to understand costs that only make sense at certain scales.
There is really something to be said about using C as a teaching language. When you start with C, the entire process is laid bare. And fully agree with earlier observation that microservices are delegating the 'link' phase to a distributed operational/runtime regime. Just being able to see that (conceptually) would steer away a mature team from adopting this approach unless absolutely necessary.
One of the patterns that I have noted in the past 3 decades in the field is that operational complexity is far more accessible than conceptual complexity to the practitioners. Microservices shift complexity from conceptual to operational. With some rare exceptions, most MS designs I've seen were proposed by teams that were incapable of effective conceptual modeling of their domain.
I'm not sure why you are being down voted. In context of calling business logic we have moved away from linking.
I guess the context is only implelied in your parent post.
The rise of the network API in the last 20 years has proven it's own benefits. Whether you are calling a monolith of a microservice, it's easier to upgrade the logic without recompiling and re-linking all dependencies.
I think there's too much being conflated to make such a statement.
For example, microservices tend to communicate via strings of bytes (e.g. containing HTTP requests with JSON payloads, or whatever). We could do a similar thing with `void` in C, or `byte[]` in Java, etc.
Languages which support 'separate compilation' only need to recompile modules which have changed; if all of our modules communicate using `void` then only the module containing our change will be recompiled.
It's also easy to share a `void` between modules written in different languages, e.g. using a memory-mapped file, a foreign function interface, etc.
It's also relatively* easy to hook such systems into a network; it introduces headaches regarding disconnection, packet loss, etc. but those are all standard problems with anything networked. The actual logic would work as-is (since all of the required parsing, validation, serialisation, etc. is already there, for shoehorning our data in and out of `void*`).
If these benefits were so clear, I would expect to see compiled applications moving in this direction. Yet instead I see the opposite: stronger, more elaborate contracts between modules (e.g. generic/parametric types, algebraic data types, recursive types, higher-kinded types, existential types, borrow checkers, linear types, dependent types, etc.)
There is one approach Fowler suggested is SacrificialArchitecture. You build your monolith quickly, get to market fit and once you understand service boundaries you move to microservices.
Personally I would like to try Umbrella Projects[1]. You can design it as microservices but deploy and build as monolith. Overhead is lower, and it is easier to figure out right services when in one codebase. It can be easy implemented in other lang/frameworks as well.
This! Service boundaries are vital and almost intractable to design up front as you won’t be sure of your systems’ use cases. I’ve worked on numerous micro services systems and all of them were designed in such a way that data loss and unknown error states were mandatory. Micro services seem simpler, but are actually harder than my methodology “as few services as necessary” + bounded contexts within them. Using network partitions to stop spaghetti code just leads to spaghetti network partitions. The discipline to keep this simple and self contained is the important part.
Unfortunately in actual practise I've encountered significant cargo culting about DDD. Attracts a lot of people with mid level experience who suddenly want to dictate architecture based on theoretical ideals rather than practicalities.
There's no substitute for experience, and specifics of adapting architecture to the context of the problem you're trying to solve.
In one case I wanted to use a technology that actually matches DDD very significantly - but the cargo cultish closedmindedness of the practitioners meant they couldn't even understand how an old idea/tech they hadn't liked or approved was, was actually an implementation of DDD.
The problem there is not DDD, the problem is the people who get closed minded and stuck to their one true way (often without really broad experience to make that judgement call effectively). I've learned that pattern of language of absolutes such as 'should be, mandatory, all' do XXX is often a sign of that kind of cargo cultish thinking.
That's very true, and a problem I've also seen. There are an army of mediocre people who've gone full Cargo Cult with it. Usually comes with a big dose of Uncle Bob: The Bad Parts.
We have exactly the same construct in .Net: a solution file which can contain multiple projects. All of the code is in one place, so you're working with a monolith^. Services are separate and dependencies are checked by the compiler/toolchain - so you design as microservices. Finally deployment is up to you - you can deploy as Microservices, or as one big Monolith.
^ Compile times are fast so this isn't an issue like it can be in other languages.
I like the idea of SacrificialArchitecture. The big downside is to really communicate to management/other departments that it is meant to be a kind of prototype. If it looks good enough and gets paying customers it is hard to find the time to stop, take a step back and re-write
That's what my company has done. We have a microservice-like architecture, but discourage dependencies on other services. If we need to use one service in another, we use dependency injection (which allows us to switch out the communication protocol later if need be) or talk over a message bus instead. The idea was that we would figure out later which sub-services actually need to be split out based on usage.
AFAIK, we haven't actually split out anything yet after all these years. All of our scale issues have been related to rather lackadaisical DB design within services (such as too much data and using a single table for far too many different purposes), something an arbitrary HTTP barrier erected between services would not have helped with at all.
Nope. Everything lives in one big DB, but services are not allowed to touch each others tables directly - communication must be done through service API endpoints.
I have seen micro-service architectures like that. It seems pretty pointless. You should be letting the database do as much of the heavy lifting as you can.
This is what I do in several of my backend Kotlin code bases and it's worked very well. I've also heard it called "distributed monorepo". There are a handful of ways to share code, and the tradeoffs of this approach are very manageable.
The biggest downside I've encountered is that you need to figure out the deployment abstraction and then figure out how that impacts CI/CD. You'll probably do some form of templating YAML, and things like canaries and hotfixes can be a bit trickier than normal.
> It can be easy implemented in other lang/frameworks as well.
No it can't, it relies on the runtime being capable of fairly strong isolation between parts of the same project, something that is a famous strength of Erlang. If you try to do the same thing in a language like Python or Ruby with monkeypatching and surprise globals, you'll get into a lot of trouble.
Modules are global singletons, class definitions are global singletons. Monkeypatching is less common in Python than Ruby but I'd still consider it a significant risk.
I think the key point of Fowler’s article, which is obscured a little by all the monolith discussions, is that try to start with microservices doesn’t work. He’s claiming this from an empirical point of view -- if you look at successful, working systems, they may use microservices now but they most often started as monoliths.
People are talking about Conway’s law, but the more important one here is Gall’s law: “A complex system that works is invariably found to have evolved from a simple system that worked.”
> I would propose alternative is to fix your monolith first. If the team can't rewrite their ball of mud as a new monolith, then what are the chances of successfully rewriting and changing architecture?
A million times this. If you can't chart the path to fixing what you have, you don't understand the problem space well enough.
The most common reply I've heard to this is "but the old one was in [OLD FRAMEWORK] and we will rewrite in [NEW FRAMEWORK OR LANGUAGE]," blaming the problem not on unclear concepts/hacks or shortcuts taken to patch over fuzzy requirements but on purely "technical" tech debt. But it usually takes wayyyyy longer than they expect to actually finish the rewrite... because of all the surprises from not fully understanding it in the first place.
So even if you want the new language or framework, your roadmap isn't complete until you understand the old one well enough.
This is the correct way to do things but, unfortunately, like I wrote in my other comment, this knowledge only comes after one has gone through a few of these "transformational" changes. I've seen this happen a few times and monolith vs micro services is such a done deal that it's very difficult to argue against so, even with experience, you will always fail if you try and go against the grain :)
That’s what I am always saying. If you can’t manage libraries you will fail at microservices too, just harder.
I am working on a project right now that was designed as microservices from the start. It’s really hard to change design when every change impacts several independent components. It seems to me that microservices are good for very stable and well understood use cases but evolving a design with microservices can be painful.
I've noticed after many optimization project in my investment bank that most architectures are fine, and rarely a source of or solution to problems.
The source of most of our problems is always that we started too small to understand the implication of our laziness at the start (I'll loop over this thing - oops it's now a nested loop with 1 million elements because a guy 3 years ago made it nested to fix something and the business grew). Most times, we simply have to profile / fix / profile / fix until we reach the sub millisecond. Then we can discuss strategic architecture.
Interestingly most of the architecture problem we actually had to solve were because someone 20 years ago chose an event-based micro service architecture that could not scale once we reach millions upon millions of event and has no simple stupid way to query state but to replay in every location the entire stream. Every location means also the C# desktop application 1000 users use. In this case yes, we change the architecture to have basic indexed search somehow with a queriable state rather than a reconstructed one client-side.
Another issue I've seen is that people push all the problems onto the monolith even if they're external - one place had a Perl monolith (which was bad, sure) but their main issue was an overloaded database which could have been addressed with moving some queries out of the (awful homegrown) ORM and using e.g. signed session cookies instead of every request causing a session table hit.
> If the team can't rewrite their ball of mud as a new monolith, then what are the chances of successfully rewriting and changing architecture?
Well sometimes there are very complex subsystems in the monolith and it's easier to create a completely new microservices out of that instead of trying to rewrite the existing code in the monolith.
We had done so successfully by creating a new payment microservices with stripe integration and then just route every payment that way. Doing the same in a huge pile of perl mess has been assessed as (nearly) impossible by the core developer team without any doubts.
But I have to admit that the full monolith code base is in a maintenance mode beyond repair, only bug & security fixes are accepted at this point in time. Feature development is not longer a viable option for this codebase.
> I would propose alternative is to fix your monolith first. If the team can't rewrite their ball of mud as a new monolith, then what are the chances of successfully rewriting and changing architecture?
Often, the problems with the monolith are
* different parts of the monolith's functionality need to scale independently,
* requirements for different parts of the monolith change with different velocity,
* the monolith team is too large and it's becoming difficult to build, test, and deploy everyone's potentially conflicting changes at a regular cadence, with bugs in one part of the code blocking deploying changes to other parts of the code.
If you don't have one of these problems, you probably don't need to break off microservices, and just fixing the monolith probably makes sense.
> the monolith team is too large and it's becoming difficult to build, test, and deploy everyone's potentially conflicting changes at a regular cadence, with bugs in one part of the code blocking deploying changes to other parts of the code.
This is the only argument I'm ever really sold on for an SOA. I wonder if service:engineer is a ratio that could serve as a heuristic for a technical organization's health? I know that there are obvious counter-examples (shopify looks like they're doing just fine), but in other orgs having that ratio be too high or too low could be a warning sign that change is needed.
So the alternative is to have a monolith deployed with different "identities" based on how it's configured. So you configure and scale out different groups of the monolith code independently, to satisfy different roles and scale up or down as needed.
However, sometimes the resources needed to load at start up can be drastically different, leading to different memory requirements. Or different libraries that could conflict and can also impact disk and memory requirements. For a large monolith, this can be significant.
So at what point do you go from different "configuration", to where enough has changed it's a truly different service? The dividing line between code and configuration can be very fluid.
> the entire monolith should be CI/CD so release cadence is irrelevant
But if one module has a bug and is failing testing, or features partially completed, it blocks releasing all the other code in the monolith that is working.
If we are at this point we aren't really talking about microservices though. More like taking a gigantic monolith and breaking it up into a handful of macroservices.
A champion will rise with a clean architecture and design in microservice form that addresses all high visibility pain points, attributing forecasted benefits to the perceived strengths of microservices. The team buys into the pitch and looks forwards to a happily-ever-after ending.
The reality though is that the team now has multiple problems, which include:
- Addressing conceptual debt that hasn't gone away. - Discovering and migrating what the legacy system got right, which is often not documented and not obvious. - Dealing with the overheads of microservices that were not advertised and not prominent at a proof-of-concept scale. - Ensuring business continuity while this piece of work goes on.
I would propose alternative is to fix your monolith first. If the team can't rewrite their ball of mud as a new monolith, then what are the chances of successfully rewriting and changing architecture?
Once there is a good, well functioning monolith, shift a subset of responsibility that can be delegated to a dedicated team - the key point is to respect Conway's law - and either create a microservice from it or build a new independent monolith service, which aligns more to service oriented architecture than microservices.