Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> Shared data structures? Common library? Services have an API. If you are sharing data structures and a common "core" library across services, yes, you are officially doing it wrong.

Maybe I've been doing this wrong all my life then.

Imagine I want to fetch user data by id from Galactus, the user data service. I know what shape of data I want from it, and Galactus knows what shape of data it provides. Should both my service and Galactus have full, individual copies of this data structure? Alternatively, if Galactus has a published schema that I refer to, isn't that a shared data structure?

By extracting the user data shape to a common repository (library, header, schema, whatever), not only we deduplicate code, but we also centralize documentation. Maybe the `address` field is cached and frequently outdated, and someone from Galactus noted this down along with where to find the golden source.

Once you factor in other sources of redundancies (username regex, recommended logging, business-specific snippets), that naturally becomes a common library.

I get that it sacrifices some service independence, but is this piece of independence always so worth it, that common libraries are "officially doing it wrong"?



> Should both my service and Galactus have full, individual copies of this data structure?

Yes!

Maintaining that structure should be painful. Why? Because it's an exposed public API and there are ramifications to updating it that need to be thought out.

Further, it can be super easy to pollute that structure with information only relevant to the server or client.

Using something like JSON schema, openapi, or grpc/protobufs can make generating that second structure easy, but sharing it is something that has caused my org tremendous headaches.


IMO the question to be asking is “how do we reduce risk of second and third order effects of changing the API, regardless of whether the change is intentional or accidental.” In your model, any clients consuming the API would have runtime errors if there are breaking changes because the types/data structures are maintained independently. These runtime errors might be tough to debug. Hopefully you know there are breaking changes coming so you can prepare for when the API change is deployed. If it’s a regression then maybe you get an incident. In the shared type/data structure model the error happens at compile time because the rest of your app doesn’t know how to consume the breaking type change. You know what broke because now your client won’t compile. There may still be runtime errors if the API was deployed after the latest client deployment, but generally it’s easier to surface where the regression/change happened because the types are shared.

I think many people prefer the compile-time error surfacing.

There’s definitely trade offs to sharing types though, especially if the API and clients are in different languages. If your team is small enough and has enough context on API changes it’s probably easier to maintain different types. But if it’s large or spanning multiple teams it might be better to rely on tooling to share or generate types. It’s always about trade offs.


HN isn’t letting me edit my comment so here’s some additional stream of consciousness from me.

There’s definitely trade offs to sharing types, especially if the API and clients are in different languages. If your team is small enough and has enough context on API changes it’s probably easier to maintain different types. But if it’s large or spanning multiple teams it might be better to rely on tooling to share or generate types. It’s always about trade offs.


Ok, so this answer, and the other answers in this comment thread thoroughly convince me you're all batshit insane.

How can you get any work done like this?

Are you all working at massive corps that develop at a glacial pace?

Sounds like you all spend your time shuffling papers rather than doing anything meaningful.


> How can you get any work done like this?

People get work done by knowing what they're doing, which I'm not sure you are able to tell.

There is plenty of literature that explains quite thoroughly the process of software architecture. Basically all major software architecture styles from the past four decades reflect the need to encapsulate and insulate implementation details, including the need to specify a domain model and how it should be specific and exclusive to each project.

Somehow, you are oblivious to basic principles but still feel entitled to insult others based on domain knowledge you clearly lack.


Do you want a serious answer, or are you just here to insult people? If you edit your comment to remove the insults, I'd be glad to share my thoughts.


Look, I've never seen microservices done well. I'm negative about it because people are implementing this architecture in applications that as far as I can tell should never even consider it. Maybe they're just doing it badly. But as far as far as I can tell it's a pretty awful architecture pattern.

To implement microservices, one takes what is a service in a normal application, a handful of code files in a normal application. Maybe some model files, a service, validators and a repo file. A slice of an application.

One creates a new project file, build files, etc. Maybe a new repo, maybe not. You then wrap that simple service in a bunch of boilerplate plumbing code so it can actually work on its own.

So, basically, a ton of extra code, right off the bat. Each time.

Then to do it right according to this thread, you duplicate the definition files between your services, multiple times, add JSON schema files that you didn't have to maintain before, and, someone else has mentioned, create an extra library on top of all this so your colleagues can implement it as if it were just a normal method call.

Even more code!

And that's your microservice. A lot of extra work. Busy work as far as I can see, no benefits. Just to do exactly what it used to do.

But, worse still, it has huge drawbacks, including:

    1. Very slow "method" calls. Normal method calls are obviously orders of magnitude faster than whatever you're doing. L1 Cache is always going to be massively faster.
    2. Poor debugging
    3. Complicated devops requirement
    4. Hidden complexity in the interaction between services that is impossible to see

I just don't get it. Never have. I tried to play along, but I personally think the emperor has no clothes. If a client is going to insist on microservices, so be it, but it's a massive waste of time and money in my opinion.


