Johnny Austin

Finding Your Abstraction Sweet Spot

Many would argue there are only two hard problems in software: naming and cache invalidation. I’d argue there’s a third problem - abstractions. Whether you’re implementing an API for devs outside of your organization or creating a reusable library for devs on your team, creating the right level of abstraction is difficult. You have to balance flexibility with the ease-of-use. The correct choice is often a function of time constraints, compromise, and trial & error. I’ll talk about how to navigate these issues more efficiently.

Portrait photo of Johnny Austin

Transcript

JOHNNY: Can everyone hear me okay? Perfect. Cool. So, my name is Johnny. I'm a staff engineer in Washington, D.C.

Working at a company called morning consult. And today I want to talk to you about a very bikesheddyworthy topic, which is finding your abstractions sweet spot.

So, moving right along, this begs the question: What exactly is an abstraction? Before we get into that, though, I want to talk just a little bit about simplicity and complexity. So, we've all heard the mantra: Keep it simple, stupid. And truth be told, other than being a bit rude, I think it's a bit misguided as well.

The fact of the matter is, simple doesn't really scale. It's really easy to get that hello world up and going and, you know, the first couple months of the thing you're trying to build, it's really great. But if you have any amount of success, things get really big, really complex really quickly.

So, how do you embrace this idea of simplicity while accounting for inevitable complexity, right? So, we don't want KISS. What I tend to think about is elegance. Right? I think this is what we really mean when we say to keep things simple. Right? Elegance I define as complexity expressed simply. And the goto metaphor I tend to use is if you think about all of our reality, all the space and all that, it exists, right? There are trillions and trillions of protons and photons whatnot in the universe.

They come together to form everything you see around you, stars and planets and meteors. And even you and I, living organisms. That's a hugely complex system.

But it's really interesting that all of reality as we understand it today can also be expressed in the language of mathematics. That's an incredibly elegant thing. We don't know why. There's a big argument in, you know, physics about, you know, whether math is real, whether it's not.

You know? Do you discover math? Or do you invent it? That's a completely different talk out of the realm of my particular expertise. But I think it's really elegant, nonetheless.

Don't keep it simple. Keep it elegant. So, with that said, what is an abstraction? In this context, I like to think about an abstraction as the degree to which complexity is encapsulated. And going a little bit deeper here, I like to think about things in terms of strong and weak abstractions.

When you think about a strong abstraction, it encapsulates a lot of complexity. And this is not necessarily a good thing. Right?

There are many times when you don't necessarily want to hide a ton of complexity. When you want to maximize the amount of flexibility you give people who consume your APIs. And we'll talk a little bit about that later. Conversely, a weak abstraction encapsulates very little complexity.

And pejorative to the term weak itself, this is not necessarily a bad thing. Sometimes you actually need people to be able to get down to the nuts and bolts of what you're providing them because that's what they need in order to be able to build something on top of your platform.

Games seem to be really popular this year at JSConf. And I swear I put this up before I saw all the other games that were available. So, this is gonna be called guess the API. So, essentially the way it works, I'm gonna show you a small code snippet and then you're going to use context clues to figure out which famous JavaScript API invokes this implementation. So, we'll go through an example real quick.

If I were to show you this block of code, you'd look at it, pick up some context clues. You know, you see the word "App," create application, all this good stuff. Does anyone have any idea where this block of code comes from? Oh, if you have any idea  if you wrote this code, you are not allowed to participate. And if you cheat, may your life be filled with misery forever.

[ Laughter ]

Anyone? Yes?

AUDIENCE: [ Away from microphone ]

JOHNNY: Express application. But specifically, which function? That's all right. You were right. It's basically when you invoke express itself at the root, right? So, that's the only example I have.

The others aren't going to be that easy. I picked stuff without revealing docs, but you get the point, right? All right. Ready? Here's the next one.

Picking up context clues. There's some event stuff in here, some listener stuff. Anyone want to take a stab? Event on, that's pretty good. Yeah.

This is whenever anyone in Node uses an event emitter and calls a .on event, that's what's invoked. Most notably in strings, but also when you're creating HTTP servers and things like that.

What about this one? This one might be a little bit easy. This is props in a particular library all over the place. React what? React dot? Right. React.createelement. So, somebody's mad.

