4 ms·
If the monolithic application is written in a language with sufficient encapsulation and good tooling around multi-module projects, then you can indeed have wel
by lemmsjid 3y ago
If the monolithic application is written in a language with sufficient encapsulation and good tooling around multi-module projects, then you can indeed have well known and encapsulated interfaces within the monolith. Within the monolith itself you can create a DAG of enforced interfaces and dependencies that is logically identical to a set of services from different codebases. There are well known design issues in monoliths that can undermine this approach (the biggest one that comes to mind is favoring composition over inheritance, because that's how encapsulation can be most easily broken as messages flow across a single-application interfaces, but then I'd also throw in enforced immutability, and separating data from logic).
It takes effort to keep a monolithic application set up this way, but IMHO the effort is far less than moving to and maintaining microservices. I think a problem is that there's very popular ecosystems that don't have particularly good tooling around this approach, Python being a major example--it can be done, but it's not smooth.
To me the time when you pull the trigger on a different service+codebase should not be code complexity, because that can be best managed in one repo. It is when you need a different platform in your ecosystem (say your API is in Java and you need Python services so they can use X Y or Z packages as dependencies), or when you have enough people involved that multiple teams benefit from owning their own soup-to-nuts code-to-deployment ecosystem, or when, to your point, you have a chunk of code that the team doesn't or can't own/maintain and wants to slowly branch functionality away from.
- protomolecule 3y ago"composition over inheritance, because that's how encapsulation can be most easily broken as messages flow across a single-application interfaces, but then I'd also throw in enforced immutability, and separating data from logic" Could you elaborate on this? I see how "separating data from logic" is a problem but what about the other two?
- lemmsjid 3y agoWell now that you mention it I think it does all come down to 'separating data from logic'. I was working backwards from the premise of: "what if we want a monolithic in-process application to have the same cognitive simplicity as an API-based client-server model?" If you want to enforce a clean interface between an in-process client and server (i.e. a piece of code calling a library interface), then the best model is to think of it as a message passing system, where once a payload is passed from the client to the server and vice versa, the other side should not be able to witness changes to the payload. An immutable payload in this context is the same as the json that goes over the wire between a client and server. If you really wanted an in-process application to look like a microservice you could take the added step of forcing a serialization / deserialization step on both client and server. I've seen frameworks that do this. But I think immutability, if it can be enforced, is a practical way of solving this problem and far less complex. Inheritance is a hazier point I was making, in hindsight, because you could use inheritance for data modeling, which is quite fine in some situations... so I think it's effectively subsumed under the "separating data from logic" argument: which is that if you're passing behavioral inheritance from a "server" to a "client", then it gets harder and harder to predict how that client is going to use it and thus it's harder to reason about the functional boundary between two pieces of code. But it's a hazy point because I think the larger and more important point to make, as you point out, is that you simply shouldn't (in most cases) pass any kind of behavior between client and server--just data. Another approach to look at (besides taking inspiration from Smalltalk) is the actor model, where it's incredibly clear that any communication between modules in an ecosystem is immutable messages. In fact a side effect of the actor model is that it gets pretty easy to move things from in-process to cross-process because most activity in the system is already performed through message passing, so you can just start channeling messages from Actor A to Actor B through a serialization/networking layer rather than in-process if you want to deploy them separately for reasons of convenience or computational needs.
- protomolecule 3y agoI see, thanks. My original interpretation was that by "separating data from logic" you meant data-oriented programming where you intentionally give up on encapsulation. "which is that if you're passing behavioral inheritance from a "server" to a "client", then it gets harder and harder to predict how that client is going to use it" You mean a "server" returning the base class object whose methods are overridden by a successor class known only to the "server"?
- lemmsjid 3y agoYep. Mentally it's rather like the unpredictable behavior you'd get if an API server was passing code to the client for it to execute.
- gedy 3y ago> If the monolithic application is written in a language with sufficient encapsulation and good tooling around multi-module projects, then you can indeed have well known and encapsulated interfaces within the monolith. Within the monolith itself you can create a DAG of enforced interfaces and dependencies that is logically identical to a set of services from different codebases. So then, not Rails apps
- lemmsjid 3y agoYes, a good point, I think the more dynamic the language, the more one gravitates towards microservices as a problem solving tool, because you'd give up a lot of the value of the dynamic environment by enforcing strict rules. Though I'm seeing a lot of convergence between environments over time, which makes me think we're all headed towards a nicer future. For example in Scala, which does a lot of type inferencing, it's typical to tell the linter to require that public methods have explicit types, even though the compiler will reify them at compile time anyhow, because that makes the interface much more robust to refactoring. Meanwhile in a more dynamic environment, in Python it's getting more typical to use type annotations, and, similarly, to especially use them on functions that define a reusable interface. I figure that the ideal languages in the future have module-level systems where they can define strict interfaces across modules, but then get as crazy and dynamic as they want inside of the modules.
- LelouBil 3y agoWith microservices, you can also version them independently. In a monolith you can't roll back "a part" of the app to the latest version if you pushed multiple unrelated features at once.
- lemmsjid 3y agoYou can do the same with the approach I described. If you set up the modular DAG as I mentioned above, you can now set up service boundaries between the leaves of the DAG. E.g. parts of the code call other parts of the code as a service. You then version and deploy the same codebase separately. Say you have Libraries A, B, and C, where B and C depend on A and not one another. You can have B call into C via a service, just as you would in a microservice. Now B and C can be versioned and deployed independency. You can also deploy updates to Library A incrementally, tying it to the B and C deployments. If you are literally pushing different features that are in fact unrelated, you don't even need to worry about B calling into C, you can just partition your app into different modules, deploy them separately, and use a load balancer with routing rules to arbitrate between the deployments. I like having this type of environment because you can make fairly quick and easily-resersible decisions about whether or not different parts of the codebase are deployed differently: sometimes it's compute and hardware requirements, sometimes it's because you want parts to be stable and other parts more experimental and volatile. The microservice argument isn't addressing this type of deployment scenario: it's suggesting a shared-nothing or shared-little architecture across services.
- charcircuit 3y ago> a monolith you can't roll back "a part" of the app to the latest version if you pushed multiple unrelated features at once. git revert <bad commit>
- LelouBil 3y agoWell yes, but if you used a compiled language you have to make a new release based on this commit. What I mean is, if you have v2 of your software that introduces a bugfix for module A that's perfectly fine and a bugfix for module B that breaks everything, you can just roll back just module B directly with your deployment pipeline. There's no need to go back to the code base and make a new release.
- cpill 3y ago> It takes effort to keep a monolithic application set up this way Yeah, this is the problem and _why_ I think microservices are the way forward as it doesn't take effort because the programmer is forced into do the right thing. On project with many different types of coders (and lets face it we are all different) consistency drops off fast. Of course you can do it with monoliths but I'm coming from a "real world" scenario where there are many people with different levels of ability and different levels of giving a cr@p about code quality. Micro services let people who code badly to do it in isolation and let themselves be the only ones who have to suffer under it, and ultimately learn from it (if they are not fired first). Also decoupling in a monolith vs by deployment is really just which git repo the code lives in, which are next to each other in the same directory on your hard drive. If there is shared code factor it out as a library/module and install it into the projects that need it. Its not a big deal
- lemmsjid 3y agoMy instinct from your response is that if we chatted on this for a bit in person we'd see eye to eye and we're probably thinking of different use cases, because I can see how thoughtful you are and usually it comes down to thinking about particular problems in particular companies. So at the risk of going back into crass generalizations, I do persist in thinking that the TCO of a monolith is lower under a lot of the constraints you're describing, assuming (big emphasis) the tooling of the language you're using for the monolith, i.e. a combination of linters and compiler, can enforce the rules. I've been in service-first environments where the equivalent of not caring about code quality in the monolith is firing up a new service without consideration for refactoring the older service, until I am in endless architecture meetings about how to make cross cutting changes around a bunch of services everyone regrets creating. I suppose in the end it comes down to the classic issue of needing to pay down of tech debt, which is true in any software ecosystem.