You're the first person in this thread to mention microservices. The discussion has been around broader service-oriented architecture. Sometimes those services can be quite large, in which case the boilerplate overhead is not nearly as onerous as you describe. I've worked on services that had 200+ engineers on them.


That's what the article is about.

That's what this thread is about when the original poster says "you're doing services wrong".

So, no, I'm not the first person to mention them. You just need to read the context of the discussion.


Friend, do you not understand that not all services are Microservices?

The article even states Microservices are not suitable for startups - the conversation in this thread has been able service oriented architectures which is a much broader topic.


None of what's described above is materially difficult or slows down a team used to this method of operation. Perhaps stop applying your narrow lens to all development.


A team, yes. This was two people doing everything. Why would they fully use a design practice designed to scale people on a codebase?


You are oblivious to the point of this approach. Scaling has nothing to do with it. It has everything to do with not imposing useless and detrimental constraints that buys you nothing but problems. You specify interfaces, and keep implementation details from leaking by encapsulating and insulating them. This is terribly basic stuff.


You can do all that without separate services.

OOP languages have interfaces.

You can do this already without adding any sort of microservice, schemas, duplicate definition files, externally maintained libraries, etc..

It's a basic feature of most languages.

It is NOT an exclusive benefit of a microservice pattern. Stop claiming that, it's one of the most frustrating claims/lies microservice advocates make.

The actual benefit is that you're forcing developer to use interfaces. At a massive cost.

There are much cheaper alternatives. You enforce a Dependeny Injection pattern on your services. Code reviews. Linting tools.

So no, this is not basic stuff.

And worse still, if your team can't properly use interfaces in your languages, how do you expect them to suddenly learn to use them properly in your services?


It'd be great if you minded your tone; this is HN.

I don't know where you're getting implementation details leaking when it's just API definitions being shared - they don't leak implementation details unless they're badly designed, which would affect them either way.


I wonder if there are two uncontrolled parameters here.

Firstly, the space and time scales. If your two-pizza team has twenty services, and they communicate like this, and interfaces change a few times a week, then there will be quite a lot of pointless paperwork. If your two-pizza team has one service, used by other teams, and the interfaces change once a month, then this might be an appropriate amount of speed bump.

Secondly, tooling. If your APIs are all done by hand, then making an update is a modest amount of boilerplate. If you are generating everything from schemas, and you have your build down tight, then it can be a matter of changing the schema file, pushing, waiting for that to propagate, then adding the necessary data to the message you changed.


the whole point of this architecture is to transform conway's law from a liability to an asset -- it's a solution to problems that only exist at org sizes large enough where product velocity is bottlenecked by inter-team friction

services map to teams, not units of functionality

imo minimum org size to use microservices is something like 50 engineers


Whether you're right or wrong, the negativity isn't needed and accomplishes nothing.


Technology should never be painful. If it is, it means you're doing it wrong. The fact that you have to put in work and tell the technology you have this shape in two different places is a code smell. The system is telling you the approach is wrong when you are working for the technology instead of the technology working for you.


I'd suggest reading "Falling into the pit of success" [1]

> a well-designed system makes it easy to do the right things and annoying (but not impossible) to do the wrong things.

When designing an API consumed by many people, changing that contract is often the wrong thing to do. You'll have unintended breaks with your consumers. So, adding an extra layer of annoyance is something that tells a dev that wants to change one of these models "Hey, what you are doing might impact more than just the place you are changing. Maybe you should rethink this".

That is, it's a bit painful.

Good software is using pain and annoyance to discourage devs from hurting themselves. When making a bad change isn't painful, it happens frequently.

Have you ever wondered why we write tests? Tests encourage pain. They make future changes annoying (because you might need to update the test, a pain). Yet most people see writing tests as a virtue, not a vice. That's because the pain of the failed test prevents unintended consequences of code changes.

That's what having 2 models does.

[1] https://blog.codinghorror.com/falling-into-the-pit-of-succes...


> Using something like JSON schema, openapi, or grpc/protobufs can make generating that second structure easy, but sharing it is something that has caused my org tremendous headaches.

That's just implicit sharing, or versioning with extra steps...


Another answer:

> Imagine I want to fetch user data by id from Galactus, the user data service. I know what shape of data I want from it, and Galactus knows what shape of data it provides. Should both my service and Galactus have full, individual copies of this data structure?

No. Galactus should have the full shape of the "user object" but should never put that on the wire or expose it. The wire API should expose sensible things like "what is the billing and shipping address(es) of this user id". Galactus is responsible for maintaining the mapping between the "user object" and whatever the relevant return format is for those data.

Edit: This allows you to run API migrations and data migrations independently. Which is crucial for any service that will last longer than 1 year.


Agreed with all of this. I consider the API structure (which might be defined by protobuf/Avro/Thrift/etc) to be separate from anything that is used by either the server or client services. The API structure is the only thing that should be shared.