So, what's the point here? The point is to illustrate that, you know, with some context clues you can kind of get an idea of what's going on. But the fact of the matter is, there's like nothing wrong with this code itself. You know, unless you hate semicolons or whatever. But the idea is that I wanted to illustrate the entropy that's involved in creating the abstractions that you use every day.

Someone had to write so you can essentially write this. This doesn't just apply to code either. When you think about abstractions and what they're useful for, they're all over the place. Most notably, when we think about infrastructure, right? This is a big thing that's been happening within the industry really for about the last ten years. A lot of innovation has been happening.

But really the last few years is when things really heated up. And I want to talk a bit about going up the abstraction chain to kind of figure out  to kind of illustrate exactly why these things are important and the big milestones that we've seen. So, for those of us who were around when you needed to essentially have access to a physical server to actually deploy something.

Whether it's a website or whatever you. This was a very imperative workflow to actually get your stuff deployed, right? Needed to know a lot of things. You needed to know where the server was. You needed to know security considerables to get into the server.

You needed to know where on the server to put your deployable artifacts. It was a very, very imperative thing. And when say you need to know where it was, sometimes that was just in your bedroom, right?

So, moving forward a little bit, someone had a great idea to just use virtual machines. The idea is like, forget the hardware, but I do need a computer. And so, began this whole revolution of computer emulation. A whole level of abstraction that was really a game changer in terms of portability.

For the first time, really the thing on which your application rely wasn't necessarily the host operating system on which your stack actually ran. And this was huge.

So, moving up a little bit, we talk about containers a bit, right? Forget the computer, all I really need are compute resources, right? This is about compute resource emulation, right? I don't need a UI. I don't need a start menu, that's kind of dumb. What I really need is a file system, CPU and, you know, memory. Other stuff.

And that's what I need to run my application. And so, the abstraction gets a bit stronger.

Platform as a service. This was really great as well because it allowed you to not even have to think about what your application was running on. That was irrelevant most of the time, right? And there are caveats here as well. But the idea is, the only thing you ship is an application.

And someone else worries about everything else. That was your interface to your deployments. Right? Forget the compute resources, mostly, here's my app running.

Which brings us to the current revolution, which is serverless, right? Forget the application and the computer. I'm gonna ship business logic. I want you to take care of essentially everything else. And that's kind of where we are now. And you see this continuously evolving and getting more and more strong as an abstraction.

And you see where the benefits are for getting things up and running really closely. But you also encounter situations where these strong abstractions don't really make sense. Other people really need to use other things.

So, what can go wrong? Well, short answer is: A lot. So, breaking changes. When you're not focusing on thinking about this up front, at least to some extent, you really run the risk of having a lot of breaking changes occur when you are developing your software, right? And you end up with nonbackwards compatibility software which forces you to essentially abandon users or have a lot of support over a long time. And truth be told, too many breaking changes is gonna prevent you from being able to sell to large enterprise customers.

They're very risk averse. If you think about customers like the government or people in highly regulated industries like the finance industry or whatnot, they will not use your software if it's not stable.

Tech debt. You could go the other direction, right? If you decide to keep those major versions in place, but you still need to make a lot of changes, you're going to have to do a lot of contorting in order to be able to maintain that backward compatibility. We've all worked in those codebases where we didn't want to break anything or we absolutely couldn't break anything, but we needed to make fixes. This created a lot of tech debt in the weight and there was no clear path to actually cleaning it up without essentially a complete refactor or rewrite.

User confusion. So, this one is a bit less obvious. But if you get your abstraction wrong, or if it's not ideal, there's gonna be a lot of confusion within the communities that consume your software. I remember back when the AngularJS 1 landed, and it was absolutely a game change fetor frontend environment. And I remember they demonstrated the twoway data binding and it was like amazing.

And this was great. But then something happened. I guess about 24 months had gone by.

Companies had started adopting AngularJS as like, you know, something that was real. And then people actually started building really big, robust web applications using AngularJS. And then what happens is, you get to a place where you need to make a deployment or you're getting close to the end. But you have some last-minute requirement changes.

