 I'm Jeffrey Mathias. I am a developer at Sengrid. And this is presentation on a collection of practices about future-proofing your third-party services. I say a collection of practices, because I'm not going to tell you they're all best. They're working pretty well for us. I hope you walk out of here today feeling like you've got at least one of these things that you're going to walk out and you're going to say, yeah, that was it. Like, I like that thing. I'm going to implement that. Hopefully there's more than that. But if we can shoot for that, we'll be great. So I work on a team whose job is to refactor legacy code. We have this monolithic PHP app. Yes. And our job is to handle our third-party interactions to go back through and refactor the way that we're interacting with those. So we're pulling them out of the PHP app. We're writing them in a Ruby code base. And since we get to start from scratch, we get to look at how we want to do these things. So some of the work that you're going to see today comes out of working with Pivotal Labs from about a year ago. And then we've built some of our own structures on top of the concepts they gave us and have put together basically a nice path for us to continue to build out the way that we're handling third-party services. We've done this with several of our existing API implementations so far, and it's worked out pretty well. So the first thing is when you are going there, when you're going back to do this refactor or if you're building from the first place, what do you want out of your third-party implementations? And the first thing is you want to keep the way that they're handling their code or the way that the third-party implements their ideas out of the way that you implement your ideas. There shouldn't be much correlation or crossover between that because as soon as you buy into their implementation, you've lost flexibility that we as developers really like to have. The next part is making your third-party behavior predictable. How do we make code predictable? Testing. And so there's testing patterns around this that we'll go over, but the idea ultimately is that if you can, before you deploy, have an idea that things are gonna work well, you definitely have won, right? And then the last part is making your third-party replaceable. And don't get me wrong, I'm perfectly aware that I work for a company that's a third-party for a lot of you. Replaceable is still good, but not just because if you want to get rid of, to replace specifically a third-party, but potentially replace the API. If a company versions up their API, you like a new feature, or you like the way their implementation is, it's cleaner or it's better, it gives you the ability to swap out that implementation or that API without having to go back through and dig through your code or try to understand your existing implementation. So from an abstract, or from a bigger layer, a step back, you can see I'm working really hard to prove that I have a degree in art here. This is about as awesome as a drawing is you're gonna get. You have the billing app, you've got an adapter layer, and then you've got your third-party system. And you'll notice that I say third-party, it's got the internet or something as that dividing line. And the reason that's there is specifically because this doesn't just mean something across the internet, it doesn't mean somebody else's implementation. We are working in a third-party, or sorry, in a service-oriented architecture at this point. And as an example, we have a database abstraction love it or leave it, it is part of our lives for right now. But the fact is, a lot of us don't love it. And we wanna leave our code flexible enough so that when we re-implement that database abstraction, we can swap in internally that database abstraction, that new version of it into our code base without having to, again, sit there and pick apart spaghetti development. So the other thing to notice is that our dependencies are running in one direction here. It's very rare that your third-party will know about your application. And you definitely wanna try and avoid that situation if you can, especially if there's actually options about the way that you implement things. So one of the things, oops, sorry, back on this. So this adapter level, the most important thing to understand about it, the PAMY adapter in this example, is it is just a gem. You probably knew that was coming because I jumped to this slide early. To the point where you're running Bundle gem adapter, you're naming it, it's the generator, it's a standard Ruby gem, but you're not pushing it somewhere. You're actually gonna keep it in your project itself and in your gem file. You're going to actually point specifically to that relative path from your application root and name it there. So fortunately, one of the ways that gem files work is that you get to do that. Hopefully you're using this pattern other places in your code already that you've realized that that ability is there. But in this case, it's tantamount because this is an implementation of a third-party that actually does explain a lot of how you do things. It's not something you want to put out publicly and it gives you the ability also to not have to deal with having an additional repo. It's still part of your existing code base, but it keeps a very strong separation there. Now, that said, I just implemented, suggested doing a gem. A lot of third-party services have gems out there for implementing their work. It's not to say that you can't use them. You just want to wrap that execution inside of your own gem. And I'll get into a little bit more about why that makes sense in a minute. So if there is a third-party SDK out there, leverage it, please. There's no reason to reinvent the wheel, especially if the SDK was written by somebody who works at the company or that third-party and knows what they're doing. By all means, use that existing work. So our gem is basically just a series of service-level objects or service objects. And they're a pretty direct pass-through. So they don't really know about each other unless that's absolutely needed. And they speak on one side about send-grid logic. They understand the language we're talking about on the other side. Their entire purpose is to translate that into the terminology of the third-party system. And so when we initialize our application, and for us, it's a Rails app, but this works with any kind of code base you're working on, we're handing over ad initialization, we're handing over those individual services. And then throughout the code base, what we're doing is referring to the configuration instance because it's a singleton payment method service. So through everywhere else in the code base, it has no idea, there's no reference to the fact that we're talking to the pay me service or the Stripe service or whatever service you're talking to. The initializer is the only place in your main application that you're gonna see that name show up. And this is important because this is one of the things that helps us make it really swappable. So to start this off, one of the most important things you can do, and this applies to any project, is define your vocabulary. Getting everybody on the same page, making sure that they know, or that they're using the same language to describe the same things as you are. It turns out that getting everybody in the same room, especially across the organization, if it's your product owners or your stakeholders and your developers, and having them actually speak about things in the same language, can be a bit of a challenge. You'd be surprised at how much the same word means different things to different people. So stepping back at that very first start and defining that language is by far the first step. And if you don't have access to those other people, at least making your best guess as a team or as a developer, what that is and kind of pushing that back up and saying, hey, this is how we're talking about that is the next step, but getting a group-wide buy-in is definitely better if you can do it. So one of the reasons you're doing that is you're actually helping already, just from using language, using your words, you're managing to keep concepts from your third-party service outside of the way that your company is handling its business or that it's talking about things. An example from our world is when we talk about a package inside at Sengrid in our billing provider, that's a product rate plan. When we talk about an add-on in our language, in our world, in our billing provider, that's a product rate plan. Fortunately, when we talk about a coupon, that's a product rate plan charge, which is so much easier to talk about and very clear and distinct when you're having a conversation. So for us being able to talk about a package, an add-on, a coupon makes it so that we don't have to talk about these really weird nested objects that exist in our billing provider when we're talking to somebody who doesn't understand what that stuff means. And so then our services actually map are the things that translate that for us. So we have a package service, it's called package service, and when it's talking to our billing provider, it's actually talking about, or speaking in the language of product rate plans. And then when we have an add-on service, it's talking to our billing provider about product rate plans. But in a different context, the calls may be the same. And if there's a case, if that is the case, you get an opportunity to maybe dry up your code, use a module. But if it's not, those nuances don't have to live anywhere, back further up your stack, right? They're all sitting in the adapter. And then again, we have the coupon service, which handles the product rate plan charges. So I've talked about this separation with the gem, and we have a couple more steps in the way that we've implemented things, or that we write our code that continue to force this separation. The first one is data transfer objects. These are basically just a codified contract of it's a single parameter that you're gonna pass into a service that sits there in code, lists exactly what that service needs, without us having to do something like implementing an options hash, which you only know what goes in the options hash when you go look at the implementation, and have to kind of dig through that stuff. It gives us the ability to, in one place, say, hey, this is what this service takes. The next part that we're doing is an abstract service where you have basically an interface definition for what your service is gonna look like. So first, let's take a look at one of our DTOs. You'll see it's a relatively small Ruby object, but more importantly, it's got an initializer. When you're passing the things in, what you're saying is, when you're working with this object, or in the context of accounts, the two pieces of information when you're creating account that are necessary, or an account ID, and an account name, you'll notice when in the initializer, the last step is that we've got validation of the attributes. We keep the validation in the DTO itself for a few different reasons. One of them is that gives us the ability to kind of move these around. The validation lives with the use, basically. So then on the implementation, the abstract class side, we have abstract classes that have the method definition, the signature, and all they do is raise. And then you'll look, you'll see we actually have, and so again, a little bit better look at that. You basically, every method in that entire thing is just gonna raise, and say, hey, you haven't implemented me. Which means that we're inheriting from those objects, and then we're rewriting the definitions, but you'll notice that the method signature matches on those, right? So why would we do this? It's actually a little bit of overhead. It means that we actually have to stay on top of this thing and remember, but what it does is it codifies, again, our interface. So between the DTOs and the abstract services, we now have a definition of, if you're gonna go implement a new service, this is what you have to do. This is how you match, or this is how you basically substitute an object in that behaves like a service. Unfortunately, most of the developers I know are human, and they do forget to do things like this. So we've actually got a solution for that that I'll get into in a few minutes. So let's take a step back and look at what that looks like inside of the graphic that I showed you earlier. So we've got our adapter, we've got our billing application, we've got our adapter, and then we've got the third-party service. The two things I just talked about, the DTOs and the account service actually live together inside of another gem that we call an adapter interface gem. And that effectively sits right there and defines the way that you interact with a service. So as long as you're meeting that interface, you're able to swap in a new service without any problems. The adapter interface is actually used by the billing application itself in the gem file so that it's got access to the DTOs so it can instantiate them and pass them in. And then the pay me adapter in this case is also implementing it because it's services are inheriting from those abstract classes. As a side note, we test drive the DTOs because they actually contain logic whereas the abstract services are so razor thin that all they're doing is raising that we leave them alone. I believe very thoroughly in testing but not over testing. So back to the thing about forgetting to actually maintain your abstract services. We have a gem that will go through and actually match everything through testing, specifically a shared RSpec context, will actually match and make sure that the interface, the public instance and the public class methods of your class actually match the thing it's inherited from as long as it's not object. And then also check the arity. So if you're actually not passing in the right number of arguments, it'll call you out on that and give you pretty obvious feedback about it. Now admittedly, this whole concept of an interface is kind of inherently against the DNA of Ruby. And so as a proof of self-awareness, the gem is actually called uptyped. Those who didn't get it, it's definitely a play on uptight. So we know that this is not something you normally wanna do. Not gonna advocate this in a lot of places but for what it's done for us here is it's given us the ability to leave ourselves in code very strong documentation for how to go through and replace the service, implement somebody else's third-party service or again a newer version of an API of the same service. So getting there from a code standpoint, one of the most important things that we do is we test, right? And from an integration, well I'll jump to that in a second, sorry. So one of the most important things when we're doing this is observing all four parts of a test. And for those of you who are thinking there are only three, let's go over them. Well you've got your setup of your data. You've got your expectations and your kickoff and depending on what kind of test you're doing or if you're testing for return objects or side effects commands, then those orders are gonna switch and then you've got your teardown. And especially when you're dealing with a third-party service, your teardown actually becomes really important. So there's the four steps of testing. The next part is never assuming that your data already exists in your third-party. This can, I don't care if you just wrote, if you went, you know, logged into the UI or whatever and just added data to your stuff so that you could go back and test it, that's not an acceptable way, that's not repeatable. You need to, as part of your testing framework or your testing setup, actually make a call out to the third-party to make sure that that data exists. If your third-party has a sandbox and this tends to be more the case when you've got a situation where having test data can actually corrupt the way that you're using your actual customer data, if you've got it, absolutely use the sandbox. Gives you the ability to play a lot more safely through your testing without having to worry about it. That actually was, all right, so I'm just gonna make you guys keep watching this. Okay, so let's take a look at what our payment environment looks like. This is what we call a test environment. This is a pattern that we have that allows us to pretty easily go through and I'll jump to much bigger close-ups on it in a second. Go through and set up our code with creation methods and track that stuff. So if you look in the initialize of our test environment, we have trackers for the things, the IDs of the things that we've made. So we're gonna do that as we go through and then if you look at the last line, there's an insure block and that's the clean up. So when we're done running our code, we've actually got a, in code we've got a way to go back and clean. So our create user, when you look at it, you can see here I'm actually using the pay me user gem and then we're tracking that ID, right? And then when we're done, we go back through and we clean. So after our test is done, we actually just go delete the data back in our third party system again. So that gives us, without having to think about it, it's just there. Once you've built that environment, your cleanup exists. You don't have to think about it, which means that you're not littering your third party with data. We actually have legacy code bases that don't show the same kind of respect and so you can potentially go make something, stop in the middle of your test and you wanna go look at what that data looks like in the third party sandbox. And by the time you log in, it turns out there's 300 different accounts that have been made since when you started running your test and when you've gotten there because again our legacy code isn't being as respectful. It's not cleaning up behind itself and that makes developing against it a little less pleasant. In fact, a whole lot less pleasant. So don't be that person. Nobody likes that person. So one of the things though about hitting your third party service regularly, especially if you're test driving is that it's slow. So for development purposes, it's really nice to have some way to speed things up, to have some sort of stub sitting there pretending to be your service. There's two different ways to go about it. I'm not gonna make a strong argument for one way or the other. It turns out this is a giant can of worms that didn't know I was getting myself into. The last time I talked to this about somebody, or I talked to somebody about this concept, but you can either make a fake service that returns calls just like your API would. So fake data from your API or you can record VCR sessions that basically it sits there and records all the HTTP interactions and do a YAML file and just replays it so that if you make the same calls, it returns the same response in that environment. So from a testing standpoint, when we unit test, we're actually our unit tests are causing our services to call out to that third party service or to that stub because that is the point of that service. So making sure that it's just making those calls doesn't quite get us where we need and additionally because in most situations, you're actually looking at HTTP interactions. It's really hard to stub that without using something like fake web where you've actually just given yourself an unnecessary blocker and unnecessary definition to work against that you then have to maintain pretty hard. So if you're gonna look at the way that our environment looks like, I'm sorry, one of our tests using our environment, if you're not using VCR, if you're using a fake, you won't necessarily have that line but you see that we've instantiated an instance of our pay me environment. We're passing that into a scoped variable. We're calling our creation methods against that. So the pay me create and remember that's gonna ultimately probably use the gem provided by whatever our services. So we're staging all of our data there and then we're just kicking it off and that's actually the same call that anything using your service is gonna look like, right? We've got our expectations and then by the time you hit that end, you've actually jumped back into your environment and you're running through that clean again. And the reason we use and ensure there is specifically if something goes wrong during the testing, you're way more likely to get that cleaned up. Something has to go pretty bad, really fall apart for it not to run that clean block after your test. So we do the same kind of thing for integration specs but instead of, but we make sure that the test environment, we actually pass that in at the same place where we do the dependency injection on our initialization so that the setup for the integration test doesn't know what adapter it's talking to either. So in the case of our environment, that may mean that we've actually written an integration level wrapper around that. So it's using send grid specific language at that level in the integration level. Depending on how you've written your environment, if you've written it so that the environment is actually using send grid language as part of the creation methods, then you don't have to send grid, I mean your third party app language, then you don't have to worry about that so much. But just remember, this is a place where you have to still maintain that the app doesn't know what service it's talking to. As soon as you've done that, you've opened the door to accidentally have some of that implementation bleed back through. So some of the things that come across are to remember while you're working through this is that your services, if you're gonna stub it, whether you're doing a fake service or VCR, you must maintain it. In the case of the fake service, that means making sure that you have a way to consistently check that your fake or your stub is behaving the same way that the third party service should. In the case of VCR, it means that you need to be able to make sure that your cassettes get deleted with some sort of regularity and re-record it again. And the whole point is you need to be able to write these things so that you can actually remove your VCR, remove your fake service, still hit your actual third party integration in your third party service, and have your tests work correctly. As part of that, and because you need to be able to hit those services themselves, you have to make sure that your tests include creating everything and tearing everything down again. And then if you're working in CI, I highly recommend making it so that your tests, whether it's the main way that they're running or if you've got a side version that ignores it, does it, skips the fake and actually hits the third party service. And the reason I say that is from our experience, we've had it where we've implemented a third party service according to, it was a mixture of reading documentation and trying things, which is how we usually develop, right? We don't like to actually read documentation, we just like to write code and see if it works. Well, it turns out we'd written some code that worked, but it wasn't actually part of the published API. So we'd set ourselves up for a failure there because when that service went through and cleaned up their API, I'm sorry, cleaned up their code, they're like, oh yeah, this is a thing, but it's not something we've promised will work. So they changed their API's behavior without changing the version of their API specifically because that call was never part of the published API. Having something like this where we actually had CI regularly running would have given us, it would have been a canary in the coal mine. It would have given us an opportunity to know a little sooner that we'd done something stupid and give us a chance to fix it before we started experiencing those same situations in production itself. And especially because most places when they do that sort of thing, they end up with that kind of behavior can end up in their sandbox first because that's a good way to implement a new thing. Hey, see if we start getting complaints off the sandbox or if we just broke the sandbox. Oh good, now we've had a kind of an integration test without having to worry about it. Let's push that into production. So it would have given us an opportunity to catch that. So let's step back and look at the goals that we had when we were talking about implementing the third, a third party implementation. And one of them was keeping the third party logic out of our logic, right? And so again, this pay me adapter, the whole point of it is to draw a box, a very clear box around where that separation lies. The next one is keeping your third party behavior predictable. And again, that ties back into testing and finding your points where you're testing and doing it regularly. But one of the most key components about keeping it predictable is making sure that that stub or that VCR that you've gotten the place of speeding up your tests can be removed and the code can actually hit the service itself and behave correctly. And then we were talking about the replaceable part and that's where the DTOs and the abstract services come into play. Again, with those two things defined very well or if as long as you maintain that behavior or that code structure, you've given yourself the ability, very clear documentation of it. Step by step, here are the methods that I need to create or implement in order to get a new service into place or to be able to replace a service. And then the DTO tells you exactly what the data needs to look like going into that service. So additionally, you've got flexibility. It doesn't just make your code more flexible. It gives your ability the opportunity to be more flexible as well. One of the things that we discovered is that how closely integrated our billing provider was specifically. We've got lots of providers but this is the one that I think it has the most bleed back in our original implementation. A lot of the ways that that was implemented have set in concrete or set in stone the ways that things had to be dealt with inside of Sengrid and moving that relationship out, pushing that back, all of the implementation of that third party into an adapter level gives us the ability to be more flexible not just on how our code is written but how our business actually operates. It gives us a chance to add more products or more kinds of products, for example, without having to worry about specifically how that would map through our current implementation. And the other thing that's really huge about this is portability. So having the DTO and the service itself defined gives you the ability to actually, so right now we are, for example, we're coming in, we're using these as HTTP calls to a service. So you call into the service, you pass in those things and it talks to the third party implementation. Well, we're talking about the possibility of moving to something that's more producer-consumer message-based like Kafka in the future. So instead of having to implement something significant around the way that we're using the service, we actually have all of our validation sitting in the DTO as we have a service that implements that third party. So what we can do is go write a Kafka consumer that actually looks for the right messages and calls that service out. It means the amount of code that we have to write to implement it in a totally new way is way smaller because the stuff that's specific to that third party already lives in a very defined, easy to use interface. So that interface actually, so that containerizing of the way that you deal with your services actually works two ways. It allows you to swap up your use of it and it also allows you to replace whatever service you're using. So again, the biggest step about all of this stuff is defining your internal vocabulary. I can't emphasize enough how many communication problems you can end up having or how much work can be lost because two people think they're talking about the same thing. And that's not just about third party abstractions. That's about every time you're dealing with code, right? So, and humans on the side of it. So then the next step after you define it is to create the adapter interface, to find that context and how you use it. And one of the advantages of writing the adapter interface as a separate from the implementation, aside from creating documentation that you can later refer to, is you've also pulled away the context in which you're using that specific thing. So you're going to define a billing API or a gem, the interface for that class or for that service, but what you're really doing, if you pull away the actual implementation you're using, you're thinking, okay, how would I use this in a generic sense? By writing the abstract class as opposed to just defining those methods on your implementation itself, it gives you the ability to remember to keep it generic. It helps as a developer, you remember that you're not always potentially gonna be using it in that same context. And especially if it's an API that you either know you don't like or that you are not sure about you're trying it out, giving yourself that flexibility can be huge. So then the next step is create yourself an actual adapter gem that implements that interface and include uptypes so that it will help you remember to maintain your abstract interfaces. Create a test environment so you are playing well, being a good citizen as far as keeping your third-party sandbox clean. And then stub your third-party stuff. Keep your tests going quickly. Anytime that you feel like your tests are getting in the way of you writing code, or at least more than just a few minutes, then you've gotta figure out a way to speed that back up. Otherwise you're gonna stop writing so many tests. And then you're gonna lose that predictability of your service. Not to mention you might lose your Ruby card because I'm pretty sure that's how we do things, right? We test. But when you do that, you've gotta make sure that you maintain your stubs or your fakes, right? So that VCR cassette's gotta be deletable. That stubbed service or that fake service has to be, has to be maintained. You've gotta continue to check it. Now I was talking to somebody earlier today who says when they write implementation for their stuff and hand out a Ruby gem, one of the things they're doing is they're providing a fake service with it. That is super awesome. That is probably not gonna happen much. So you're probably gonna be looking at a fake service that you're implementing, which means that you've got the responsibility of making sure that happens. So with that, I just wanna say thanks. That was future proofing your third party integrations. I wanna throw some things out real fast to my team, the portal team, and my big brother Carl for getting me into coding. And as far as questions go, I can take some now, I've got time. Additionally, can you guys stand up? If you have questions, you can hit them up. They've been working in this stuff too and they helped to find these things. So those are the local Sengritters here. Well, if anybody else who wants to hit me up privately, you're more than welcome to, aside from that. Thanks.