When you start out, it will feel very repetitive because your client object structure matches your API structure matches your server object structure. You might even think "DRY!" and want to combine these, but IMO, resist the temptation. You will eventually have a case where these need to evolve independently. For the Galactus case, imagine that for compliance (or whatever) reason, you need to start soft-deleting users, and you do that via the simple case of an `IsDeleted` flag on the User table. You'll need that flag on the User object on the Galactus side, but you don't want to add this to the API and expose it to clients.

> Edit: This allows you to run API migrations and data migrations independently. Which is crucial for any service that will last longer than 1 year.

Totally agreed here too.


Is it normal for people to just splat their database across the wire? Because that seems pretty terrible. We wrap every API response in a DTO and are deliberate about picking and choosing which fields we want to expose. Anything else sounds like a recipe for bad times.


I think it is both an awful idea and very common. In the early stages your database looks very similar (if not identical) to what you want your API to look like, so it's very tempting to reduce boilerplate by having your API and database objects use the same data structures internally.


Yep, it’s a terrible idea but that’s exactly what frameworks like Rais push you to do.


Yes, but that's not his point. Obviously Galactus has some internal user data structure that is private to it.

But there's also something going over the wire, as a response from Galactus' API, and consumed by one of more other services.

His question was, shouldn't the knowledge of the structure of that be in some type of shared code?


IMO if that structure is autogenerated from the API specs (like protobuf, OpenAPI, ...), it's not needed to share it. Plus you can make it evolve at the pace of each service, if your API keeps backwards compatibility.


> This allows you to run API migrations and data migrations independently. Which is crucial for any service that will last longer than 1 year.

I've felt nervous for years about going all-in with Django Rest Framework, and you've articulated exactly why.


We use it at work. We always define the exact fields we want, and can swap them for SerializerMethodFields if we need to do something different/funky. It works very well, imo.


Yeah, I wish every language had something as nice as DRF. I’ve never had issues with it for a few different multi-year codebases.


Well yeah, but Omega Star still doesn’t support fucking ISO timestamps like they said they would over a month ago. And so Galactus wont be able to find the Birthday Boy provider which mean WingMan wont know how to talk to anybody…


I'm fucking blocked and I'll never find love.


You’ll never know my pain - or Galctus’s pain - you stupid, pathetic little product manager!


> Imagine I want to fetch user data by id from Galactus, the user data service.

Reference: https://www.youtube.com/watch?v=y8OnoxKotPQ


The ideal way this works is the Galactus team publishes a library that consumes their API. They build their service, you then use their library to access it.

But the key here is that their own library still goes through their API, not directly to their database. So that if you don't want to use their library you don't have to.


The thing to watch out for with the client is that the dependencies of the client aren't shared with the server dependencies. Client libraries should be minimal and optional when working with an API.

The failing pattern we've seen at my company is a client library, a server library, and a shared "model" library.

The issue that has happened time and time again is the model library ends up including classes and dependencies that either the server or client needs. This in turn leads to a frustrating experience of dependency wrangling when someone decides to throw in lombok, or kafka, or JavaFx into the model library for that 1 sweat `Pair` class the server is using internally. Or worse, they depend on a different model library because they need that one sweat enum. (Which in turn depends on other model classes, or server classes, or whatever).

If you do a client library, it should depend on practically nothing. The transport library (http client), authentication, and the data marshalling library. Nothing more. It should also be viewed as an example of how to work with your API and not the only way to work with the API. (Don't reinvent graphql... please!)


The way to do this wrong (by forcing the use of the "blessed" client or common platform dependencies, instead of leaving it open to the consumer as you suggest) is discussed in this talk: https://www.microservices.com/talks/dont-build-a-distributed...


Ah, that's exactly what I've been doing, and unconsciously assumed here. I can attest it works very well, hence my confusion.


the whole point of an API is to define an interface in terms of a runtime protocol, invariant to any specific language, code, library, SDK, etc.

in fact the galactus team should actively _not_ publish a library for their API

doing so introduces a source-level coupling between consumers and producer, which the API was (presumably) created specifically to avoid


It's basically a reference implementation. Their library should not do any work other than marshal data. Kind of like how Amazon produces a command line tool to interact with their API, but you don't have to use it. It's just a lot easier.


the problem is that when an producer publishes a library like this, it's very tempting for them to assume that all access will go thru that library, and maybe even that all access will go thru the most recent version of that library

it takes an enormous amount of discipline to ensure that this kind of software is treated as "a reference implementation" rather than the canonical path

if an internal service API is too difficult to use without an API library, then this is a pretty good signal that something is wrong and needs to be fixed, imo


> Should both my service and Galactus have full, individual copies of this data structure?

Yes. I recommend you read "Domain-driven design" by Eric Evans, specially the part explaining the concept of a bounded context.

> Alternatively, if Galactus has a published schema that I refer to, isn't that a shared data structure?

No, that's the interface.

> not only we deduplicate code

You don't. You just needlessly add a constraint for no reason at all, and in the process make your life a mess.




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

Search: