Hacker News new | past | comments | ask | show | jobs | submit login
Modules, Monoliths, and Microservices (tailscale.com)
322 points by kozmico on Feb 24, 2021 | hide | past | favorite | 87 comments



My observation is that much of industry does not care about any of these technical or security issues.

In theory microservices are technical artifacts, but what they tend to be are cultural artifacts.

Microservice adoption is often driven by cargo culting, or (better) a considered decision to work around a lack of organisational cohesion.

What microservices let you do is ship your org chart directly, and also map back from some functionality to an owning team. You know who to page when there's a problem and it's easier to tell who is late delivering and whose code is slow. In cultures with "lax" technical leadership (aka no everyone uses same thing mandate, I'm not judging) it lets teams use their favourite tools, for better or worse.

Other org related things are the ability to have independent (per team) release schedules. Separable billing. Ability to get metrics on performance, cost and failures that can be used as indicators of team performance and promotion material. Microservices can also act as "firewalls", limiting the impact a bad hire or team can have across your codebase.

None of this is intended to be negative judgement; microservices can (among other things), help teams feel a sense of agency and ownership that can be hard to maintain as org size scales up.


Yes. Conway’s law is socially inevitable, align with it. Hari Seldon approves this message.

NB: one may also just call them “services” with no practical loss of generality and sounding less like technobabble to everyone else.


NB: one may also just call them “services” with no practical loss of generality and sounding less like technobabble to everyone else.

Problem is that SOA has been around for decades whereas “microservices” sounds hip and cutting-edge


I think the SOA/microservices distinction has to do with the patterns of architecture more than anything else. (For example, the enterprise service bus is... well, ignore that if you squint it looks a bit like the service mesh and Kafka in a sufficiently complex microservice system.)


There’s no distinction anymore , just like DevOps is now a role not a movement, and hacker is a criminal not a maker. You can’t fight the natural language evolution when it’s buzzwords that people get hired on.


> “microservices” sounds hip and cutting-edge

Maybe 8 years ago it did.


I was definitely contemplating the definition of microservices back in 2012. I think Netflix had been blogging about it then, or just before.


> NB: one may also just call them “services” with no practical loss of generality and sounding less like technobabble to everyone else.

This is completely untrue, and I think it's part of the problem with discussions about microservices. People think it means "lots of small services". That is not what it means. That would be closer to SOA. Microservices is not SOA, and it is not "technobabble".

It has defined constructs, methodologies, and patterns.


> Microservices is not SOA

This is not true at all. Microservices are a Service-oriented Architecture:

From section 2.3 in "Microservices: yesterday, today, and tomorrow"

" Microservices are the second iteration on the concept of SOA and SOC."

https://arxiv.org/pdf/1606.04036v1.pdf

There's probably other links I could dig up to prove the point, but I think that one's good enough.


Of course they're related.


You're arguing that the majority of people use the term differently than you and are therefore wrong. Sounds like the definition of technobabble to me.


I think that tech has this extreme, albeit well-earn, revulsion to hype that makes them reject things based on terminology and community over actually understanding the thing.

I see it all the time. It's simply wrong to say that microservices is just "services". Many people being wrong changes nothing.


> Microservices is not SOA

The earliest things on microservices seemed to be identical to early SOA stripped of lore, usually of specific-context origin, it had accumulated in a decontextualized way, and which was often, especially once decontextualized, not particularly in-line with the original principles.

It's now gone on and attracted a whole bunch more of its own lore the same way, arising out of somewhat different contexts.


What _are_ microservices? </DavidDuchovny>


Then what is the difference?


https://www.oreilly.com/content/a-quick-and-simple-definitio...

I feel like you could just google SOA and Microservice architecture, pick up a book, and answer that for yourself.


> People think it means "lots of small services". That is not what it means.

Your link:

> Sam Newman provides a succinct definition of microservices in Building Microservices: “Microservices are small, autonomous services that work together.”


Yeah, this is what I see over and over again. Cherry picking to support bias.


You're the one making a claim that there's a difference, then offering a page that doesn't mention SOA as evidence. So 1) it's your responsibility to prove your own case, and 2) if you just googled SOA and microservice architecture and came up with that page, the strategy you're suggesting has failed.


