As software engineers we are always trying to be more productive, to deliver better code, and to have faster development feedback. In this talk, we'll explore how functional programming, tests and hexagonal architecture can perform great together in order to support a maintainable codebase for hundreds of engineers and services. Diving deeper on how we can leverage hexagonal architecture with dependency rejection in order to decouple decisions from effects, resulting in a code that is easier to reason, compose and test. The codebase is not the only one that takes advantages from that, but also the developers. It helps everyone feel more comfortable and engaged about maintaining good practices.
Building a Sustainable Codebase with FP
Hi folks today. I'm here to talk about how you can build a sustainable architecture. First. I'm going to present myself. My name is Karina pascalicampus. I'm from Brazil. I'm a software engineer that works at briza and here you can follow me on my social networks. And I love books cough and and running. So let's go to the presentation now. So the main tops that I'm going to talk about today are functional programming tests in the exagonal architecture. So let's start with functional programming. We know that functional programming is about pure functions, right? So given it's inputs. We are always going to return the same output and we're not going to have side effects. So when we talk about side effects, it could be like saving for database and an email so anything like like that. But it's important to have in mind that we will also need to deal with improved functions because we know that in order to have like a useful code base. We need to have imperfections. We our programs needs to have input and output we want to be able to interact with it and those interactions always happening Perfections. So after we write through a database, for example, we're going to change it state. So when we are going to read from the database after saving to it the results going to be different and how we're going to handle that. So we need to talk about side effects when we are like functional programmings. We love to talk about the pure things. But we also need to talk about the facts and that's a really important topic to be discuss it how to deal with them. So we need to be really careful about the Imperial functions because they change state so if I ran something today, Tomorrow the results can be different. So this is the hard part to manage, but we need to manage that so that's the reason we need to carefully structure our database to not mess with it. So here's an example imagine that you have improved function inside appearance. So you no longer have a Perfection. This is why when it should be careful about stretching our code. We want to maximize the pre functions we have so we cannot call and improve function site approvement because we have just lost it. So we need to separate what we built. We need the effects from the pure functions. The effects can in fact everything and our goal here is to maximize our functional programming and to minimize and isolate the improve functions the imperative code. So what we need to have in mind always is that if a function could be pure and it's not we are doing to wrong. So let's try to refactor this. Another part of the powerful thing about functional programming is when we talk about immutability versus motability. So let's start with the image that we we have in the left. So we have a book with many pages. This book's day identity when you want to change something in the draw you just turn the page and draw nothing in the end. So the act of turning Pages represent the state of the draw during time. So if you stop on a page, you can observe the state in a certain point of time, but if we look now to the right, we have a mutable example where the book is only one page page if you want to make a change we need to raise stuff and we draw things. So we have just lost a notion of time here and state and identity are now Blended. So when we talk about like immutability here, we have the animation, you know, all the steps your draw has been through to get to the end. You know the whole story So when talking about pure functions plus immutability we are going to have functions that are easy to reason and easier to the book. This is what we want because with prefer functions, you know that given an input that puts always the same so it's really easy to reason about it and also with motability you always have no pages and when you change something so you don't need to go deep in the cold to understand which function is mutating the page. So we want to be mindful about the things we want that's pure functions any metability When developing so we can seek them because this combo is going to give developers a lot of power and speeds. Nice, so when programming is functional link, which we want to focus on data calculations functions and not classes interfaces in barley plate code. So the intent of Records clear and you can try new things faster. So what we want here is be as functional as we can to be able to move faster and with confidence. Now, let's talk a little bit about tests because if you want a sustainable code base, we need tests and test are important because they give developers confidence to change your trick extend or change our code. So when trying to understand for example, what we didn't write we can check the test to understand what was expected from that quote when it was reading. So it's also like documentation if we change quote we need to change tests because otherwise it would get stale but they don't because it's alive documentation. But how do we know and check if our progress doing what it's expected to do? How can we check the quality of this feedback and here I'm not going to talk about 100% coverage. So let's start with a currency if we are tests fail. Can we determine exactly which version of the code has failed? How long do you take to know where it failed? So what we want here is functions that are really granular. So as granular as it is the more currency you can get so it's way faster if you have like small functions with no dependencies from the outside world, so that remember pure functions. Also about speeds the first we find a test failing the sooner we can identify the problem and smaller their impact will be so what we need here is tests that are simple to write in to maintain and also that are fast to run if you have a lot of complex code with a lot of dependencies from front outside world. It's going to take a lot for us to human thing this this tests. And also we need tests that are reliable. We need to trust the outcome of our our tests if the outcome changed from one execution to another if we don't change the code the configs already dependencies. Why do you would we keep this kind of tests? Because we don't trust them. So it makes no sense to keep them. And here we are usually talking like about entrance tests. so entering thefts are a problem because and we don't you don't walk anything. You're really safe stuff to the database. It's really common for them to failure for like obscure reasons, like ladency garbage collection and synchron operation. So we need to think in a different way. So here we need to think on a little a little bit larger. We know that unit tests over the function levels integration test over the flows and we need to guarantee that conversation between Services is working. So usually we use and transfer that but we can just think a little bit larger and test the conversation between two services at a time. For example, so here use construct test. So every contract test going to have a Provider Services that consumer serves see and for each communication between them we need to differ a clear interface called the contract. So now we have like automate test that the shirt the data generated by the provider can be consumed by the consumer and if you change anything in the contracts going to break so it's important here to have your mind. This is not a one-tr. Ladysmith with the interest and Trend test but most of their heard that in 200 catch those tests can also catch them. So with that we can have a currency speed and reliability and that's what we we want. So now I'm going to talk about how to design a record because okay. We have the functional program. We have the test but how to design the cultural leverage this what we want this High mind ability with low Tech debt because we want applications that I used to give me in this and have low Tech that We need them to be simple and easy to work on and this is hard because many ability is a long-term concept because in the beginning it's really is your project has just started with a few lines of code three people working on it. So it's just a couple of dependencies. But as soon as your teen in your code grows things get harder and the book can result in a whole architecture. So that's the reason an issue think about maintability since the beginning. What we are going to Thursday here. So the first attempt we we had was try to use executor architecture. So here we have just simple executor texture that domain model where we have the implementation of our our domain. So here we're going to have the pure functions and the multiple data. That's why it's green. After that, we have like the second layer that there's where we have the controller that's responsible for connecting the parts with the adapters and the logic the domain so here is all about coordination. But here we are red have some functions that are impure right and some multiple data, so it's not all of them, but some this whites yellow Any deter layer here is where I have it's called the io or parts and here we interactive external word. So here we're going to have call for other services. We're going to have calls to the database to third party apis. So here it's okay to have impure function and data mutation. We expect that in there IO layer. So what we want with executor architecture is to be able to separate the business logic from the I/O so we want to structure the code in a way that we protect the domain from the technical stuff. So we can Define what are the business rules. What are the educational rules? And what's the infra code? And how do you tell we do it tests in these are texture so we want to test an isolation with mocking steps because we don't want to rely on external database or external Apex. We really want to Mock and step things for that. We use dependency injection, right so that the penis is here are going to be passed as arguments. So when we're running production because the real database and forecast, for example, we can use a hardcoded file. But what was the first Discover it is architecture. So remember that in executor architecture, we have our improved fractions in the boundaries right in the I/O layer the ones that generate side effects. So the exit request the emails the message everything is there so it's nice to realize that in this architecture. We can isolate and control the side effects in this layer. So we have the boundaries here as we as we can see so let's talk about this concept one does isolation means so isolation is when all the info are function has from the outside world is pass it through arguments so we can come to the conclusion that every pure function is isolated. So up your functions are subset of isolated functions, not all isolated functions, they're pure but all the parents are as related and isolation is the dough of encapsulation. So encapsulation means that the object has a state and outside where those nothing about it unless it made explicitly available to it together and centers. So isolation is a functionals nothing about this Center word unless it's important on it. So it's the door right and isolated functions are really it's your test and give him for free when using functional programming but in object oriented programming there are problem because of the calculation principle, so When developing with object oriented code that's going to be tested. If you're finding intersection between isolation and encapsulation and that's really hard because it's way easier to work in a subset then an intersection, right? So it's hard to maintain the balance between isolation and encapsulation when dealing with object oriented programming but with functional programming, it's no problem at all. The minimum expected from a good functional side is you have pure functions and they are subset of isolated ones. So if you're doing pure you are doing it like isolated and you get the easiness of the test for free. And we want to have as many prefer functions as we can to get all of those benefits because we know that if you have pure function that causes an imperial it would be Imperial also. So why exactly now architecture in FB is natural so one of the language that help to understand that's asking because Haskell is a functional language that expects all functions should be pure and so when you're writing Podi has killed the compiler helps, so if that function checking if that function experi or not because if you wrote an improved function and forgot to declared's type is I oh it's going through. So in order to have as many Perfections as you can you push the real function to the boundaries so they improve functions can call the chain of pure functions, but a pure function never can call an imperial. Otherwise you get a compiler. So we are going to have a show of empirical codes. So did this remind you of external texture? So this is why I help Haskell is a language that helps to discover that because it expects all function. Be pure and so good good functional program design can be and is part and adapters and this is a great discovery. But remember that at the beginning I said that we want to leverage functional programming should maximum. And as I showed you we still have controllers with empirical difficult and side effects. So, how can we why do we don't want that here? I'm going to show you an example. Imagine that I have this function call that's called block and I'm passing a card a database and a producer so here. We when I look at this function call. I have no idea. What's the output it could be true. And if it's true, what does that mean? Okay, let's so enter the function traction to understand that so as you can see I have the card. I'm doing some Logic on it. And then I'm calling the update in the in the database. And after that I'm calling the producer card status change it here, right? So here I needed to enter the function to understand what's doing and I'm not returning anything here. So I have no idea what happens before entering the function. It's also harder to compose when I have imperative quotes. So here imagine. I'm calling block for 10 cards imagine that at the third card it failed. So now I have called the big update the the producer should send message but only for three cards so I cannot just re-run stuff because the kids effects how they handle with this composition. So it's hard because I'm just doing the things. Yeah on the phone fly, right? And what we have here is not the permit that we want. We have a permit that we have more integration than unit tests because as you can see in my controller, I have those kind of functions that are calling to the B that are calling the the producer. So this is not what we want. Right? We those functions are not pure the tests are not that easy and also the main entability is not going to be good. So let's try another approach for that. What about if you exagon architecture with dependency rejection, so what does that mean that when should a couple that decisions from the facts when the pudding tent from the execution so the same function but now when I call block I only pass the card I'm not passing the database or the producer and you as you can see I have a return in my return is there is that oh is an object that has a key called the facts data and it has its value in a key called a text message and it has value. So now I know what the function does but sure I can enter to Understand and as you can see, I'm just calling the mess the logic here. That's a pure function. Right and I'm just creating what I want to do. What's my decision, right? And I want to update the card and I have the data that I need for that and also when speaking a message I have the data that I need for that but I'm not doing anything this feeling change pure so I can test using unit test. For example, it's really true reason about also it's going to be easy to compose because before we were iterating and say things to the database in producing message, but now I'm not doing that. I'm just creating an object. We're making the 10 cards that's going to have an array with the 10 cards and what I want to do with them and I'm just to try to do that only in a single in a single time not during that during the process that would be harder to to recover from that. So what we have here is that now we have Atomic operations and we have data as first class seats and that's really powerful. But because as you could see we saw the date of going because we're returning an object with what we want to do before we are just doing stuff and we No idea what was happening. And when I have data is first class citizens, we get this power to better understand what our codes doing. And also now our controllers are pure we're only doing the side effects in the io that's what we wanted since the beginning, but we were not able to do with the dependency injection. So now we are only running the Imperial code in the io layer and we have the controllers pure. This is why it's all so green. So that's it. We have controllers without side effects. And now we became more expressive our code tell us better the the story of what we are doing. So as we what we wanted for having like a sustainable code base was to have as much pure functions as we can. We did that it was to be able to have tests that are that I have accuracy because we need we have a lot of pure functions we are able to have May notability because when we have unit tests is way easier to maintain the integration. That's right and also with immutability. It's also easier to understand stuff is also way easier to the book. So with this architecture we're able to get all of those things. So that's it. Thanks for for attention. Here are again, my social networks and also briza. The company that I'm working for is hiring. You can also follow me Twitter so I can tweet this link. It's going to be easier for for you, but that's it folks. Thanks for your attention.