It happens all the time. But you've architected your application in such a way that your directives need to communicate with each other in a very elegant way. But you didn't architect, you didn't account for this, right? This is before we had sort, you know  before redux was really a thing and we had like really mature sort of like state management solutions on the frontend.

So, what would happen is people would try to get two directives to talk to each other very quickly. And instead of having to refactor the way the communication messaging happened, they would adopt what I refer to as the spray and pray architecture. And you take advantage of Angular's dependency injection. You inject root scope and you do a rootscope.broadcast and throw out data out into the either a hope something receives.

I know some of you have done it. You look guilty, heads down.

I know, I have been there. This was not Angular's fault. This was the abstraction we were given in order to build our applications. And it just so happen when is trying to scale out he's applications, this was the challenges we ran into. In fact, a lot of these issues were some of the early inspirations for, you know, architect for libraries like React and Vue, et cetera.

And these are the types of things that can happen, right? This is not bashing Angular 1.x. I'm a big fan. I don't think it deserves half the flak it received. So, the reason you're all here. Where is my sweet spot? How can I figure out where I need to be placing my abstractions such that it benefits both myself and my users? So, opinions, opinions, opinions, warning.

So, the big thing I think everyone should really think about is flexibility versus ease of use. Right? This is the big one. This is going to be your ultimate tradeoff. The idea is the more flexible you are, you're gonna tend to sacrifice some ease of use, right? This is gonna be your weaker abstractions with flexibility. This is gonna be your low-level stuff.

This is gonna be your WebGLs of the world which are really great. But someone like me, not a 3D programmer, I need something higher level. I need the 3JSs of the world. Or higher than that, truth be told.

Versus ease of use, right? You are a 3D programmer and you really know what you're doing, a lot of the helpers out there are just gonna get in your way more than anything else. Maybe that's not what you need. So, what you have to do is figure out where your users are and make sure you're meeting them where they are. Similar to that, you have to consider your audience, right? Who are you building for? Is this internal or external? Right? Internal you can get away with a lot of stuff.

If it's external and people are gonna be using this either as a product or maybe just an open source tool, you need to make sure that you have that correct understanding. What are people using your stuff to build with? You can't control that, but it may come into play. This is when you really get intimate with the users and figure out what they're doing.

Platforms. This is a good one as well. Some platforms and languages lend themselves better to certain levels of abstraction. Practitioners of a language might regularly work on a certain level, right? JavaScript developers generally want objects and strings, that sort of thing.

Maybe some arrays. Go Lang, they want buffers and errors, have you looked at go code? Rust developers want jobs.

Just kidding. That's a troll. I love Rust. It's a great language.

Life span. This is a big one as well. Right? This is very similar to, you know, the breaking changes. If you're very up front with your audience and your users about the type of timeline they can expect for support, this is gonna inform the level at which you actually, you know, write your software. That's gonna be really important.

So, coming to wrap things up a little bit. You want to make sure you focus on elegance. And not just simplicity. Simplicity won't scale. No one's gonna buy anything that's simple, right? Because anyone can reproduce your work.

If you're gonna be successful, you're gonna inevitably head towards complexity. The best thing downing is account for it in a way that allows you to build a competitive product. But also ensures that your users are happy.

Think in terms of weak or strong abstractions and when is the right time to use either one. Sometimes you need to support the whole spectrum, right? That's something that's really important. That's gonna take a lot of resources, it's gonna take a lot of time. Maybe you start at one end of the spectrum and you end up on the other.

This is a decision that you have to make. So, remember the tradeoffs of flexibility and ease of use.

Very similar to the previous point. There are real tradeoffs here. And you need to make them as close to up front as you can. But not necessarily completely.

Think about the consequences of getting it wrong. Frequent breaking changes, tech debt, user confusion, there's nothing you can do to avoid all of these things, unfortunately. Part of the planning up front is to make sure that you know that you're gonna accumulate that tech debt. So, just have a plan to kind of pay it down, right? Or if you're gonna have frequent breaking changes, maybe you want to launch in a beta for a while and make sure people understand that up front. If you know this is gonna be crazy and maybe people are gonna be a bit confused, focus more on developer reach out and make sure people know exactly how to use your software.

And think about your audience. you can have the best product and the best software in the world. But if no one is going to buy it and no one wants to use it, then you're essentially dead in the water. And with that, thanks.