Yes, I said that microservice architecture has distinct patterns and methodologies. I leave it to you, reader, to discover them.


And this is a real killer w.r.t. startups adopting microservices: if you're shipping your org chart, but your org chart needs to change frequently to adapt to different contexts as you pivot, arrange, create and eject use cases, microservices cause frequent friction because the relationship between the way you want to organize, and the way the boundaries are established in code, are so different.

I even see the reverse happening, org chart changes made based on service boundaries, due to this friction.


Is this really a problem though? Departments, shifts, brigades have been organized based on plant layout, machines, manufacturing lines, mine shafts, routes, regions and so on for ages.

Microservices or not, above a certain headcount there's no escaping this reality, there's an overhead due to communication, organization, etc. As long as the throughput is greater this way (than without organization, or without the increased headcount) it makes sense to do it.


> You know who to page when there's a problem ..

I don't believe that's true in practice.

Having many services moves a lot of complexity from 1 program to "the system" as a whole. All the stuff outside the services themselves.

Who do you call when the incoming requests are failing, but each service is working fine in isolation?


You call your resident "cloud team" or "infrastructure team" for help. Unfortunately, those guys are busy having an existential crisis looking into the abyss of machines, queues, cache layers and databases that are supposedly enabling "smooth team based development".

So you get adventurous and open up XRay or Application Insights or whatever the fuck. After a few hours you realize that the erroneous response code that you thought was the culprit is there by "by design" "intentionally".

So you say fuck it and add another microservice or cache layer that "fixes" the problem and call it a day. You can hear screams of agony from the infrastructure team, but who cares.

When you leave for the day, the boss pats you on the shoulder. "Nice work today, handling that incident", he says.


Further, I think microservices can encourages devs to stay in their own little walled garden and they're quick to say "everything's working on my end, must be somebody else's problem".

Very few people, if any, have a complete understanding of the system and this is often by design which may not necessarily be a good thing. Reasoning between microservices feels harder to me than reasoning within a monolith.


Isn't the only thing here that is enabled by microservices the separate tooling part? Couldn't the rest of it be achieved somewhat easier with a standard service-oriented architecture in a monolith? Why do we need to insert network calls between everything?


It allows different teams to write in different languages, have different deployment cadences and different testing mechanisms.

The overhead of network calls and its attendant problems can be a worthwhile price to pay if you need to loosely couple your teams.

This is why my yardstick of "should it be a micro service?" is "could it work as an entirely separate business?"

I was a massive skeptic of micro services until I saw that it actually solved some people problems pretty well despite incurring a technical cost.

I'm pretty convinced it's an anti pattern in small startups though.


I believe network calls are a looser contract than function calls that would be checked at compile time, so it allows a softer decoupling between multiple teams. Compatibility or intermittent failures are handled with different strategies (rollout, rollback, deadletter queues) instead of impacting release cycles.


They can do exactly the same when shipping libraries into the company's central library repository, NuGET, Maven, Gems, Eggs, whatever.


IMO this is a confused take on modularity. I think isolation and security are related concepts, but not what modules are for. Those things have to follow module boundaries, but they're not the goal of modules. For me, modules are about understanding; in order to use this code you don't need to read all of it, you just need to understand the interface. Modularity is for humans, not computers.

They do often follow Conway's law, but that's not a desirable thing. Most modules are outside of that kind of context -- they're in libraries written by people you don't communicate with at all!

I'm interested in people's opinion here. I'm also planning an article on the relationship between services and modularity.


> IMO this is a confused take on modularity.

You are restricting the scope of what we call a “module” to language runtime modules while the OP has expanded the scope to include things like network tiers and processes with an IPC interface.

I found the OP's perspective both new and useful.


Yeah, that and the author made it clear in the opening paragraph that that they are viewing "modularization" from a Systems Design point-of-view and not software engineering per se.


I think what I'm trying to say is that my definition of a module as a "unit of encapsulation or understanding" is the more inclusive and general one, as it includes network and process boundaries. The OP's use of the word is the restrictive one because it excludes language-level modules. I say this because security and isolation are not typically goals of language-level modules.

This is fine to talk about, of course. It's just confusing to reconcile the article with my definition of modules, which I thought to be the common one.


In some languages, like Modula-2, modules and libraries are exactly the same unit of measure.

You just don't group multiple modules into a single library.

Well you kind of can with sub-modules, but at the end of the day the binary library is generated exactly the same way.


Naming things is hard. My purpose was to emphasize the need to juggle the two definitions when assessing the article. We need a better word for the deployable unit of code that the OP calls a “module".


“Component” might fit the bill.


I think that's more or less the author's point as well. Modularity has some overlap w/ security (in the sense that we need stronger forms of modularity to get stronger forms of privilege isolation), but many of the other benefits of modularity that are usually the justification for adopting micro-services can be achieved through forms of modularity that assume trust (i.e. libraries, and other means of code splitting that may be bundled into a single unit of deployment).


Modules make it easier to understand software. Yet what is "security" but understandability? Modularity brings about understandability. And understandability brings security.

Software threats arise from the fact that coders don't in fact fully understand everything the software is doing, how it makes it impossible or possible for hackers to gain unauthorized access.

If we understood the software perfectly we could easily and quickly remove vulnerabilities from it. And if we understand its limitations we wouldn't let it run a commercial airline on auto-pilot.


> Yet what is "security" but understandability?

I prefer to think of security as the opposite of functionality: increasing functionality makes more things possible, increasing security makes fewer things possible.

I like this view because it forces us to acknowledge the tradeoff: the most functionality we can provide is a root shell with no passwords; the most security we can provide is an inert brick; we need to be specific about what functionality should be provided, what shouldn't, and design a system which sits between the two.

From this perspective, modularity can increase security by preventing one module from accessing/executing unexported parts of another module. Yet this implies that modularity also reduces functionality, for exactly the same reason. Again, we need to specify what should be exported, what shouldn't, and implement something between the two.


Is following Conway's law a bad thing, though? If the module is written/maintained by a single person, it seems to be more efficient and clear (of course subject to skill limitations of that particular human). What realistic alternative is there to Conway's law? Design by committee?

Conway's law might as well be, like democracy, the best of all the bad worlds.


Specifically in the context of modules, I think it is a bad thing. It's hard to describe because I do it quite intuitively, but (very broadly) the way I find module boundaries is by trying to find the minimal interface for the maximal amount of code. For me, this usually revolves around some kind of data structure.

Importantly, this is completely independent of team structure. The boundaries between teams are not necessarily the minimal interface.


Are the modules you're thinking of heavily shared/shaped by the needs of others? I think it's fair to say that there are more influences than just communication patterns on the design of software. For instance, there's can be a different set of concerns/pressures on developers of open source libraries than on time-sensitive and closely coordinated projects within a company.


> The boundaries between teams are not necessarily the minimal interface.

No, but they are the inevitable interface.


>Is following Conway's law a bad thing, though

Is following the second law of thermodynamics a bad thing? I say so jokingly because thermo is a little more rigid, but I still think it's futile to fight Conway. After all, if you change your org structure to try to beat it, you're actually proving it.

It's not something to beat as much as it's something to inform decisions. Code should be architected with knowledge of how the organization is broken up, and orgs should be structured with knowledge of how that affects the technical side.

I guess all this is to agree with you and say...if breaking up modules in a certain way for small teams is effective for your organization, then it makes sense to do it.


To add to that, I think the biggest thing to take away from Conway's law is that splitting teams of developers becomes a technical problem. If your goal is to efficiently build a product, technical expertise needs to go into that decision.

This is why I say Conway's law is a bad thing for modules (regardless of how inevitable it is): technical considerations are often not taken into account, and technical design is ossified by team structure.


Same, the whole tirade about sandbox escalation and other security aspect is of course a violation of isolation, but it's not particularly related to modularization.

Microservices and the like are not about isolating code, are about isolating responsabilities within code first.

The concerns expressed are valid, but the premise they move from are weird. Privilege escalation is a problem both in monoliths and micro services. People figuring out encryption keys from timing attacks is about the same threat both for micro services and monoliths.

also, for such a long article, I find it weird that not once the session management, authorization, and relation integrity topics are mentioned, while schema versioning gets touched but not interface versioning, as if the suggestion is to make modules talk to each other by touching each other data.


Some of the connective tissue in this article could have been better written, but I think the point of talking about isolation from a security perspective is this:

A. Privilege isolation is the hardest to refute benefit of microservices in theory, but you have to keep in mind that we need hardware virtualization to achieve a reasonably strong form of that (i.e. your k8s cluster isn't helping you out here, modulo Fargate perhaps)

B. This is rarely the primary concern when we look to microservices (or generally, modularity) to cure what ails us.

C. If we have less concern about trust boundaries, there are easier ways of modularizing code that probably solve the things we're really worried about (i.e. libraries or any other way of splitting code that is built into a single artifact).


I found this to be surprisingly interesting and novel - usually I expect to see really low quality content when it's a post about microservices, whether for or against the idea. This boils it down to isolation, which I myself have written about in a similar vein[0].

> at this point security people simply don't believe that any of the following (each one the very best technology available at the time) is totally safe

To be fair, we don't think anything is totally safe :) that's the point. Actually a lot of those things listed are why Chrome is so secure. I think this is a bit pessimistic about process isolation, which is actually quite powerful, but I get the point - there's no gap like an air gap.

I prefer to think of monoliths and microservices as being completely unrelated ideas - they aren't on a spectrum of "big to small", they're just two totally different ways of approaching software development. I might put "monoliths" closer to SOA, in that they both describe a very basic approach to a problem, whereas Microservices is not "small services" it's about bounded contexts, patterns, organizations, etc.

Anyways, I appreciated this post.

[0] https://www.graplsecurity.com/post/architecting-for-performa...


There is a subtle link between monoliths and microservices: co-routines. You can emulate a service oriented architecture inside a monolithically linked application. That way if you ever do decide you need to break for further isolation and/or to be able to parallelize the execution for more performance it down you at least can limit that to cutting on the dotted lines.

I personally like microservices a lot because they allow you to do one more thing (a variation on the isolation theme, sure), to limit the scope of what you, the programmer have to deal with at any given time. Your specifications, tests and mental activity all focus on a much smaller chunk of code, which is much easier to deal with.

I don't think I've ever built a bug-free monolith, but I'm fairly sure I've been able to repeatedly produce bug-free (or nearly bug free, you can never be 100% sure) microservices. And that to me is the bigger advantage, the scope limitation from the programmers perspective.


I find the most common outcome of adopting microservices is a buggy system made of bug-free microservices, via misunderstood specifications or surprising interactions.


Yes, what will typically happen is that people go overboard, and instead of 5 or so big functional blocks they explode the monolith into 50 (I've seen 200!) microservices and then end up with a huge overhead in terms of IPC and complexity in the IPC protocol that is worse than it ever was before. Bonus points if all it does is synchronous calls between services leading to a deadlocks and other hard to debug issues.

Good software architecture is hard.


Yes. Also something I see in messaging oriented architectures, often seen with microservices for async communication, since synchronous calls usually make the whole as reliable as its least reliable element.

Message orientation, in isolation for each message handler, is wonderful, easy to test, decoupled etc. But the overall flow is harder to understand and coupling at the business level still exists. You still need to remove stock from inventory after a sale even if the two parts don't communicate directly. Messages are COMEFROM.

The other thing is reliability. Monoliths can usually rely on the ambient availability of resources - a synchronous in-process call will have a CPU available because that's how in-process calls work - but services across the network might not have resources to respond for some time. It cuts the other way too, though; when the monolith is down, everything is down. There's tradeoffs everywhere.


Every bit of complexity that you add has a price tag, before adding it you need to look at that tag and decide whether or not the price is worth it in this particular case. More often than not the answer is 'no' but people will go ahead and do it anyway.


Coroutines can have shared memory though and the lack of shared memory is perhaps one of the defining features of microservices.


You can have same-process memory isolation in any programming environment that support isolates/actors, like JavaScript (via web workers[1]), Dart[2], Pony[3], and if done carefully, Rust[4] (probably many others too but I am not familiar with others that I can remember now).

[1] https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers...

[2] https://medium.com/dartlang/dart-asynchronous-programming-is...

[3] https://patterns.ponylang.io/data-sharing.html

[4] https://doc.rust-lang.org/book/ch16-02-message-passing.html


Yes, it is an in-between step, which is why I used it as an illustration. You get roughly the same architecture but without the process isolation. In fact: co-routines will share some memory, whether you like it or not, they are in the same executable.


> You get roughly the same architecture but without the process isolation.

I think that's only true for very specific kinds of architecture which are only tangentially related to coroutines: stateless data transformations or actor-inspired architectures. Things like shared atomic data structures or mutexes translate pretty poorly to microservice architectures (they're doable, but really painful, bug-prone, and you give up a ton of performance).

EDIT: Hmmmm... on reflection, I suppose if you think of the actual stateful set of yield and continuation operations of coroutines they are one particular possible microservice architecture, but it strikes me as a bit messy? Although I guess you can retrofit the usual set of other concurrency operators on top.


It's a hack for sure, you are essentially creating a new little operating system within one binary. But it is an architecture that works remarkably well for quite a few applications: message switches, games and so on. And it even works on architectures where you only get one thread of execution.


Ah I see, you perhaps mean a slightly more general concept of coroutines than I was thinking about? Closer to something that fits a thread-like interface on top of actual threads regardless of whether it's being done through direct yields or intermediated by some sort of scheduler?

Basically the crucial point being getting concurrency without the need for actual parallelism?


Yes. And you can simulate that perfectly using the co-routine mechanism.


But Redis....

Seriously, I've seen a monolith broken up into a microservice using Redis as shared memory. Didn't know it was an anti-pattern.


> Didn't know it was an anti-pattern.

You really just have to be clear whether you are building a distributed monolith vs a set of microservices. In the former case, you are optimizing entirely for performance and scalability concerns. It just so happens if you have things that require scaling of the underlying hardware, our current state-of-the-art tooling handles scaling hardware resources (number of cores and memory) better when you have separate services rather than bundling it all in a single program.

However, you get none of the operational/human-level process changes of microservices. Teams are still coupled to the changes in the shared state. Anything that changes the shared state cannot be deployed independently. In fact from a process perspective things are probably even harder to coordinate now since the coupling is still there, but not explicitly expressed in code anymore. And from a site reliability perspective, you still have a single point of failure: the shared memory. Any mitigation strategies you use for that you could've used for a monolith as well.


There is one other reason I don't often see addressed for the push to microservices. This mostly pertains to government acquisitions, but any software contracted to enterprise customers faces the same issue. It's vendor diversification. It's very difficult to split contracts for a monolithic system between multiple contractors, which means you end up getting tied to one vendor. If you can decompose a large system into pieces that can be individually developed, tested, and deployed independent of another, you can split the contracts for developing each of those pieces between different contractors and there are many good reasons to want to do that, from the perspective of security, financial risk mitigation, encouraging competition, and just the basic principle that many minds tend to be better than one.


That cuts both ways. It's a great reason for government to want microservices, but it's also a great reason for contractors to never even present them with that option.


I once worked on a project (US DOD) that was basically a conversion from monolith to SOA. The contractors did, in fact, fight tooth and nail to keep it from happening because it would reduce their apparent value and necessity. USAF totally fucked up the management of it (at the time I was involved), other services had similar projects which had much better transitions. And for them it substantially reduced costs and decreased time-to-feature. USAF just seems dead set on using the old block release style (big bang releases every 12-60 months) which was at odds with the attempted conversion and helped the contractors persist their miserably awful architecture that practically ensured another few billion in revenue for them.


Yeah, totally. I'm anonymous enough here and not going to out my former employer, but one of the things that turned me off and a reason I'm not there any more was finding email trails intentionally suppressing findings from trade studies suggesting the government could get applications deployed faster and cheaper if they didn't use the ridiculous giant framework we'd managed to rope them into years earlier.


This is the best article on software (and a bit of hardware) architecture I’ve read in a long time.


A good primitive rule of thumb that's worked for us is to fork services when: a. They're performing an action fundamentally different to the the block they originate from (one service may respond to user driven actions, another may be batch or cron driven), and b. there isn't synchronous coupling between the blocks.

If all interactions are synchronous, you're often adding complexity at the IPC layer, and need more boilerplate on either side since you need the presumption that the service you depend on may not exist.

Even so, if the services are fundamentally different in their scaling needs, security demands, availability, etc splitting services is a consideration.


Where do you draw the line with co-routines in that mental model? Languages like Erlang/Elixir built on BEAM are one approach to solving this problem of "synchronous programs calling components with varying scaling needs". It feels fundamentally wrong to me to deploy the components as isolated services simply because of scaling differences.

Maybe, over time, the ideas behind k8s and Service Meshes will start to close this gap of complexity? Or is it a language problem that will create the solution, ala Erlang?

How does one manage authentication/authorization in a distributed system? Is that on the service mesh also?

Complexity is a bitch!


It is indeed bitchy!

I'd say simply having different scaling needs may not suffice to isolate services, especially if all the other parameters I mentioned stay the same. Even so, some cases for needing to isolate purely based on scaling - in my experience - have been when the scaling requirements are drastically different, with completely different geographical needs, scale ceilings, etc.

We try to keep authorization within the system itself, with additional hardening on the service mesh/container/vm/network level as needed. I don't know that this is the right way to do it, but it's worked for us thus far.

Biggest problem with isolating services is that it reduces predictable errors by giving you smaller chunks you can reason about, but raises unpredictable, unreproducible (aren't those a bitch) errors down the line because the overall complexity has grown.


I worked at two big companies in Chile since the last 3 years and I see microservices everywhere, it's huge here, almost a standard right now from my point of view.

We have still some monoliths, like some EARs in some Jboss on-premise but the cloud+kubernetes+microservices trend seems the obvious way to go for everybody. Those monoliths are minimally maintained, new features go to microservices, and the end goal is to throw them out, it take years though.

It is very likely than microservices are way too small IMO, I've seen microservices just doing an IF, just doing an SQL select, or just adding some credentials in a HTTP header, stuff that could work in one service are sometimes splitted in tens of microservices.

Althought these microservices are Java/Spring Boot or Node/Express ones most of the times, despite the fact that you could potentially use the best tech for each microservice, managers want to maintain a limited tech stack where they can easily find cheap developers, basically Java and Javascript, some Python or Go maybe but not so much.

What strikes me the most is that it seems to work, till now at least, each developer have to maintain like between 3 and 10 microservices, but very simple ones.

Merging two microservices into one never happens.

Writing a new microservice is almost more frequent than evolving an existing one.

Is it a worldwide trend or am I living in a particularly excessive bubble?


Monoliths have a long history, going back to frameworks like CGI, Django, Rails, and PHP.

I don’t understand what he means here - a website where every page is a separate CGI script and state is managed by the client is the very prototype of microservices.


Each CGI script has access to pieces of state that the other scripts in the system have access to. For example, "login.pl" could access state related to more than just logins; it has full access to the database and could conceivably do anything. For that reason, people call the architecture monolithic, even though each API endpoint is its own computer program that runs top to bottom in isolation from the other CGI scripts in the directory/app.

A microservices approach would separate the state related to logins from the state related to other features. So there would be a login services, that accepts usernames and passwords from end users, and provides internal services like "given a session cookie, tell me if it's valid and what the user's name is" or "store this key/value to the session" or "read this key/value from the session".

Isolating concerns from each other is common in both monoliths and microservices. It's just how people tend to design ANY computer program or distributed system.

Obviously it's hard to draw the line between "function that does this" and "service that does this", and it's possible to have microservices that you call over an RPC that just reads/writes the same database the caller uses. It's messy and indistinct, and is probably too general a word to attach a ton of meaning to.

(My followup is to read deeply into critiques of the patterns. The HN commenter that says "microservices suck" will be perfectly fine that Apache sends a WSGI request to a Python app, which speaks TCP to a database. Those, they say, are services, not microservices, so their rage does not apply in that case. But... it's a pretty arbitrary line, so you have to read and understand carefully to identify the problems they have and what advice of theirs to take.)


If anybody wants to chat about how to harden production systems, let's talk. That's a huge focus for what I have in my head to fix right now. (Founder of a YC startup)

Lambda helps but is limited. Dealing with existing code is hard. If this is a problem that keeps you awake at night, I want to talk with you. I want to figure out what can be done to remove single points of failure/compromise in production software systems.

Email is free at refinery dot io


Not an expert, but if you like lambda, I suggest looking at its predecessors, in particular, CICS and OTP.

My current take on this. The "business logic" should not be written in Turing complete languages, but rather composable languages based on simple type theory. So the "business logic" would be limited in what it can do to compromise the system. And then it should be deployed on the execution platform, which is a different concern altogether.

I think using TC (like Java or Javascript) languages for everything is a wrong direction in SW engineering. I think we want to have TC languages for the low level tools (like parsing data, algorithms), and for the execution engine, but we want to compose these in languages that can easily compose, and thus are restricted.

IMHO there are at least two different categories of compositional restricted languages. One is data manipulation languages, that describes how to manipulate data in the schema, regardless of the platform or physical data representation (I really like what David Spivak is doing in this area with categorical databases). The other is system architecture languages, which shows how are these data manipulations executed, and how they compose into transactions, and more broadly, software systems. The former category concerns with how the schema of the data changes, and the latter category concerns with how are the data in the same schema exchanged at the boundaries of different SW systems. (There are probably also useful categories of restricted languages to describe UI and visualizations.)


Super awesome write up -- thank you! Honestly I have been thinking about this problem for a few years now but not as succinctly as you have described it here. That's really helpful to split it down the line like that. It's very hard to uncouple complex topics cleanly.

"Workflows" have been an area of focus for a product we've been building for a while now. A big part of that goal is to remove the "Turing completeness" from the system and keep each "block" of a workflow as a simple blackboxed data transformation. And "conditional transitions" are used to emulate business logic.

It's a bit clunky though and, after reading your text here, it makes me want to sit down and think for a few hours. Thank, internet stranger, you have made a material difference in my day over here! It's a nice feeling to hop out of bed early and excited :)


> My current take on this. The "business logic" should not be written in Turing complete languages,

I think this is an excellent point. I've been coming to this same conclusion for some time.

For a lot of use cases a less powerful but more predictable language is probably what we need. State machines and DSLs go some way towards this but I don't think that's the full solution either.

Maybe something based on a restricted APL language, either way it sounds like a good PHD project to me.



Going to give this a deep dive. Thank you. Super curious how it works after reading the intro page!


I also believe in this as a promising direction! Specifically I have been trying to apply it to full-stack web app development for some time now by building a DSL (https://github.com/wasp-lang/wasp).

Why do you think DSLs are not the full solution? My main thought was that having DSLs for specific areas of programming domains is the way to go. But they should have interop with Turing complete languages to implement specific sub-parts. That way DSLs can capture a lot of knowledge and best practices that we learned through years in that domain while still facilitating innovation being flexible. Such language would be very limited, more like "smart configuration languages". Some of them I have seen around are Terraform, Nextflow, Meson-build.

APL still sounds too general and self-sufficient to me to be a solution to this (but I don't know much about it so maybe I am wrong, I just checked it out on the wikipedia).


Monolith vs Microservices is a false choice. You can easily use both, as we do. Monolith for what makes sense (a user application) and microservices where they are helpful (scalable email services, auth services and stuff in a cluster).

Microservices solve many problems and cause others.

People pointing out that they aren't a silver bullet are preaching to the converted. Anyone who honestly believes that anything in engineering is always better than something else haven't been listening in class!


Did you read the article? The author builds a very interesting mental model for reasoning about what purpose each provides. Basically, as you are alluding to, the choice between Monoliths and Microservices is a false dichotomy. By analyzing _why_ we feel we must choose between them and the benefits they each provide, the author providers the reader with a framework to understand how/when to make choices about software isolation. He approaches it from the perspective of Security, which tends to align well with overall "system complexity".

It was a very good read and I'd encourage you to grind through it all (and not skim ;) ).


Refactoring of software is here to stay, whether it is monolith or Microservices, we need to refresh the codebase based on changes in use cases/product, technical evolution & new learning. Software is a living thing. We need gardeners to prune, clean & replant regularly. If we do not mess around with it, it is going to die with time.


Interesting perspective on the architecture of everything software. I love the overarching "security is an illusion" theme


>By far the part we're worst at is #1, isolation. If we could truly and efficiently isolate one bit of code from another, the other goals would mostly fall into place. But we simply do not know how.

This might sound a bit mean but the author hear lacks clarity in thinking and therefore really doesn't know what he's talking about.

We 100% know how to isolate code. In fact the primitive that guarantees this isolation is part of the name of this site: combinator. Isolated logic is literally the definition of a combinator.

If every line of your code was written as a combinator then all your code would be reconfigurable and modular to the maximum amount theoretically possible.

The hard lines drawn with physical hardware are largely irrelevant and only obscure the main point. It's not relevant whether your code is isolated by a combinator or hardware acting like a combinator. There is no theoretical difference between using a virtual folder to group things or using a computer to group things.

The main issue with 'isolation' then comes down to IO, because all IO related primitives cannot be part of a combinator by definition and IO is a required part of computation. (by IO I mean input/output and shared mutable memory access)

The article doesn't even mention this. The main problem with modularity is how to deal with it in the face of IO. Hence he's not clear about what he's talking about.

IO breaks modularity & isolation and so does the use of anything that is not a combinator.


I have a question, I am also a total noob on this but you maybe able to teach us (I mean it, not being sarcastic, actually I like your reply a lot).

In Scala, for example, when using functional programming, even IO is represented with an object (take ZIO for example, or Cats effect), does this IO still breaks isolation and modularity?

I have seen code that looks extremely modular, reconfigurable, and isolated (exactly like you mentioned) but even IO (lazy/async) didn't make it bad, but again it is encapsulated and not actually directly dealing with IO and it only materializes at absolute end of the computation.


>In Scala, for example, when using functional programming, even IO is represented with an object (take ZIO for example, or Cats effect), does this IO still breaks isolation and modularity?

Yeah. It does. Think about it. if I have a function called:

  getNumberFromMutableMemory :: void -> IO[Int]
The value of the int is reliant on an external dependency. Whatever IO represents, whether it'd be user input or a database is an external dependency and by definition NOT isolated and NOT modular because the IO function is coupled with the external dependency.

Additionally if the IO is mutable memory the value of the function is also dependent on every single other thing that modified that piece of memory.

The IO types form another type of isolation that is more meta. These types serve to segregate your code away from modular code that basically has NO external dependencies namely the above code cannot be directly composed with a function of this type:

  addOne :: int -> int 
This kind of thing is cleverly done by making it impossible to construct anything of type IO[T] without doing a system IO call. You will find that because of this even if all your functions are typed with IO, you can't really compose them together or even access the internal value of IO without using the bind operator to segregate IO from the world of pure calculations.

>I have seen code that looks extremely modular, reconfigurable, and isolated (exactly like you mentioned) but even IO (lazy/async) didn't make it bad, but again it is encapsulated and not actually directly dealing with IO and it only materializes at absolute end of the computation.

Code being bad or good is a matter of opinion. I'm not saying IO makes code bad, it makes it less modular (and IO is also a required part of all apps, so all apps require a degree of broken modularity). Categorically, anything that touches IO or mutable memory is less modular simply because it has an external dependency on IO. Anything with a dependency is not modular by definition.

What you'll find is that there tends to be a type of IO that can maintain purity of your logic and that is if the IO only goes in one direction: outward. If all you're doing is printing statements to the console or writing but never reading from external memory, you don't even need an IO type to segregate your pure logic, everything will work transparently. Let's say I have a function called print that prints a value but also returns the same value given to it. I can use it without an IO type and it won't effect any of my pure logic:

  print :: Int -> Int
  addOnePrintThenAddOne = addOne . print . addOne
It's only when you start reading from memory that code becomes less composable.

One key to modularity is to see how composable your logic is..

For example:

   addTwo = addOne . addOne
The above is literally the definition of modular code. I take two pieces of logic and put them together to form new logic.

Good luck trying to do the above with IO functions that write and read from IO or mutable memory.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: