Learn Building Modern Go applicationshttps://threedots.tech/Recent content on Learn Building Modern Go applicationsHugo -- gohugo.ioen-usThe Distributed Monolith Trap (And How to Escape It)https://threedots.tech/episode/the-distributed-monolith-trap/Wed, 28 Jan 2026 16:00:00 +0000https://threedots.tech/episode/the-distributed-monolith-trap/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Start with a monolith</strong> - don&rsquo;t create microservices from day one; the overhead isn&rsquo;t worth it until you have real pain points</li> <li><strong>Microservices solve human problems, not just technical ones</strong> - they help teams work independently, not just scale systems</li> <li><strong>Tight coupling over HTTP is still tight coupling</strong> - separating services doesn&rsquo;t automatically give you isolation</li> <li><strong>Watch for signals</strong> - lots of calls between services or frequent changes across multiple services suggest wrong boundaries</li> <li><strong>Sometimes joining services is the answer</strong> - merging tightly coupled microservices back together can be the right move</li> </ul> <h2 id="introduction">Introduction</h2> <p>We talk about splitting software: when to do it, how to avoid common pitfalls, and why microservices aren&rsquo;t always the answer. We share stories from past projects where we jumped on the microservices hype train without thinking through the real problems we were trying to solve. The key insight is that splitting software is more about organizing teams and maintaining good boundaries than about technology.</p> <h2 id="notes">Notes</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fthe-domain-engineer%2F" target="_blank">The Domain Engineer training</a> - includes a complex modular monolith example</li> <li>Modular monolith approach - starting with a well-structured monolith that can be split later if needed</li> <li>Event Storming - a technique for discovering system boundaries by mapping domain events</li> <li>Conway&rsquo;s Law - the idea that systems mirror organizational structure</li> <li>Team Topologies book by Matthew Skelton and Manuel Pais - recommended for understanding how team structure affects software design</li> <li>Reverse Conway Maneuver - designing organizational structure first, then letting software follow</li> <li>Service mesh tooling (like Istio) - for observability between microservices</li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>This aspect of microservices I think can be also misleading. Because it seems like you have these separate things. You see the deployments, you see the different repositories. For an outsider, you can think they are separate beings that can be changed independently of each other. It can be a trap.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>You can say that microservices are maybe solving a bit of technical problems, but it&rsquo;s mostly solving the human problem.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>So if you consider splitting your project, I would start with this question: if you even need to do it. Because you often see people struggling or hear them asking, I&rsquo;m starting a new project, kicking off some application, and what services do I need to create? So probably at this stage, you don&rsquo;t need to consider this at all.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>I think it&rsquo;s also nicely visible if you take your service mesh tooling, and probably will see that between some microservices will have a lot of calls going back and forth. And it&rsquo;s usually the sign that maybe those two services shouldn&rsquo;t be separate.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>The more developers you add to the project, the more complicated it gets. Because this is also important factor. If you are a single person working on software like this, it will be fine probably because you can keep the context in your head. You will refactor to whatever you want. But if you have like 20 people working on it, it starts getting more complicated.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>But there&rsquo;s one good thing about microservices is that it&rsquo;s forcing the boundaries really hard. So it&rsquo;s hard to go over network if it&rsquo;s not possible.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DYJwbfG-nXkQ%26amp%3Bt%3D0s">00:00:00 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DYJwbfG-nXkQ%26amp%3Bt%3D97s">00:01:37 - AI and splitting software</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DYJwbfG-nXkQ%26amp%3Bt%3D162s">00:02:42 - When splitting matters</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DYJwbfG-nXkQ%26amp%3Bt%3D279s">00:04:39 - Monolith first approach</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DYJwbfG-nXkQ%26amp%3Bt%3D368s">00:06:08 - Microservices hype era</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DYJwbfG-nXkQ%26amp%3Bt%3D574s">00:09:34 - Microservices force boundaries</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DYJwbfG-nXkQ%26amp%3Bt%3D796s">00:13:16 - The isolation illusion</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DYJwbfG-nXkQ%26amp%3Bt%3D1111s">00:18:31 - Merging services back</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DYJwbfG-nXkQ%26amp%3Bt%3D1389s">00:23:09 - Event storming for boundaries</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DYJwbfG-nXkQ%26amp%3Bt%3D1661s">00:27:41 - Core vs generic domains</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DYJwbfG-nXkQ%26amp%3Bt%3D1925s">00:32:05 - Bounded contexts and shared kernel</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DYJwbfG-nXkQ%26amp%3Bt%3D2130s">00:35:30 - Conway&#39;s Law and Team Topologies</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Miłosz</strong> [00:00:00]: So, if you consider splitting your project, I would start with this question: if you even need to do it, because you often see people struggling or hear them say, &lsquo;How do I start?&rsquo; I&rsquo;m starting a new project, kicking off some application. What services do I need to create? This aspect of microservices I think can be also misleading. Because it seems like you have these separate things. You see the deployments, you see the different repositories. For an outsider, you can think they are separate. Entities separate beings that can be changed independently of each other. It can be a trap. You have to think: what problem do you want to solve? This way, rather than just &lsquo;Oh yeah industrial best practice.</p> <p><strong>Robert</strong> [00:00:41]: Probably, can say that microservices are maybe solving a bit of technical problems, but it&rsquo;s mostly solving the human problem. Probably, you&rsquo;re mentioning the monolith first approach that we are big advocates. We&rsquo;ve tried it multiple times for products that become bigger later. If done properly, it&rsquo;s working. It&rsquo;s accelerating you a lot at the beginning and allows you to change it later.</p> <p><strong>Miłosz</strong> [00:01:05]: And what&rsquo;s surprising to me, it&rsquo;s not as popular.</p> <p><strong>Miłosz</strong> [00:01:11]: So, welcome after break. Let&rsquo;s talk about splitting software, different ways of splitting software, and why it&rsquo;s important in the first place.</p> <p><strong>Robert</strong> [00:01:20]: Yeah, I think it&rsquo;s a good topic because, probably, it&rsquo;s the topic that everybody writing software in this or another way needs to know how to do that. And if it&rsquo;s not done properly, it may create a lot of issues.</p> <p><strong>Miłosz</strong> [00:01:37]: Probably even if we end up doing coding with AI in the next couple of years, it will be an important topic because it&rsquo;s like one of the key architectural decisions you can make in your project.</p> <p><strong>Robert</strong> [00:01:50]: Especially, from our experience, even if people were doing that, usually they were missing that. And I would not expect that some cloud code or something like that.</p> <p><strong>Miłosz</strong> [00:02:00]: Handle it better than probably the opposite, actually, because there are so many anti-patterns on the internet. You know, like people tend to make mistakes when splitting software or do it in a very naive way. So yeah, I would expect coding agents to probably make similar mistakes.</p> <p><strong>Robert</strong> [00:02:20]: I think it&rsquo;s you know not only about some inexperienced people—I think also sometimes very experienced people, without experience, may miss some signals from the systems. So when you&rsquo;re building some system, it&rsquo;s sometimes hard to miss some signal when something is going in the wrong direction. And it happens even for those experienced people.</p> <p><strong>Miłosz</strong> [00:02:42]: So it&rsquo;s often the issue with big codebases, right? When we work with a big project, over time tends to be more difficult to work with, especially with bigger teams or multiple teams. And then the topic of splitting comes up. And I was thinking about, you know, our past projects on different companies, how important the problem is. And one thing I realized is that when we were working on our own projects before our full-time jobs, the stopping wasn&rsquo;t really that important. Important so it tells me something right, that&rsquo;s even if those applications were sometimes fairly complex, wasn&rsquo;t. If it&rsquo;s just you and maybe another person working on some project, it might still be fine sticking to one repository or codebase.</p> <p><strong>Robert</strong> [00:03:45]: Yeah, so probably can explain why, for example, people like Peter levels can have one PHP file that has 10,000 lines of code and he can work.</p> <p><strong>Robert</strong> [00:03:55]: But if you are two, also probably. But if you have people of five, ten, twenty, fifty people, it no longer works.</p> <p><strong>Robert</strong> [00:04:05]: When some application is done as proof of concept, later it evolves to some product that is making money. Sometimes it&rsquo;s missing this. Transition phase from the application that was built as proof of concept by a team of five— even if okay, if you have team of five, it&rsquo;s still not a big issue to handle, even very bad code, and be able to extend that. But when the team is growing, the product is growing, the number of features is growing, well, we start to have a problem. And the question is, what you do about that.</p> <p><strong>Miłosz</strong> [00:04:39]: Yeah, so if you consider splitting your project, I would start with this question: if you even need to do it. Because you often see people struggling or hear them asking, &lsquo;How do I start I&rsquo;m starting a new project, kicking off some application, and what services do I need to create? So we are probably at this stage, you don&rsquo;t need to consider this at all. Right, because it&rsquo;s not not the pain point, and the overhead may be big enough that it will not pay you back.</p> <p><strong>Robert</strong> [00:05:14]: So you probably are mentioning the monolith first approach that we are big advocates of. We tried it multiple times for products that become bigger later, and if done properly, it&rsquo;s accelerating you a lot at the beginning and allows you to change it later.</p> <p><strong>Miłosz</strong> [00:05:32]: And what&rsquo;s surprising to me it&rsquo;s not that popular, or maybe now it&rsquo;s probably a bit more popular than it was. I i see it mentioned here and there, but back back when we first tried it, I think no one talked about it that much.</p> <p><strong>Robert</strong> [00:05:45]: Yeah, but I think it&rsquo;s probably still some small percent. And it&rsquo;s maybe also a bit because of our bubbles. Yeah, maybe. But it&rsquo;s still not.</p> <p><strong>Miłosz</strong> [00:05:55]: It&rsquo;s not like a crazy different idea from creating microservices.</p> <p><strong>Robert</strong> [00:06:00]: But if you ask a random developer, what do you hear about? Kubernetes or modular monolith? I&rsquo;m pretty sure I know the statistics.</p> <p><strong>Miłosz</strong> [00:06:08]: Yeah, the hype is a funny factor here. So I remember in one project—when I joined— we had one huge codebase.</p> <p><strong>Miłosz</strong> [00:06:20]: Typically, that&rsquo;s the software no one wants to work with, and everyone would like to see refactored. And it was also around the time of the microservices hype era. So somehow we made a decision together as a team that we will split the monolith, as probably everyone back then did.</p> <p><strong>Robert</strong> [00:06:40]: Yeah, I remember how everybody was optimistic because it was like, &lsquo;whoa,&rsquo; microservices. Finally, a solution for all of our problems.</p> <p><strong>Miłosz</strong> [00:06:49]: And when I think about it now, it&rsquo;s funny how we had all these issues with this project and we assumed, &lsquo;Okay, microservices will solve this for us I mean, the project was better in the end for sure, and I was responsible for the design of the framework and all the networking and stuff. So, as a programmer, it was very fun. It was great. Probably we followed the</p> <p><strong>Robert</strong> [00:07:16]: &lsquo;fun&rsquo; principle too often.</p> <p><strong>Miłosz</strong> [00:07:18]: Yeah. But looking back at it, did it need all this complexity and distributed systems stuff? Probably not. Probably some parts could be simplified.</p> <p><strong>Miłosz</strong> [00:07:31]: But I think we just jumped on the hype train back then.</p> <p><strong>Robert</strong> [00:07:37]: But I think a good lesson from that was that you, as a team, saw some signal that &lsquo;okay, there is something wrong with this project,&rsquo; probably it was like, &lsquo;okay, we cannot maintain it, we cannot add new features easily.&rsquo;</p> <p><strong>Robert</strong> [00:07:51]: So the problem was visible there.</p> <p><strong>Miłosz</strong> [00:07:54]: Right. So the question is whether splitting software is a good approach to fix these kind of issues.</p> <p><strong>Robert</strong> [00:08:02]: But do you think that maybe what would work better there was maybe a bit more balanced approach— so maybe a bit less microservices or staying as a monolith— was better.</p> <p><strong>Miłosz</strong> [00:08:16]: Maybe less microservices or modular monoliths for the start. Because the issue for sure was that the boundaries were wrong in the initial software, so there was, you know, some function calls all over the place from one place to another with no control. The more developers you add to the project, the more complicated it gets. Because this is also important factor. If you are a single person working on software like this, it will be fine probably because you can keep the context in your head. You will refactor to whatever you want. But if you have like 20 people working on it, it starts getting more complicated.</p> <p><strong>Robert</strong> [00:08:57]: But still, I think it&rsquo;s still manageable with modular monoliths.</p> <p><strong>Miłosz</strong> [00:09:01]: Yeah, for sure. Yeah, but the issue is if you don&rsquo;t have these good boundaries, you don&rsquo;t know how to progress. Everyone tries to do something and it ends up as this big ball of mud.</p> <p><strong>Robert</strong> [00:09:14]: So in other words, you are modifying something in one place and everything around it goes up and you have no idea why. It should have zero dependency, but it seems that it does.</p> <p><strong>Miłosz</strong> [00:09:27]: Yeah, it&rsquo;s risky and you don&rsquo;t like making changes there.</p> <p><strong>Robert</strong> [00:09:30]: So the big bow of math or spaghetti code by the right, right.</p> <p><strong>Miłosz</strong> [00:09:34]: So if you have these good boundaries, as microservices, promised everything will be fine because you make these changes in one small Please.</p> <p><strong>Miłosz</strong> [00:09:45]: There&rsquo;s low impact, you can deploy it independently from the other services, everyone&rsquo;s happy, which makes sense, but you can&rsquo;t do the same thing without microservices.</p> <p><strong>Robert</strong> [00:09:56]: But there&rsquo;s one good thing about microservices is that it&rsquo;s forcing the boundaries really hard. So it&rsquo;s hard to go over network if it&rsquo;s not possible. I mean, when you have a big ball of mud, you can call everything if you don&rsquo;t have proper encapsulation. Okay, you can do proper encapsulation, but it requires some kind of regime of hardware. Or tooling, at least. Yes, yes. And with microservices, as long as you will not get exposed public endpoint, it&rsquo;s not. So it&rsquo;s probably showing why sometimes microservices may help. And have the same outcome with more cost, obviously. But again, if you go too far, it&rsquo;s much harder to undo that because, for example, if you were wrong with modules in modular monolith, you can copy from one catalog to another, join everything. We have one module with microservices. You need to change your Kubernetes configs. You need to change all dependencies in other team services.</p> <p><strong>Robert</strong> [00:11:02]: It&rsquo;s a mess, right?</p> <p><strong>Miłosz</strong> [00:11:03]: So maybe it&rsquo;s easier to see the microservices as well because you can show someone, &lsquo;Yeah, this is our Kubernetes deployments. They can see, okay, yeah.&rsquo; So this is your, those are your modules or microservices. The monolith is more complex because you need some kind of structure. It&rsquo;s still possible, and we did that, but maybe not as easy to see.</p> <p><strong>Robert</strong> [00:11:28]: And I think there is also one thing that microservices may make easier out of the box. So this is all sort of observability, because you can put some Istio or whatever between those services and you have it kind of out of the box again, not for free. Yeah, with much less flexibility, because Again, it&rsquo;s just put in the place by some cloud ops, DevOps team, whatever. But probably the flexibility is a lot limited. Right. From other side. You can achieve the same thing with your module monolith, but it requires some effort because you need to write it in code and you need to maintain that. other side, for example, I like this approach because it&rsquo;s giving you a lot of flexibility. And it&rsquo;s also removing some external dependency on the team that, in my opinion, should be more about running some transparent infrastructure rather than adding something like that. But from other side, it also depends because, for example, for some big enterprises, super big products, it kind of makes sense because you can have it out of the box and have it unified everywhere.</p> <p><strong>Robert</strong> [00:12:36]: So again, it depends on what kind of company we&rsquo;re talking about.</p> <p><strong>Miłosz</strong> [00:12:40]: Right. So microservices are kind of more tangible. You have better observability. You can see them. And modules are more abstract. You have to more understand the project to see them. Yeah.</p> <p><strong>Robert</strong> [00:12:54]: But yeah, trade-offs. We are mentioning that to show that there is not only one way, because I think now the way with doing it with Kubernetes, with some service mesh, it can be a way. Definitely for some bigger companies. But again, if you&rsquo;re working on a startup, maybe not— maybe you can do it by hand and it will be much more flexible, much more expensive.</p> <p><strong>Miłosz</strong> [00:13:16]: But this aspect of microservices, I think, can be also misleading because it seems like you have these separate things, right? You see the deployments, you see the— you know.</p> <p><strong>Miłosz</strong> [00:13:30]: different repositories. From an outsider, you can think they are separate entities, separate beings that can be changed independently of each other, but it can be a trap. I remember one project where, when we joined, they were described as independent and isolated from each other. So you can change one thing, it doesn&rsquo;t impact the others. But it turned out the kind of isolation layer was HTTP API between them. So, on the outside, it looks like nice, clean separation. But in reality, they are still very tightly coupled. They just communicate over the network instead of function calls. I remember this confusion we had and trying to explain it to the management as well. Why this nicely separated system is suddenly a problem?</p> <p><strong>Miłosz</strong> [00:14:34]: Because from the outside, it looks like you have those tangible, separate microservices. The reality is much different.</p> <p><strong>Miłosz</strong> [00:14:43]: That&rsquo;s also one of the hard parts: how to know. If the isolation is good enough.</p> <p><strong>Robert</strong> [00:14:51]: Yeah, and I think it&rsquo;s often outcome of, let&rsquo;s say, simplistic approach to splitting those services. So I would probably see two cases. One. We&rsquo;re getting database tables and we have separate service for each database table. Hooray, microservices.</p> <p><strong>Miłosz</strong> [00:15:07]: I often see this when someone asks us, on Discord or whatever, for some feedback on a system they did. And very often it&rsquo;s something like, &lsquo;So I have this application. And I have users microservice and orders microservice. And at this point, I know this won&rsquo;t be easy.&rsquo;</p> <p><strong>Miłosz</strong> [00:15:31]: For most e-commerce systems, you can just separate users out of the orders, or it will be, you know, there will be some tight coupling probably between them anyway, as long as you&rsquo;re not building some extremely big yeah, exactly. So it depends, but you know, if you take a fairly simple system, and you have those three main entities, and you think each entity should have their own microservice. Then you will probably end up with many problems because just separating those services won&rsquo;t give you isolation. It will just give you this illusion of isolation.</p> <p><strong>Robert</strong> [00:16:13]: Yeah, and I think it&rsquo;s also nicely visible if you take your service mesh tooling, and probably will see that between some microservices will have a lot of calls going back and forth.</p> <p><strong>Robert</strong> [00:16:26]: And it&rsquo;s usually the sign that maybe those two services shouldn&rsquo;t be separate. For example, those users and orders, those orders. So you can see that.</p> <p><strong>Robert</strong> [00:16:40]: To do an update to order or to do some shipment stuff or whatever, you need to call users many, many, many times.</p> <p><strong>Robert</strong> [00:16:50]: Can see that if, for example, you would need to add some field to, probably it can be customers, maybe not users in this case. So let&rsquo;s say orders and customers. Commerce. And let&rsquo;s imagine that orders is handing the shipment or something like that. And you can see the Skype link nicely when you have some new. For example, you need to add to user something that needs to be added to shipment. And in worst case, if it&rsquo;s handled by two teams, a project that could be done in one day with modular monolith is starting to take one month because you need to go back and forth with two teams and agree on contracts and on everything.</p> <p><strong>Miłosz</strong> [00:17:30]: You have to wait for the other team to pick it up for the sprint or whatever they work with. So it takes twice as long at least.</p> <p><strong>Robert</strong> [00:17:38]: Yeah, yeah. And, you know, It&rsquo;s often killing these small features that could be developed within one day or even if I code it now. Sometimes those features are that simple that it can be easily vibecoded. But with microservices, you no longer can because it needs to be orchestrated on a higher level. Maybe, you know, era of vibecoding will be era of modular monoliths. Hopefully modular. For sure, easier to deploy. But. And I think it&rsquo;s also one important technique to mention when we are talking about these patterns, when we see that. So I would say that dark two signals when you should consider if something is wrong with us. One signal is when there&rsquo;s a lot of calls between those two services to do some scenario. And the second thing is when you are modifying some functionality.</p> <p><strong>Robert</strong> [00:18:31]: It requires changes in two or many services multiple times. So you see this pattern over and over. And it may be a sign that it may be wrong separation of services.</p> <p><strong>Robert</strong> [00:18:41]: To solve that, there&rsquo;s one technique that I think it&rsquo;s very rarely mentioned because maybe it&rsquo;s, I don&rsquo;t know, some heresy for many people, but it&rsquo;s the microservice station. So basically you&rsquo;re getting those two services and joining them.</p> <p><strong>Robert</strong> [00:18:56]: And I know that for many people it sounds like a heresy. I hope it will change over the time.</p> <p><strong>Miłosz</strong> [00:19:02]: And yeah, it&rsquo;s not never not easy, depending on how they are built.</p> <p><strong>Robert</strong> [00:19:06]: But Yes, but if you are getting into problem of changing those two services over and over and two teams that needs to do that, or even within one team, you need to always change two services and kind of synchronize how you&rsquo;re deploying them. It may be worth considering if it&rsquo;s worth investing in joining them. Because yeah, probably to introduce some costs, they need to do some kind of cost and income.</p> <p><strong>Miłosz</strong> [00:19:32]: Yeah, it&rsquo;s a good thing you mentioned Teams because it&rsquo;s also something I think you can&rsquo;t skip when designing.</p> <p><strong>Miłosz</strong> [00:19:40]: Creating software, especially if you have teams working on the same project, maybe not the same code base, but microservices that work with each other.</p> <p><strong>Miłosz</strong> [00:19:53]: And if you have many of the services. I think once we worked for a company that had probably more services than developers or it was something like that, which sounds funny now, but I think it&rsquo;s the reality for many companies actually. Because when it&rsquo;s easy to spin up a new service, maybe you have great tooling and some platform team that handles it for you.</p> <p><strong>Robert</strong> [00:20:16]: It&rsquo;s interesting risk, actually, of having great tooling for easily creating microservices. I mean, if it wasn&rsquo;t easy, you would think twice before creating a new microservice.</p> <p><strong>Miłosz</strong> [00:20:28]: If creating that takes one minute, you end up with teams who own 10 or more. Microservices for very simple tasks sometimes I don&rsquo;t know— maybe it&rsquo;s fine. Sometimes, for things like keeping avatars, for example, so it&rsquo;s like—overhead— but I think the most problematic story I remember from from this was where there were some services that had no clear owner, and we both our teams and other teams&rsquo; responsibilities kind of crossed in one area. That&rsquo;s not really a technical issue—it&rsquo;s funny, that&rsquo;s like a team organizational issue. Mm-hmm.</p> <p><strong>Miłosz</strong> [00:21:19]: And it&rsquo;s very difficult to figure out if you didn&rsquo;t start with a good design of the system first, but you just, you know, added the microservices for this entity here. This entity here— this entity here— maybe this database table sounds like a separate service, and then you have to kind of— you know— create some teams and you assign them the services. Artificially. Yeah. So that&rsquo;s not a good idea.</p> <p><strong>Robert</strong> [00:21:47]: And two years later, if somebody asks why those services are like that, it&rsquo;s: &lsquo;It was always like that.&rsquo;</p> <p><strong>Miłosz</strong> [00:21:54]: Why you ask? Yeah. So we finally figured it out somehow, but it was very painful.</p> <p><strong>Miłosz</strong> [00:22:02]: Not only because of the technical challenges, but also all the discussions between teams, company politics, stuff like that.</p> <p><strong>Robert</strong> [00:22:13]: What I remember was useful there. So in this scenario, we used event storming for that to discover that. But you don&rsquo;t need to use event storming. You can use any technique to see how the flow of the system is going. And I remember that it was quite funny to see that entire critical flow of the application was. So, for example, the beginning of the flow was owned by one team, later it was some small element owned by another team, it was going over again to the previous team. And it was a lot of some small pieces. Crossing the boundary. Yes, yes. And those were usually the biggest problems because if something didn&rsquo;t work, it was sometimes not clear who is responsible. And it was also harder to change this law because if team didn&rsquo;t have entire ownership of this law, they were just not able to do it without some cross-team coordination.</p> <p><strong>Robert</strong> [00:23:09]: Well, what we did there, the solution was quite simple. So it was looking on high level on this and trying to understand how we can change ownership.</p> <p><strong>Robert</strong> [00:23:19]: Historical ownership, and it helped a lot. And we added also to that a bit of the microservice station, and it helped even more.</p> <p><strong>Miłosz</strong> [00:23:26]: I thought about even storming when you mentioned service mesh visualization.</p> <p><strong>Miłosz</strong> [00:23:31]: So maybe instead of using this visualization after the fact, after you have the microservices in place, you could just have this high-level plan with even storming or not.</p> <p><strong>Robert</strong> [00:23:45]: Seeing how system works before it works? How?</p> <p><strong>Robert</strong> [00:23:49]: How is it possible?</p> <p><strong>Miłosz</strong> [00:23:51]: What kind of magic is that?</p> <p><strong>Miłosz</strong> [00:23:54]: Try to design it first and then see. But you know, it&rsquo;s also not that easy, right? Because it&rsquo;s not just a UML. Diagram where you have users, orders, and you see—okay, this is separate, but more like &lsquo;yeah,&rsquo; maybe that&rsquo;s why I would still recommend event storming because it focuses on behavior, not on the entities. So, if you map the events that happen, then the behaviors you can see there are some— those clusters of things that happen together— and it probably means they are a good place for some kind of boundary. And if you have, for example, two clusters that are joined by two events or something like that, you can clearly see, &lsquo;okay,&rsquo; those things can be mostly separate and they won&rsquo;t interfere with each other. They won&rsquo;t be tied up.</p> <p><strong>Robert</strong> [00:24:53]: And I think what&rsquo;s also pretty good there, compared to some UML diagrams, is the context of time. And because, when you are looking at UML diagrams, you cannot see the order of flow. Even storming is more so for people that don&rsquo;t know how to it looks like. So TLDR is that it&rsquo;s basically some brainstorming session when you&rsquo;re putting sticky notes for domain events or whatever events that are happening in the system. And you don&rsquo;t need to do event-driven architecture for that later. So it&rsquo;s more for the high-level modeling and how you implement that later, whatever. And what&rsquo;s nice, it&rsquo;s putting the in time order.</p> <p><strong>Robert</strong> [00:25:32]: So basically, when you have flow, you can see when the flow is starting, when it&rsquo;s ending, and it&rsquo;s kind of in a magical way helping you to see how to do the split there. Anyway, I think it&rsquo;s a big advantage compared to those UML diagrams or in general some diagrams.</p> <p><strong>Robert</strong> [00:25:52]: Flow diagrams probably are a bit better because they also go from left to right, but I think they are not readable for very complex systems. You cannot make a flow diagram for entire system. Don&rsquo;t try. But with event storming, basically you can do super big systems on higher level, but you can see the big picture of it.</p> <p><strong>Miłosz</strong> [00:26:16]: So it&rsquo;s a great technique if you&rsquo;re just looking for the boundaries, or even if you have an existing system and you want to see how it could be done in a different way.</p> <p><strong>Robert</strong> [00:26:25]: If you would like to hear a bit more about event storming, we&rsquo;ve been discussing it one episode earlier, two episodes earlier, something like that. That so check it out but okay let&rsquo;s come back to splitting.</p> <p><strong>Miłosz</strong> [00:26:38]: So the nice benefit is of having everything mapped out on some whiteboard or you know some software. Is that you can look at it from different perspectives. For example, one thing you can do is try to differentiate core domains from the generic domains. So for example, you can see some cluster where there is more of your domain language of what your software is very specific. About, different from other software. This is probably your core part and it might be one cluster. And maybe there are some parts that are more supporting, more generic and they don&rsquo;t need to be involved as much. So they don&rsquo;t need to know all the details of your domain. Very often this helps you to simplify this part. For example, it can be some e-commerce software or some account.</p> <p><strong>Miłosz</strong> [00:27:41]: Mounting software you can just buy, or you know, wipe code, or whatever— you don&rsquo;t need to build it yourself from scratch if you don&rsquo;t want to.</p> <p><strong>Robert</strong> [00:27:51]: So, in other words, not all software in your product is equal. So some parts of software probably require a bit more of love.</p> <p><strong>Miłosz</strong> [00:28:01]: And the less they know of each other, the cleaner your boundaries and the simpler the software as well.</p> <p><strong>Miłosz</strong> [00:28:08]: So even if you build yourself this generic part, I would do it in a way that it could be reused in a different company.</p> <p><strong>Miłosz</strong> [00:28:18]: It doesn&rsquo;t mean you need to do it in the end, but it often helps to keep these boundaries clean and just keep some translation between them. Hmm-hmm. Of course, it can also be overdone if you try to make it super generic or use cases. So you have to be careful about that. But in general, I would try to not let the domain terms spill out of the core parts.</p> <p><strong>Robert</strong> [00:28:48]: Yeah, I think it&rsquo;s also some other direction that sometimes you can also watch out for with buying things because, it sometimes works that there are some services that are generic enough that can buy it, like sending emails, sometimes accounting software, but it depends. So sometimes when you are handling very specific things like marketplaces, you might not find accounting software that can handle it for you. I mean, for example, you are building some food delivery application. I guess you won&rsquo;t find accounting software that will handle it for you. So it&rsquo;s kind of interesting, because if you&rsquo;re building this kind of application, it&rsquo;s not your core domain, probably, but for other sites, it&rsquo;s a very important domain. Unit also yeah— probably building a house, but parts of it will be generic, right?</p> <p><strong>Miłosz</strong> [00:29:42]: Like printing invoices, that&rsquo;s probably something someone solved some time ago and you don&rsquo;t need to write it from scratch. Or if you do, we can keep it very generic.</p> <p><strong>Robert</strong> [00:29:52]: Or maybe, for example, trying to understand what&rsquo;s art. tax rate for each country is a good type. I mean, you can probably hire an entire team that is spending their entire time researching what the rates for every type of food or whatever that you can have in any countries. But probably you can just buy it from external company. And other techniques for event storming. So maybe not forever event storming, but I have also in mind one thing that you mentioned: sometimes you have parts of the system, some maybe services, maybe some libraries that multiple teams are kind of owning. And sometimes it&rsquo;s on the pattern, but I think sometimes it&rsquo;s also not, because sometimes it can happen. For example, from domain-driven design, we have concept of shared kernel, and it&rsquo;s there for some reason. So sometimes you have some parts of code that two teams need to share, need to own. M. You can use it, but I would say that it&rsquo;s rather an exception than something that you need to use by default, because if you try to use it by default, probably you&rsquo;ll overuse it.</p> <p><strong>Robert</strong> [00:31:02]: But I think it&rsquo;s also important to keep it in mind that sometimes you have some things like that and it&rsquo;s also fine.</p> <p><strong>Miłosz</strong> [00:31:09]: Yeah, it&rsquo;s a very important decision when you have two teams working with some similar entity or the same entity in different contexts. So first, there&rsquo;s also the bounded context concept from DDD. What I think about it the most is exactly this situation when, for example, you have this user entity which can be also a customer entity in another context and, in other contexts, be something else. And you have multiple teams working with it. One naive approach would be to deduplicate and create one main user that decides about everything and has all the fields and behaviors and so on. But it that means all teams share this part and you know they need to agree on how to develop it or, from other side, if you&rsquo;re one team, don&rsquo;t that because it will just create too much overhead.</p> <p><strong>Robert</strong> [00:32:05]: And this was something that we see often: there was one team and went too far with all those patterns.</p> <p><strong>SPEAKER_2</strong> [00:32:12]: And later, it&rsquo;s like, oh, no, Domain Driven Design is making our software so complicated.</p> <p><strong>Miłosz</strong> [00:32:19]: Yeah, but if you have multiple teams, you can use the bounded context concept. So you try to figure out how this user entity works. Different contexts may be separate; you know, maybe both teams can have their own user entity and they are completely separated and that&rsquo;s fine. And sometimes you have this shared kernel, as you mentioned. So some parts that are so common and probably specialized.</p> <p><strong>Miłosz</strong> [00:32:49]: Like you mentioned, maybe the tax rate calculation, something like that. But it doesn&rsquo;t really make sense to separate that. You just agree, okay, on this small common part. We will work together and you know both teams— for example— need to agree on reviews and stuff like that on the design of it instead of the entire big system. So when I think about what&rsquo;s difficult about this problem of splitting software well, I think it&rsquo;s a combination of organizational and technical complexity. It&rsquo;s not just one thing.</p> <p><strong>Robert</strong> [00:33:23]: So sometimes I remember that there are some companies where the teams and services split and organization is somehow going from the top. Maybe it&rsquo;s not from the top, but level or two levels higher than you are.</p> <p><strong>Miłosz</strong> [00:33:39]: Or another way is that it became kind of the default to create microservices, and very often in a naive way.</p> <p><strong>Miłosz</strong> [00:33:50]: So, for example, when I joined some project and there was some kind of migration happening from an old system to a new one.</p> <p><strong>Miłosz</strong> [00:33:59]: A few people in the team assumed, let&rsquo;s use microservices from the start. It&rsquo;s kind of like a default approach for some people, but it can be a trap. Because you have to think, what problem do you want to solve this way, rather than just, &lsquo;Oh yeah, it&rsquo;s the industry best practice.&rsquo;</p> <p><strong>Robert</strong> [00:34:19]: Worse if you have problem that you&rsquo;re trying to solve, because sometimes people may think, &lsquo;Yeah, we have problem with I don&rsquo;t know scalability or &lsquo;We had overcomplicated POC earlier and microservices will solve our problem.&rsquo; Yes.</p> <p><strong>Miłosz</strong> [00:34:36]: So it&rsquo;s helpful to think of microservices. Deployment patterns, something we discussed some time ago, I think.</p> <p><strong>Miłosz</strong> [00:34:45]: It helps when you want independent deployments, for example, for different teams. That&rsquo;s a good excuse to split software.</p> <p><strong>Robert</strong> [00:34:56]: Or for example, you have that many deployments of your module or monolith that the queue is longer than you. I mean, queue is long enough that not every person is able to merge within one day. Probably it&rsquo;s already designed that you can split it.</p> <p><strong>Miłosz</strong> [00:35:13]: So it&rsquo;s connected to teams and how you organize developers.</p> <p><strong>Robert</strong> [00:35:20]: So in other words, probably we can say that microservices are maybe solving a bit of technical problems, but it&rsquo;s mostly solving the human problem.</p> <p><strong>Miłosz</strong> [00:35:30]: Humans working together problem.</p> <p><strong>Miłosz</strong> [00:35:33]: There&rsquo;s Tim Topology&rsquo;s book. I&rsquo;ve read some time ago. It&rsquo;s great. I can recommend it. It&rsquo;s specifically about this problem with the reverse Conway maneuver, I think it&rsquo;s called. So basically, instead of creating a software first and then trying to assign people who work on what, you do it the opposite way. So you first design, as we mentioned, event storming, a bit similar idea. You first design the organizational structure you want to have in place, and then the software system follows that.</p> <p><strong>Robert</strong> [00:36:08]: I love when such old patterns, like always low, are still working those days.</p> <p><strong>Robert</strong> [00:36:14]: Even if people don&rsquo;t realize, but, but I think. It&rsquo;s good to know about some that something like that exists, because you can be a bit maybe more mindful when you&rsquo;re doing such decisions. And it&rsquo;s not like, oh, just everybody&rsquo;s doing that or we&rsquo;ve been doing it like that for ages, why we should change it. Yeah.</p> <p><strong>Miłosz</strong> [00:36:36]: It&rsquo;s also great that we are past hype. Era now and many people talk mainly about how complex microservices can be. So now it&rsquo;s probably we are more balanced.</p> <p><strong>Robert</strong> [00:36:47]: Probably won&rsquo;t you won&rsquo;t be put on the stake after saying that microservices is not the only way.</p> <p><strong>Robert</strong> [00:36:55]: Maybe in 10 ways we&rsquo;ll be again in the place when everybody is using monoliths. Probably not modular, something tells me. Probably it will be still a problem.</p> <p><strong>Miłosz</strong> [00:37:06]: I think, as an ending note, we can recommend starting with monoliths if you are just getting started, especially if you are just a single developer or a small team. Yeah, I hope we should. And do it the modular monolith way.</p> <p><strong>Robert</strong> [00:37:20]: Yeah, I hope that we&rsquo;ve shown that there are alternatives and it&rsquo;s really not that hard to do things in those alternative ways.</p> <p><strong>Robert</strong> [00:37:30]: Definitely will not finish with this episode about modularization because it&rsquo;s a topic that requires a lot of attention, in my opinion, and it can give a lot back— i mean, when you are doing it properly, it can really accelerate your work a lot, yeah, especially with modular monolith.</p> <p><strong>Miłosz</strong> [00:37:49]: Because I think what we&rsquo;ve figured out works pretty well.</p> <p><strong>Robert</strong> [00:37:54]: And we&rsquo;ve seen it work with teams. And we also see that it&rsquo;s not that popular approach. So I think it may be also some unique skill to have.</p> <p><strong>Miłosz</strong> [00:38:04]: Yeah. And if you&rsquo;d like to see a real example of this, we have a domain engineer training coming up and we will have an example of a pretty complex application with different modules built as a modular monolith. And we mentioned parts of it in this episode as well. Yeah, I think it will be cool. So we&rsquo;ll leave a link in the description if you want to learn more. So we shared our stories with Spitting Software. Let us know about yours so we can also learn something and maybe help you figure out something.</p> <p><strong>Robert</strong> [00:38:40]: Yeah. So we&rsquo;re here to help you. So this podcast is for you. And yeah, if you will be happy to answer in comments and maybe include something in the next episode as well.</p> <p><strong>Miłosz</strong> [00:38:53]: Yeah, if you have any topics you would like us to cover, let us know.</p> <p><strong>Miłosz</strong> [00:38:58]: See you in the next episode. See you next time. Bye. Bye-bye.</p>How to Know If your Software Is Overcomplicated or Oversimplified?https://threedots.tech/episode/overcomplicated-vs-oversimplified-code/Fri, 19 Dec 2025 08:00:00 +0000https://threedots.tech/episode/overcomplicated-vs-oversimplified-code/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Complexity comes from two extremes</strong> - projects fail both when they&rsquo;re overcomplicated with unnecessary patterns and when they&rsquo;re oversimplified for a complex domain</li> <li><strong>Essential vs accidental complexity</strong> - essential complexity comes from the domain itself and can&rsquo;t be removed, accidental complexity is created by poor implementation choices</li> <li><strong>&ldquo;Keep it simple&rdquo; is lazy advice</strong> - achieving simplicity takes effort; closing your eyes to complexity just pushes it elsewhere</li> <li><strong>Match patterns to the problem</strong> - using the same approach everywhere is a red flag; mix simple solutions for simple parts and sophisticated patterns for complex domains</li> <li><strong>Ship fast and iterate</strong> - if you can&rsquo;t deploy daily and fear making changes, something is wrong regardless of whether it&rsquo;s over or under-engineered</li> </ul> <h2 id="introduction">Introduction</h2> <p>We discuss where complexity in software projects comes from and how to deal with it. There are two common extremes: projects that are overcomplicated because someone applied patterns they saw at a conference without understanding them deeply, and projects that started simple but became a tangled mess as they grew.</p> <p>Both can be equally difficult to work with. The key insight is understanding the difference between essential complexity (inherent to your domain) and accidental complexity (created by your implementation choices). We share diagnostic signals to identify unhealthy projects and practical advice on matching your tools to the actual problem you&rsquo;re solving.</p> <h2 id="notes">Notes</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fworrydream.com%2Frefs%2FBrooks_1986_-_No_Silver_Bullet.pdf" target="_blank">No Silver Bullet</a> paper by Fred Brooks - discusses essential vs accidental complexity</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Ftags%2Fddd%2F">Domain-Driven Design (DDD)</a> - a pattern for tackling complex domains</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Ftags%2Fclean-architecture%2F">Clean Architecture</a> - another approach for managing complexity</li> <li>YAGNI (You Aren&rsquo;t Gonna Need It) - principle against premature generalization</li> <li>Defensive programming - validating inputs early to prevent errors propagating</li> <li>Canary releases and rollbacks - deployment strategies that can add overhead</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech" target="_blank">Three Dots Labs blog</a> - articles on Go patterns and architecture</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F" target="_blank">Go with the Domain ebook</a> - free ebook about DDD in Go with 60,000+ downloads</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">Wild Workouts</a> example DDD project - complex Go project demonstrating real-world patterns</li> <li>Microservices architecture - discussed as sometimes adding unnecessary complexity</li> <li>CRUD applications - simple approach that works for some domains but not others</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fthe-domain-engineer%2F" target="_blank">The Domain Engineer</a> training - upcoming training mentioned for early next year</li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>On one hand, you have someone who says we need to support 10 million users a day—this is one extreme— but on the other hand, you also have people who say, &lsquo;just keep it simple and it will be fine&rsquo; and it sounds good. But to me, it&rsquo;s one of the lazy advice. It&rsquo;s easy to say to tell someone, &lsquo;keep it simple but it&rsquo;s not that easy in the end.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>The complexity doesn&rsquo;t go anywhere, no matter how you implement it.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>If your team is unhappy working with the project. If the team brings up plan for migration or rewrite every other day, that&rsquo;s a solid sign something went very wrong. You&rsquo;re probably on one extreme already and it&rsquo;ll be hard to get out of it.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>If just keeping projects simple would be that easy, all projects would be great. But unfortunately, it&rsquo;s the opposite.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>So you cannot close your eyes and say that this complexity doesn&rsquo;t exist. It will pop up somewhere else and it will be much more expensive to handle at the end.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>Don&rsquo;t be dogmatic about using any tools because it&rsquo;s never ending up properly.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DKgb45RQirvM%26amp%3Bt%3D0s">00:00:00 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DKgb45RQirvM%26amp%3Bt%3D81s">00:01:21 - Two sources of complexity</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DKgb45RQirvM%26amp%3Bt%3D314s">00:05:14 - Complex vs complicated</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DKgb45RQirvM%26amp%3Bt%3D560s">00:09:20 - Essential vs accidental complexity</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DKgb45RQirvM%26amp%3Bt%3D840s">00:14:00 - The trap of generic solutions</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DKgb45RQirvM%26amp%3Bt%3D1155s">00:19:15 - Complexity doesn&#39;t disappear</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DKgb45RQirvM%26amp%3Bt%3D1385s">00:23:05 - Patterns and defensive programming</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DKgb45RQirvM%26amp%3Bt%3D1618s">00:26:58 - Red flags in projects</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DKgb45RQirvM%26amp%3Bt%3D1925s">00:32:05 - Avoiding premature optimization</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DKgb45RQirvM%26amp%3Bt%3D2112s">00:35:12 - Don&#39;t be dogmatic</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DKgb45RQirvM%26amp%3Bt%3D2260s">00:37:40 - Practice and resources</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Miłosz</strong> [00:00:00]: On one hand, you have someone who says we need to support 10 million users a day—this is one extreme— but on the other hand, you also have people who say, &lsquo;just keep it simple and it will be fine&rsquo; and it sounds good. But to me, it&rsquo;s one of the lazy advice. It&rsquo;s easy to say to tell someone, &lsquo;keep it simple but it&rsquo;s not that easy in the end.</p> <p><strong>Robert</strong> [00:00:18]: If just keeping projects simple would be that easy, all projects would be great. But unfortunately, it&rsquo;s the opposite.</p> <p><strong>Miłosz</strong> [00:00:25]: It takes a lot of effort to have simplicity in the project.</p> <p><strong>Robert</strong> [00:00:28]: Your eyes and say that this complexity doesn&rsquo;t exist, it will pop up somewhere else and it will be much more expensive to handle at the end. It&rsquo;s a very important concept to understand the problem. Accidental complexity and essential complexity. Accidental complexity is created by poor implementation. If you approach the implementation of your application in an overly complicated way, so we&rsquo;ll put just some patterns that are unnecessary, you will have accidental complexity. On the other hand, if your project will be, for example, this complex financial product and you will try to build as a CRUD, you&rsquo;ll end up with a lot of accidental complexity there.</p> <p><strong>Miłosz</strong> [00:01:05]: Yeah, but you can reduce it if you use some patterns that play well with the domain. So you can reflect it in the code in a more natural way. Then it&rsquo;s easier to work with. So the accidental complexity goes down.</p> <p><strong>Robert</strong> [00:01:21]: I&rsquo; m Robert.</p> <p><strong>Robert</strong> [00:01:21]: And I&rsquo;m Miłosz.</p> <p><strong>Robert</strong> [00:01:22]: And today we&rsquo;ll talk about complexity in projects and from where this complexity is coming. And from my experience, there are probably two types or two sources from where it can come. So the first one. Probably. Many of you can relate to that. It&rsquo;s the situation when you are joining some project and there is some team leader or some very senior engineer. And this engineer is dictating, let&rsquo;s say, some good practices and some patterns in this project.</p> <p><strong>Robert</strong> [00:01:55]: You can clearly see that those patterns are not adding a lot to this project. Actually, it&rsquo;s adding a lot of unnecessary complexity. Often, this person is getting those techniques from some conference, maybe, or from some video, or maybe this person read some article. Later, this person is forcing everybody to use those patterns.</p> <p><strong>Miłosz</strong> [00:02:15]: So it&rsquo;s this ivory tower architect who dictates from the top how to do things and the solutions are already invented.</p> <p><strong>Robert</strong> [00:02:26]: Yeah, so it&rsquo;s often the problem. It doesn&rsquo;t need to be architect because it&rsquo;s sometimes maybe, again, some more senior person. But from my perspective, very often the problem in this situation is that this person doesn&rsquo;t really understand those patterns very deeply.</p> <p><strong>Robert</strong> [00:02:45]: As I said earlier. This person has seen those patterns in one video or maybe one talk and knows how it looks like. So you are going to conference, there is some idea, it looks great on the paper. In practice, Well, there are a lot of complexities and it doesn&rsquo;t work as simple as it was thought on the conference.</p> <p><strong>Miłosz</strong> [00:03:04]: And being an engineer on this team, maybe peer of this person, you will probably feel like this project is overengineered, right? There is too much patterns and it seems overcomplicated. It&rsquo;s very difficult to get something done.</p> <p><strong>Miłosz</strong> [00:03:22]: Because there&rsquo;s so much work involved.</p> <p><strong>Robert</strong> [00:03:24]: Definitely. And I think for many people, it will be the main source of complexity in projects, but maybe you have some other idea what can be another issue that can happen in projects.</p> <p><strong>Miłosz</strong> [00:03:35]: So on the other hand, I can remember other types of projects where everything seems very simple, but at some point it&rsquo;s very difficult to continue development.</p> <p><strong>Miłosz</strong> [00:03:50]: Project lifetime, it&rsquo;s sufficient to have some very basic patterns in place. Maybe no patterns at all.</p> <p><strong>Robert</strong> [00:03:58]: If you have small enough application, maybe you can just do only one file and sometimes it&rsquo;s starting as some proof of concept or maybe side projects and because of some lack reason this is becoming pretty successful project at the end. And it&rsquo;s becoming also the problem because it started to be it started as a proof of concept. And at the end, it it became a successful product.</p> <p><strong>Miłosz</strong> [00:04:22]: These are fun. You can just add new endpoints, add new database tables.</p> <p><strong>Miłosz</strong> [00:04:30]: Works fine, but then that team grows and at some point it starts to be very difficult to extend this project because you know everything is coupled together when you change something you don&rsquo;t know what else is going to change and usually at this time you you might be long for some patterns actually to be used in place and I started researching some architecture, how to approach this thing. It has two extremes, right? One is overcomplicated, the other is no patterns at all and both can be equally annoying to work with, difficult to develop.</p> <p><strong>Miłosz</strong> [00:05:14]: Thank you.</p> <p><strong>Robert</strong> [00:05:14]: In this episode, we&rsquo;ll cover a bit more. How to understand if your project is in the bucket of overcomplicated by using too many patterns or maybe oversimplified. So the problem that you are solving or the product that you are working with is already pretty complex and building is a crud with just frameworks. It may not work anymore. And I think it&rsquo;s a very important skill that many people is missing. So I think many people are stopping on the side that, yeah, this project is very overcomplicated because we use too many patterns but it&rsquo;s not only the case yeah so it&rsquo;s a similar issue but Okay.</p> <p><strong>Miłosz</strong> [00:06:01]: two extremes, right? But the root cause is kind of the same. It&rsquo;s using wrong tools to the wrong problem. Or you don&rsquo;t know what the problem is yet and you use only what you know.</p> <p><strong>Robert</strong> [00:06:14]: Bye. It&rsquo;s not only about the complexity of the project but sometimes it&rsquo;s also the scalability. So we&rsquo;ll also cover a bit about this today, because also you probably heard many stories like: &lsquo;This application will have millions of users.&rsquo; At the end, we have 10 users. And it&rsquo;s hard to add anything more because you prepare your application for big traffic.</p> <p><strong>Miłosz</strong> [00:06:35]: And because of that, it&rsquo;s very, very complex. On one hand, you have someone who says, &lsquo;we need to support 10 million users a day,&rsquo; which is one extreme. other hand, you also have people who say, &lsquo;keep it simple and it will be fine and it sounds good but to me it&rsquo;s one of the lazy advice.</p> <p><strong>Miłosz</strong> [00:06:55]: It&rsquo;s easy to say, to tell someone, &lsquo;keep it simple.&rsquo; But it&rsquo;s not that easy in the end.</p> <p><strong>Robert</strong> [00:07:03]: If just keeping projects simple would be that easy, all projects would be great. But unfortunately, it&rsquo;s the opposite. It takes a lot of effort to achieve simplicity in the project. If you&rsquo;re joining a new project and you notice that this project is complex. So in this case, do you wonder if it&rsquo;s complex because of some patterns that make it overly complicated? Or maybe this project has some kind of baseline complexity that made this project complex.</p> <p><strong>Miłosz</strong> [00:07:33]: I think it&rsquo;s very different. Tell the difference when you just joined the project— actually. You might know that it is difficult to work with, but you might not yet know what it solves, what&rsquo;s the business domain.</p> <p><strong>Miłosz</strong> [00:07:47]: So it might be not that easy to tell if it&rsquo;s over-engineered or it&rsquo;s just inherent domain complexity that&rsquo;s not going anywhere.</p> <p><strong>Robert</strong> [00:07:58]: And I think what may be pretty useful here is distinction between complex and complicated, because I think those words are nicely encapsulating the different characteristics of this. So when we have a project that is complex, so it&rsquo;s a project that has, for example, some complex domain like accounting, complex e-commerce, or is pretty old product supporting many use cases. So in this case, it&rsquo;s complex, but it doesn&rsquo;t need to be complicated. So, if somebody will not approach it properly, it may be complicated, but I think what&rsquo;s also pretty interesting is that some projects are not complex, but they are just complicated. And in this case, it&rsquo;s the scenario when, for example, this team leader went from a conference and just read some blog article, started to copy some patterns, and just unnecessarily complicated that. For the sake of it, instead of solving something.</p> <p><strong>Miłosz</strong> [00:08:53]: Exactly. Yeah, but it might be hard to tell the difference if you are not very deep into the project. I remember working with some MVPs that looked complicated on the surface. And then you start rebuilding them or trying to understand what&rsquo;s going on. And you see that, okay, this is not actually technical complexity. This is how the business works. So that&rsquo;s why someone did this this way.</p> <p><strong>Robert</strong> [00:09:20]: So some examples of projects that are complex, but maybe not complicated, are for some financial-related applications. So for example, some accounting systems that are supporting multiple countries and even hundreds of countries, different tax rules that can be different for multiple countries and some domain. May have some special requirements, like for example, taxi or something like that. And this kind of projects are very big. Baseline complexity. And by the way, we&rsquo;re currently working actually on a training that we&rsquo;ll be releasing at the beginning of the next year when we&rsquo;ll be showing how to build such very similar domain actually. So you can see the description. Yeah, so check the domain engineer in Google. So you will find it. But what I think it&rsquo;s pretty interesting in this kind of projects is that if you, at the first site, look on all these accounting rules and support of multiple countries, you might have a feeling that it&rsquo;s super complicated, but actually depends how you approach implementation of that.</p> <p><strong>Robert</strong> [00:10:37]: So no matter what you do, in the implementation part, it will be complex because it&rsquo;s just a complicated domain.</p> <p><strong>Miłosz</strong> [00:10:46]: The complexity doesn&rsquo;t go anywhere, no matter how you implement it.</p> <p><strong>Robert</strong> [00:10:49]: And often this is named as essential complexity. This is the complexity that is kind of created by the domain. And the opposite of that is. Accidental complexity. So accidental complexity is complexity that is created by engineers, basically. So when you are, for example, preparing your application for global scalability, millions of users. So this is where you are creating the accidental complexity.</p> <p><strong>Miłosz</strong> [00:11:16]: Yeah. I think it comes from the No Silver Bullet paper from Fred Brooks, which shares the name of our podcast. It&rsquo;s actually a good read.</p> <p><strong>Robert</strong> [00:11:24]: What a cool incident.</p> <p><strong>Miłosz</strong> [00:11:26]: Check it out. It goes into detail on essential versus accidental. But maybe we can just quickly go over it. Hmm, hmm.</p> <p><strong>Miłosz</strong> [00:11:37]: Hmm.</p> <p><strong>Robert</strong> [00:11:37]: Also, because I think it&rsquo;s very important to understand that concept to understand the entire problem of accidental complexity and essential complexity. So basically, the accidental complexity is created by poor implementation. Because if you approach the implementation of your application in an overly complicated way, so you&rsquo;ll put just some patterns that are unnecessary, you will have accidental complexity. But on the other hand, if your project will be, for example, this complex financial product and you will try to build as a CRUD, you&rsquo;ll also end up with a lot of accidental complexity there.</p> <p><strong>Miłosz</strong> [00:12:16]: Yeah, but you can reduce it if you use some patterns that play well with the domain. So you can reflect it in the code in a more natural way. Then it&rsquo;s easier to work with. So the accidental complexity goes down.</p> <p><strong>Robert</strong> [00:12:31]: Unfortunately, if you do the opposite— so if you have a complex problem to solve and you try to approach it in an oversimplified way— it will add. So we will have the base complexity, so the essential complexity, and you will have also accidental complexity. So this product probably will be a great mess that nobody would like to touch.</p> <p><strong>Miłosz</strong> [00:12:51]: And suddenly, we developers often bring this on ourselves, focusing on the technical details, trying to build a framework that will capture cases in the future, stuff like that, which all can add up to extensive complexity before you need all the features.</p> <p><strong>Robert</strong> [00:13:11]: It&rsquo;s starting with a solution that is generic from day one, and it&rsquo;s basically creating a solution that is generic. Always much more complicated than creating a solution for one use case. I have some examples when we&rsquo;ve been adding some functionality and some teammates were proposing like, &lsquo;Oh, let&rsquo;s maybe create a library to do that.&rsquo; You will not need to do it in the future. But after trying to design that, we found that actually making it in a generic way is a couple times more complicated than just doing it in one way for one scenario. And sometimes it&rsquo;s even easier to just copy some logic and have a bit different implementations if it doesn&rsquo;t change that often in two places and independently.</p> <p><strong>Miłosz</strong> [00:14:00]: Because again, having this generic solution be just very expensive, yeah, so you need to be very conscious of where to use the generic solution, where to when to abstract away the details, and for for what system you need, what will approach.</p> <p><strong>Robert</strong> [00:14:18]: Let&rsquo;s go back again to the problem of complex projects, because I think it&rsquo;s also interesting that if you are working with these more complex projects, sometimes some people from other teams or some new joiners may look at it and think, &lsquo;Oh, guys, you&rsquo;re overcomplicated that so much. You&rsquo;re using so many patterns and it&rsquo;s hard to go over this code and understand how it&rsquo;s working. It may look like this at the beginning when you are working with some more complicated domains. But people may sometimes miss the context here. And sometimes you may be in a rescue project, basically, that earlier failed. Because it was created as a CRUD, you are trying to save this project and implement it in a way that you can tackle this.</p> <p><strong>Robert</strong> [00:15:10]: Essential complexity. And some people looking at that later may be missing this context here.</p> <p><strong>Miłosz</strong> [00:15:22]: Overengineering. Yes. Yeah, but on the other hand, it&rsquo;s also the opposite issue, right? If you oversimplify things from the beginning, it may be too late to change the architecture.</p> <p><strong>Robert</strong> [00:15:38]: And I think it&rsquo;s also sometimes interesting when, for example, the team was changed and most people left this team.</p> <p><strong>Robert</strong> [00:15:47]: If it was a complex project, not overcomplicated, but the person that was trying to change it later. May have in mind that, oh, okay, it&rsquo;s so overcomplicated, let&rsquo;s try to rewrite it. So this person or this thing is spending like their half year or year to rewrite that to just realize at the end that basically, all the complicated or the complex, not complicated stuff is again there. Because when you start to support more and more use cases and edge cases, you&rsquo;ll see that actually all those checks and all those complex parts were necessary there so yeah and they don&rsquo;t go away when you change the approach.</p> <p><strong>Miłosz</strong> [00:16:31]: Even if you change the technical approach, the underlying domain complexity doesn&rsquo;t disappear. All right. And. If you&rsquo;re like a developer focused on simple code, if you like simple code, you might be annoyed that there&rsquo;s new requirements coming in.</p> <p><strong>Miłosz</strong> [00:16:49]: I know you need to change the factor to capture it, but it&rsquo;s just the reality. You need to support the business this way. And however you model it, the complexity is there. And if you try to avoid patterns at all costs and just do it simple, you may end up down the road with some weird solutions, like for example, instead of having a good transaction boundary where you need it, you start running cron . checks for consistency. In your system and report some errors, and for some teams this is like the business as usual approach. Let&rsquo;s just fix it afterwards, but that&rsquo;s so much— you know— with operating efforts. You are bombarded with alerts all the time.</p> <p><strong>Miłosz</strong> [00:17:52]: You need to react and fix the system.</p> <p><strong>Miłosz</strong> [00:17:57]: After the fact, instead of just designing in the first place in a way that it&rsquo;s resilient and I think it doesn&rsquo;t need to be.</p> <p><strong>Robert</strong> [00:18:04]: We don&rsquo;t need to reuse any advanced patterns to avoid that. So sometimes it&rsquo;s basically just some defensive programming. So let&rsquo;s take an example when we&rsquo;re building this accounting system, for example, and you are not validating if, for example, the amounts are positive or non-zero. And you can accept any value and issue thousands of invoices that have a total value of zero. And. Okay, you can later clean it up. You maybe have some chrome that will void them or do whatever with that. But basically, it will be much cheaper to do this defensive programming approach earlier and ensure that it will not propagate further. It&rsquo;s not only basically about uh operating overhead from side of your team, but it may be also some financial team seeing later that, oh, no, we have thousands of invoices that we need to do something about that. And those developers again.</p> <p><strong>Miłosz</strong> [00:19:09]: Right. So you only push around the complexity this way instead of solving it.</p> <p><strong>Robert</strong> [00:19:15]: So you cannot close your eyes and say that this complexity doesn&rsquo;t exist. It will pop up somewhere else and it will be much more expensive to handle at the end.</p> <p><strong>Miłosz</strong> [00:19:24]: Some analogy that comes to my mind is like if you decided you won&rsquo;t have files bigger than 100 lines in your project. And you will have simple files, right? Small files. Sure, it&rsquo;s great, but then you end up with thousands of them. And you push the complexity to the number of files instead of the length of the file.</p> <p><strong>Miłosz</strong> [00:19:47]: Back in the day, there was some big question: how big should be a microservice, and some people suggested metrics like this. This comes to my mind.</p> <p><strong>Miłosz</strong> [00:20:00]: Let&rsquo;s build our huge application into really tiny services. So you solve this complexity of big code base, but now you deal with thousands of code bases.</p> <p><strong>Robert</strong> [00:20:15]: So basically, divide and conquer doesn&rsquo;t always work with software. It can work, but you need to know basically how to split them. I think it may be also an interesting topic for some other episode because, Well, not a surprise. There are some patterns and ways how to detect that. And spoiler alert, it&rsquo;s not as small service as you can because, at the end, you can just end up with big mess and some sagas that you need to orchestrate some really small part of the flow. But I think it&rsquo;s enough for today.</p> <p><strong>Miłosz</strong> [00:20:51]: The problem here is you think the size of the code base is the issue, right? It&rsquo;s similar to what we discussed before. The complexity is in the domain, so you can&rsquo;t just change this. If you have a complex business and it needs a big application to work, this complexity will be there, however you model it. So you just need to make effort to model it in a way that&rsquo;s best to reflect how it works and for teams to work with it.</p> <p><strong>Robert</strong> [00:21:20]: And I think it may be misleading sometimes, because you may feel that splitting this big problem into smaller microservices can actually make this problem simpler, easier to tackle. On the other side, sometimes you may wonder. How?</p> <p><strong>Robert</strong> [00:21:38]: Some application can have, I don&rsquo;t know, 5000 of developers for running and developing such simple application. I know that it&rsquo;s sometimes hard to judge an application from outside because you may not know all the complexity inside. But come on, thousands of developers— it&rsquo;s a ton of people. Splitting it into hundreds of microservices can create an illusion that you just cut this problem into smaller and it&rsquo;s easier. But there are cases when, basically, this application could be one service with 50 times less developers developing it.</p> <p><strong>Miłosz</strong> [00:22:17]: Maybe you won&rsquo;t need a whole platform team then to manage all this complexity and communication between them.</p> <p><strong>Robert</strong> [00:22:25]: And if you add the extreme scalability requirements that are, I think, also often out of nowhere, because really, you can probably handle Twitter-like scale on a single VM. Just a reminder that you can have VMs that have one terabyte of RAM, 96 cores. On GCP, it&rsquo;s even 150 cores per one machine. So come on, you can really handle Twitter scale on this one VM.</p> <p><strong>Miłosz</strong> [00:22:54]: RAM is expensive now, so I don&rsquo;t want to do that.</p> <p><strong>Miłosz</strong> [00:22:58]: So coming back to the essential versus accidental complexity, what is the solution here?</p> <p><strong>Robert</strong> [00:23:05]: So I think for the case with cron. Again, some simple solutions like defensive programming and very very very very very very defensive programming. So you have any input that this not as expected, don&rsquo;t accept that. Don&rsquo;t allow to propagate it further. And the other thing, when you are splitting problems into smaller, also watch out to not split it too granularly. Other thing that is coming into my mind is trying to tackle this essential complexity instead of always splitting that. Because there are some patterns that are helping to tackle that. In, for example, this one service, it can be clean architecture, it can be domain-driven design. Thank you. Very important disclaimer. I&rsquo;m not saying about some dogmatic clean architecture or domain-driven design because it&rsquo;s very easy to go into caricature of those techniques instead of some pragmatic approach. So don&rsquo;t be this person that you see in one video or one presentation or one article.</p> <p><strong>Robert</strong> [00:24:08]: Dig a bit deeper, try to understand what problems those techniques are solving, what problems they are not solving, when to not use them, and apply them in places where you have the essential complexity to not add more accidental complexity.</p> <p><strong>Robert</strong> [00:24:27]: Sounds like a plan.</p> <p><strong>Miłosz</strong> [00:24:28]: Yeah, that&rsquo;s something I would recommend to consider where in the project you use what approach. If there&rsquo;s one approach you use in your entire project everywhere, that&rsquo;s probably an amber flag. Probably something is weird because The most projects don&rsquo;t have only you know, only complex or only simple domains. There will be a mix of some very generic parts that, you know, we can use something simpler for it. And the core domain where you need your most sophisticated patterns.</p> <p><strong>Robert</strong> [00:25:06]: Definitely. And also when you are using some patterns that you read about, you&rsquo;re trying to apply that. Also, if you feel that it doesn&rsquo;t fit, don&rsquo;t try to push it by force because probably it may not be fitting there or maybe you don&rsquo;t understand something about this pattern.</p> <p><strong>Miłosz</strong> [00:25:21]: Sometimes you can look back after implementation and I remember we did it many times. Uh-huh. Uh, which created some domain layer and try to model something in it. And then we look at it and, hey, it&rsquo;s like mostly boilerplate. Maybe we can just simplify it because there is nothing it gives us, no benefit.</p> <p><strong>Robert</strong> [00:25:44]: It&rsquo;s basically a crud. So we just can have one struct getting it from API, put to the database and it&rsquo;s fine.</p> <p><strong>Miłosz</strong> [00:25:51]: We have a mix of these parts of complex domains, complex patterns and some very simple ones. I think it&rsquo;s a healthy sign of a codebase in general.</p> <p><strong>Robert</strong> [00:26:01]: So watch out for being dogmatic. So it&rsquo;s.</p> <p><strong>Robert</strong> [00:26:05]: It&rsquo;s coding, it&rsquo;s not religion.</p> <p><strong>Robert</strong> [00:26:09]: Many people will probably disagree, but it&rsquo;s reality.</p> <p><strong>Robert</strong> [00:26:13]: And I think it would be useful to maybe give you some advice from our experience.</p> <p><strong>Robert</strong> [00:26:20]: What are some, let&rsquo;s name it, diagnostic signals to understand if your project may be over complicated or oversimplified? Because I think we can probably find a couple of signals that may suggest that there is something wrong with this project. Well, I think it&rsquo;s pretty interesting is the fact that those signals may be the same for projects that are oversimplified and overcomplicated. So you may remember that at the beginning we said that it&rsquo;s the same problem, but from another side, basically.</p> <p><strong>Robert</strong> [00:26:58]: It&rsquo;s about half of the codebase in general. So what would be, from maybe your perspective, some red flags that the project is unhealthy? So it may be overcomplicated or oversimplified.</p> <p><strong>Miłosz</strong> [00:27:11]: Probably the king of all would be if you can ship the project fast and do it over time, iterate often, and maintain it— that&rsquo;s a good sign. If you can&rsquo;t do it, something is wrong. So it&rsquo;s like a big number one rule.</p> <p><strong>Robert</strong> [00:27:33]: Some anti-examples here, maybe some old enterprises that are deploying. Every half year, for example.</p> <p><strong>Miłosz</strong> [00:27:40]: Yeah, like the other extreme. Yes. For example, you need to hand over your deployment to someone who does it during the night because it&rsquo;s, I don&rsquo;t know, risky.</p> <p><strong>Robert</strong> [00:27:56]: And you can also do it daily because it&rsquo;s the scenario when you have an essential with accidental complexity. So it&rsquo;s a very old project that has already a lot of edge cases to handle. But it&rsquo;s also overcomplicated because maybe in some places it&rsquo;s not planned strategically properly.</p> <p><strong>Miłosz</strong> [00:28:15]: Yeah.</p> <p><strong>Miłosz</strong> [00:28:17]: So if you can deploy daily, that&rsquo;s probably one red flag. If you fear merging changes or just changing some areas of the project, because you don&rsquo;t know what can happen. That&rsquo;s another red flag, I would say. Whatever the reason, you should be comfortable deploying and managing stuff.</p> <p><strong>Robert</strong> [00:28:40]: So in other words, if your project is crude and you are able to merge it daily or a couple of times a day, and you are not afraid to introduce new changes, it&rsquo;s totally fine. It&rsquo;s probably enough. But for example, when you have some complex domain and you are not able to do that. Well, you may have some excuse that, oh, it&rsquo;s so complicated. We are working with money. We cannot merge every day. But yeah, there it is. The reality is that if you tackle this accidental complexity, if you handle this essential complexity properly, you can even deploy multiple times a day application. Pretty complex and is doing some risky operations.</p> <p><strong>Miłosz</strong> [00:29:21]: Yeah, if you are working with money, I hope you just don&rsquo;t just you know cross your fingers and deploy. But you are confident this will work. If you, if you&rsquo;re not, then probably something is wrong. Maybe the project is oversimplified.</p> <p><strong>Robert</strong> [00:29:37]: Other red flag that I have in my mind is when you have some new joiner in your team and it&rsquo;s taking weeks to onboard this person. So it&rsquo;s kind of connected. Previous points because, if you aren&rsquo;t able to deploy daily, you have fear of touching anything in your project. The same will also apply to new joiners and even more, because you&rsquo;re a new joiner. You see that this project is complex, and/ or people warn you or don&rsquo;t touch this part.</p> <p><strong>Miłosz</strong> [00:30:09]: You don&rsquo;t want to see this.</p> <p><strong>Miłosz</strong> [00:30:14]: I&rsquo;ve seen projects like this.</p> <p><strong>Robert</strong> [00:30:16]: Exactly, exactly. And in a healthy project, I would say that it should be quite safe to fail, basically. And it&rsquo;s connected to defensive programming. Basically, and also having some good infrastructure in place that is maybe doing some Canary releases and rollbacks, but it can be also the opposite. For example, if you don&rsquo;t have application with complex domain, and making mistake is not that big problem, but you have very complicated infrastructure for rollbacks, for Canary releases. It can add a lot of overhead for your deploying pipeline and, in this case, it even cannot deploy the simple application daily because the process of deployment is too heavy compared to what&rsquo;s required for this application.</p> <p><strong>Miłosz</strong> [00:31:05]: Also, if your team is unhappy working with the project. If the team brings up plan for migration or rewrite every other day, that&rsquo;s a solid sign something went very wrong. You&rsquo;re probably on one extreme already and it&rsquo;ll be hard to get out of it.</p> <p><strong>Robert</strong> [00:31:28]: I think many people may say, like, &lsquo;projects like that don&rsquo;t exist.&rsquo; I think it&rsquo;s a nice excuse, but it&rsquo;s not the case. Such projects exist that you can really deploy daily, even if it&rsquo;s not very simple projects.</p> <p><strong>Robert</strong> [00:31:46]: So, to maybe recap.</p> <p><strong>Robert</strong> [00:31:48]: Those indicators a bit. So I think there are some questions that you may ask yourself. So the first one is asking yourself, Can you remove some stuff without fear that you&rsquo;ll break everything, basically?</p> <p><strong>Robert</strong> [00:32:05]: And when you&rsquo;re thinking about those red flags, remember, it&rsquo;s not just because some patterns that some people use sometimes, it may be because premature optimizations, like preparing for scale that will never happen.</p> <p><strong>Miłosz</strong> [00:32:21]: Yeah, that&rsquo;s a lot of accidental complexity there.</p> <p><strong>Miłosz</strong> [00:32:25]: Or focusing on performance before you need it. So instead of choosing something more explicit and easier to grasp, you choose something very optimized, but not really needed at the time.</p> <p><strong>Robert</strong> [00:32:41]: And sometimes it may be also preparation for the future—like, maybe at some point we&rsquo;ll need X. Let&rsquo;s do it now, so it will be easier. Unfortunately, usually it&rsquo;s a trap. Usually it will never happen, or if this functionality will be needed, if we will need to add this functionality, we&rsquo;ll find out that actually what we prepared earlier no longer works in this way. And we spent a lot of time on writing that and maintaining it. So, jagni.</p> <p><strong>Miłosz</strong> [00:33:10]: Yes, it&rsquo;s easier to get things that are easy to remove or replace instead of looking for all the possible edge cases.</p> <p><strong>Robert</strong> [00:33:19]: Always have in mind that most of the techniques, tools, or whatever you are using, you&rsquo;re using it to make your development lifecycle as efficient as you can. So, if you see that but something is making your development lifecycle worse, slower, and it&rsquo;s slowing you down, you should maybe rethink if it&rsquo;s needed or you&rsquo;re applying it in the right way, or maybe it&rsquo;s not needed at all. And at the end, you should aim to making some small incremental changes. And I know that it sounds easier than it is, it&rsquo;s easier to say than do. But it&rsquo;s a matter of experience and learning, learning and learning, and trying to be as close to this go to deploy multiple times a day. And also, watch out to not go into the trap that we already invested couple of months into this feature to make it super scalable. It doesn&rsquo;t matter that we need to spend now weeks on maintaining that.</p> <p><strong>Robert</strong> [00:34:21]: Watch out for this trap. Don&rsquo;t be afraid to remove some stuff that is slowing you down.</p> <p><strong>Robert</strong> [00:34:29]: Yes. So it&rsquo;s important to be aware of that. So if you already invested a lot of time into something, it&rsquo;s harder to work some framework. Some generic library trying to do everything. Some stocks. But it&rsquo;s showing it similarly. Like you invested in something. Company and you see that it went down, and you are waiting years to recover. And probably just saying that and buying some better stock can help more. By the way, not investing advice.</p> <p><strong>Robert</strong> [00:35:03]: I was about to say that, yeah.</p> <p><strong>Robert</strong> [00:35:07]: And at the end, I think it&rsquo;s important to emphasize one thing.</p> <p><strong>Robert</strong> [00:35:12]: Don&rsquo;t be dogmatic about using any tools because it&rsquo;s never ending up properly.</p> <p><strong>Robert</strong> [00:35:22]: Have an analogy of some worker that is, I don&rsquo;t know, big fan of pneumatic hammer, and this person is just using pneumatic hammer everywhere. And it may work if this person is demolishing buildings, but if you need to nail nails, well, probably using nomadic armor for that. At best, you&rsquo;ll hurt yourself.</p> <p><strong>Robert</strong> [00:35:44]: But in this case, it would be just more practical to use the normal hammer. But from the opposite side, maybe there is some fun in using a normal hammer. Saying that a normal hammer is great and it&rsquo;s simple— it doesn&rsquo;t need any other energy source. You can just use it everywhere. But it&rsquo;s true. However, if you need to demolish a wall, it will take ages.</p> <p><strong>Miłosz</strong> [00:36:07]: For example, something that we mentioned in a previous episode about having a toolbox. You can choose your tools basically for the job.</p> <p><strong>Robert</strong> [00:36:16]: And even if it sounds, maybe, very obvious. From other side, we&rsquo;ve met a lot of people that were like these people with simple hammer or pneumatic hammer used for everything, and even if it sounds that obvious, they were doing it in this way.</p> <p><strong>Miłosz</strong> [00:36:38]: So it helps if you ask yourself, why are you doing this? Why use this tool in the first place? If you want to solve something specific, for example, if you have a complex domain and that you want to model better, that&rsquo;s probably a good reason to use something. Or if you kick off a project that you know is supposed to be a big one, you probably want to have some idea of the architecture of the design beforehand. But on the other hand, if you just kick off an MVP, you don&rsquo;t need all this stuff. You can just start simple and grow.</p> <p><strong>Miłosz</strong> [00:37:23]: and yeah watch out for extremes, what we mentioned before. So if you are far on the over complicated or over simplified spectrum, the project probably will be difficult to work with.</p> <p><strong>Robert</strong> [00:37:40]: And the thing that we can probably recommend most, and we are saying it over and over, but play with technics sometimes in work, sometimes after work, also. So if you would like to be good at what you are doing, it&rsquo;s it&rsquo;s also good to practice this outside of work, because you&rsquo;re also sometimes limited by the projects or maybe sometimes you&rsquo;re limited by some dogmatic people that, for example, like oversimplified code and you cannot do much about that, but you can always experiment a bit in in the home. So one thing that we can probably recommend is checking our blog tree . tech and where we have a lot of articles that are totally free. We also created one pretty big Go project that you can play with and try to apply. Look how we applied some patterns there. And you can also try to play with this project and add some of your features there. And you can gain this muscle memory on how to use those patterns and later, when you will encounter some problem, you&rsquo;ll have some tools in your toolbox and you&rsquo;ll be able to experiment with that.</p> <p><strong>Miłosz</strong> [00:38:51]: So we tried to create more complex examples, so you can see the &lsquo;why&rsquo; behind the patterns, not just some toy projects where they don&rsquo;t make sense.</p> <p><strong>Robert</strong> [00:39:01]: Yeah, because if you have sometimes those dogmatic people about using some patterns, you can notice that very often they have very shallow knowledge because they&rsquo;ve been reading some article. That again, are very shallow. So it&rsquo;s like five minutes read. And they are thinking that they know everything about some technique after doing that, or doing it differently. So our articles are not simplifying things, but there are—pretty long, but they are long enough to prepare you for applying some patterns in the wild. There are some good articles out there. It&rsquo;s not like only our articles are good, but very often there are some articles that are very, very shallow. And when you look on them, you can just see that applying what&rsquo;s suggested there will introduce more troubles and this accidental complexity without solving any problem actually. So we would have your base complexity with accidental complexity, and at the end it will be how to work with.</p> <p><strong>Miłosz</strong> [00:40:01]: So we also have the Go with the Domain ebook about working with domain-driven design in Go. And it shows refactoring of a bigger codebase.</p> <p><strong>Miłosz</strong> [00:40:14]: This is not a toy project, but more like a real-world scenario.</p> <p><strong>Robert</strong> [00:40:19]: And it&rsquo;s, I think, already pretty popular. So last time when I was checking it, it had over 60,000 downloads. And we also received a lot of emails from people that it helped them a lot in the project. So for some people, it&rsquo;s quite useful.</p> <p><strong>Robert</strong> [00:40:34]: Check it out. It&rsquo;s also free by the way, so just google go with the domain and I&rsquo;m sure you will find that Cool. So I think that will be all for today. So thank you Miłosz. Thank you Robert. See you next time. That&rsquo;s it.</p>DDD: A Toolbox, Not a Religionhttps://threedots.tech/episode/ddd-toolbox-not-religion/Wed, 10 Dec 2025 12:00:00 +0000https://threedots.tech/episode/ddd-toolbox-not-religion/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Domain complexity matters more than technical complexity</strong> - Most projects fail not because of technical challenges, but because they don&rsquo;t handle the business domain well.</li> <li><strong>DDD is a toolbox, not a religion</strong> - You don&rsquo;t need to use every pattern from Domain-Driven Design. Pick what solves your actual problems.</li> <li><strong>Start with the domain model</strong> - Understanding how the business works is more important than designing the perfect schema.</li> <li><strong>Avoid solving imaginary problems</strong> - Spending months on frameworks or platforms before building actual features often leads to wasted effort.</li> <li><strong>Strategic patterns are essential for everyone</strong> - Even if you don&rsquo;t use tactical DDD patterns, thinking about core domains and module boundaries matters in every project.</li> </ul> <h2 id="introduction">Introduction</h2> <p>In this episode, we discuss why software projects become legacy code that nobody wants to touch.</p> <p>We talk about how Domain-Driven Design can help, but also why it&rsquo;s often misunderstood or overused.</p> <p>Instead of treating DDD as an all-or-nothing approach, we suggest to use the ideas pragmatically - picking the patterns that solve real problems in your project.</p> <p>We share stories to show how focusing on domain complexity rather than technical complexity leads to better software.</p> <h2 id="notes">Notes</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F" target="_blank">Go With the Domain ebook</a> - our guide to applying DDD ideas in Go</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fthe-domain-engineer%2F" target="_blank">The Domain Engineer training</a> - launching early 2026</li> <li>Domain-Driven Design (The Blue Book) by Eric Evans - the original book that introduced DDD almost 25 years ago</li> <li>Design Patterns book by Gang of Four - classic software design patterns book</li> <li>Event Storming - collaborative workshop technique for understanding domains (covered in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fepisode%2Fbecoming-a-product-engineer%2F" target="_blank">the previous episode</a></li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>The bold assumption for today&rsquo;s episode is that you can go deep into many of the ideas and they shine in complex projects, but very often you can use some more basic version of the key ideas and apply it to most projects actually.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>If you are hiring specialists in many fields, the sentence &lsquo;if you have hammer in your hand, you will see nails everywhere&rsquo; may happen. They will spend a ton of time on solving problems that may be not the problem in this company yet.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>If you consider most of applications, like SaaS apps, they are usually very similar. If you change jobs, you can end up at another completely different company and use the same tech stack. It&rsquo;s not much different to use the same programming language. But on the other hand, what the company does can be completely different, and this is the differentiating factor between them.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>The worst thing about that is sunk cost fallacy. So you&rsquo;re spending more and more time on maintaining that, losing a lot of resources, time, and nerves on maintaining that. But it&rsquo;s no longer needed.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>We spent many hours creating this messaging framework using generics. And in the end, we had this good interface that you can very easily publish an event, very easily subscribe to it. And the result was that it was probably the only game jam we never finished.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>You should use proper technique for a proper problem. That&rsquo;s it.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>If you boil DDD down to one primary theme, I would say something like it&rsquo;s about understanding the domain you work with and then modeling it well in the code. Whether you call it DDD or not, it&rsquo;s like a mindset.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>It&rsquo;s kind of forcing you to think, &lsquo;Okay, is this code really important to my business, or is it less important?&rsquo; And it can kind of affect how we are thinking about this code.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DlZ2KTDaJLxc%26amp%3Bt%3D37s">00:00:37 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DlZ2KTDaJLxc%26amp%3Bt%3D258s">00:04:18 - Technical vs domain complexity</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DlZ2KTDaJLxc%26amp%3Bt%3D733s">00:12:13 - Technology choices</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DlZ2KTDaJLxc%26amp%3Bt%3D1003s">00:16:43 - Framework trap</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DlZ2KTDaJLxc%26amp%3Bt%3D1272s">00:21:12 - What is DDD?</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DlZ2KTDaJLxc%26amp%3Bt%3D1896s">00:31:36 - Core theme of DDD</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DlZ2KTDaJLxc%26amp%3Bt%3D2302s">00:38:22 - Context matters</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DlZ2KTDaJLxc%26amp%3Bt%3D2581s">00:43:01 - Domain model</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DlZ2KTDaJLxc%26amp%3Bt%3D3120s">00:52:00 - Wrap up</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Miłosz</strong> [00:00:37]: Hello, I am Miłosz.</p> <p><strong>Robert</strong> [00:00:38]: And I&rsquo;m Robert. And this is No Silver Bullet Podcast, where we discuss mindful backend engineering. In this show, we share takes on software engineering that will help you to grow into principal engineer level.</p> <p><strong>Miłosz</strong> [00:00:51]: Let&rsquo;s talk about the usual arc of a software project.</p> <p><strong>Miłosz</strong> [00:00:55]: So first we have the kickoff, and now we have high hopes for good design and this product will be great and everything is fine. This time we&rsquo;ll do it. Right. Yeah. And then we work on it for some time. Then we deploy it to production. Everything is great. And suddenly it becomes a legacy project no one wants to touch. Did you ever see this happen?</p> <p><strong>Robert</strong> [00:01:20]: Every time. Okay, not every time, but yeah, it&rsquo;s very often the case.</p> <p><strong>Miłosz</strong> [00:01:27]: So it&rsquo;s interesting, right? Why does it happen? Why every project ends up as this big ball of mud no one wants to work with.</p> <p><strong>Miłosz</strong> [00:01:40]: And the first version works quite well. We come up with something, it&rsquo;s pretty much what we wanted, that&rsquo;s fine.</p> <p><strong>Miłosz</strong> [00:01:48]: And then new features come in and very often they are quite different than what the software did in the first place. Right?</p> <p><strong>Miłosz</strong> [00:01:59]: So we need to change the design. But very often there is not enough time or developers are lazy or they don&rsquo;t feel like it, whatever. But they don&rsquo;t remodel everything to fit the new design. Instead, we add some workarounds or hacks and it works pretty well.</p> <p><strong>Miłosz</strong> [00:02:20]: For a while, except that maybe some bugs, maybe some unusual behaviors. And we repeat it over and over, over months or years. And then we arrive at the state that we are afraid to touch the codebase. Is there a way to fix it?</p> <p><strong>Robert</strong> [00:02:39]: Oh. It. Wouldn&rsquo;t be surprising to say that it&rsquo;s a problem that software always had. So probably there are some magic techniques that are trying to solve this problem. One of the most popular, let&rsquo;s say, tools or techniques that are trying to approach that is domain-driven design. What?</p> <p><strong>Robert</strong> [00:03:00]: If you heard about Domain Driven Design, you probably also noticed that for some reason it&rsquo;s not the default way of many teams to work. From one side we have some solution, from other side probably didn&rsquo;t solve all the issues that we have.</p> <p><strong>Miłosz</strong> [00:03:16]: And it&rsquo;s also sometimes confusing; sometimes it seems like something you would use only for enterprise projects, but actually it worked for us before. So we like talking about it and sharing the ideas.</p> <p><strong>Robert</strong> [00:03:29]: And look how people are surprised sometimes to hear that it worked for us because many people tried to use domain-driven design and it introduced more mass for them. Or maybe they worked in a project when somebody tried to use domain-driven design and it&rsquo;s the worst experience that they have. Interesting.</p> <p><strong>Miłosz</strong> [00:03:45]: Mm.</p> <p><strong>Miłosz</strong> [00:03:47]: So the bold assumptions for today&rsquo;s episode are that you can go deep into many of the ideas and they shine in complex projects, but very often you can use some more basic version of the key ideas of it and apply it to most projects actually.</p> <p><strong>Robert</strong> [00:04:06]: So— In other words, in this episode, we will cover How to not use domain-driven design, but some part of domain-driven design and how to make them useful in your projects.</p> <p><strong>Miłosz</strong> [00:04:18]: First, let&rsquo;s think about what is difficult about building software.</p> <p><strong>Miłosz</strong> [00:04:24]: So let&rsquo;s consider someone wants to start. and they want to learn some programming language right, what would you recommend them to learn?</p> <p><strong>Robert</strong> [00:04:34]: Maybe not, why, what I would recommend, but what people are using.</p> <p><strong>Miłosz</strong> [00:04:37]: Maybe it was a default, like us were here.</p> <p><strong>Robert</strong> [00:04:40]: So. They probably need some programming language, right? Yeah. Kubernetes, OOP design patterns.</p> <p><strong>Miłosz</strong> [00:04:48]: There are some frameworks, some good practices, some algorithms, maybe data structures, stuff like that.</p> <p><strong>Robert</strong> [00:04:55]: Useful stuff that you are using on interviews, in other words. Exactly. And it&rsquo;s actually, I think, pretty interesting that a lot of learnings nowadays is focused around things that are useful for interviews, but on the other side they are not that relevant when you are working in a job. And I would say that it&rsquo;s even worse because you are solving problems that normally you should not solve in the problems that you are working like.</p> <p><strong>Robert</strong> [00:05:23]: You should probably not implement your own databases and you implement binary trees because most graphics have that out of the box.</p> <p><strong>Miłosz</strong> [00:05:31]: But at the same time, if this person wants to learn, wants to join a company, they probably need to learn some computer science theory, like what is big O notation. How to write optimized algorithms for sorting, how to create data structures, stuff like that.</p> <p><strong>Robert</strong> [00:05:52]: It&rsquo;s useful.</p> <p><strong>Miłosz</strong> [00:05:54]: Yeah, it is. And it&rsquo;s often what you need to know to join a company. But my question is, is it enough to ship good software?</p> <p><strong>Robert</strong> [00:06:03]: Well, I would say that it&rsquo;s not enough, and probably it&rsquo;s even not needed, because well, we are people who are self-taught, so we&rsquo;ve been learning programming ourselves, and we&rsquo;ve been shipping multiple projects without that, basically, and many people were using that. They were maybe not perfect, but for the other side, they worked.</p> <p><strong>Miłosz</strong> [00:06:27]: Yeah, so I would say it&rsquo;s like half of the skills you need.</p> <p><strong>Miłosz</strong> [00:06:32]: And it&rsquo;s useful, it&rsquo;s needed. It&rsquo;s often what we focus on. But there&rsquo;s also the other half of stuff you need— to ship good software. So what we discussed so far is the technical complexity, right? So making software fast and scalable, and optimizing memory usage.</p> <p><strong>Miłosz</strong> [00:06:54]: Stuff like this.</p> <p><strong>Robert</strong> [00:06:55]: Probably we can put it into the bucket of software engineering. So this is this hard part. Maybe not hard, but this. Yeah, this low-level part maybe. Yeah.</p> <p><strong>Miłosz</strong> [00:07:06]: The coding part.</p> <p><strong>Miłosz</strong> [00:07:09]: And the other half, how would you call it?</p> <p><strong>Robert</strong> [00:07:14]: So if you are working with product, probably product engineering.</p> <p><strong>Miłosz</strong> [00:07:18]: Yeah. So the product or domain challenges. So basically figuring out how the thing should work and what should it do.</p> <p><strong>Robert</strong> [00:07:29]: So this product engineering or those domain challenges, it&rsquo;s less about those technical parts, but rather thinking.</p> <p><strong>Robert</strong> [00:07:42]: How to tackle complexity, how this feature should work, maybe even thinking if it will be useful. Because if you know if something is not useful, you can remove some functionality and decrease complexity with that.</p> <p><strong>Robert</strong> [00:07:58]: It&rsquo;s also this: less priced part, let&rsquo;s say, and that it&rsquo;s connected to communicating to people.</p> <p><strong>Robert</strong> [00:08:06]: Yeah, figuring out together what you want to build.</p> <p><strong>Miłosz</strong> [00:08:11]: Maybe also talking to your users?</p> <p><strong>Robert</strong> [00:08:13]: I think it&rsquo;s also partly related to some long-term maintenance. So it&rsquo;s, I would say, partially part of the software engineering, but I think it&rsquo;s often lost somewhere because, when you have software that is not that maintainable from other side, it&rsquo;s also challenging. And I would say that a lot of people that have this enduring mindset, they like challenges. So if software is complicated, it&rsquo;s great because we have a lot of challenges there to solve.</p> <p><strong>Miłosz</strong> [00:08:45]: Yeah, the technical parts are very interesting to solve.</p> <p><strong>Miłosz</strong> [00:08:49]: We like puzzles. At least I know I like them too. And the technical parts often have a very specific answer to them. Mm-hmm.</p> <p><strong>Miłosz</strong> [00:09:01]: For example, we can have an SQL query, you can benchmark it, and then you can come up with a faster query. And it&rsquo;s very easy to compare if it takes 2 seconds or 10 milliseconds and it feels great to have this measurable progress. But for many businesses, it&rsquo;s not really critical at all. If your website handles a request in 50 or 70 milliseconds— or whatever it is— usually not a huge difference, except for the first page load, which is critical. But for many web applications, saving some data takes one second longer or shorter; it&rsquo;s not a huge deal and probably not worth spending days on.</p> <p><strong>Miłosz</strong> [00:09:52]: coding on.</p> <p><strong>Robert</strong> [00:09:53]: As long as cost is not the important part, but again, it&rsquo;s also some subset of products. For many products, it&rsquo;s not that important at the end.</p> <p><strong>Miłosz</strong> [00:10:03]: Exactly.</p> <p><strong>Miłosz</strong> [00:10:05]: But for most businesses, the hard part is taking the problem domain they need to solve and representing this in code.</p> <p><strong>Miłosz</strong> [00:10:17]: Especially if the problem domain itself is complex.</p> <p><strong>Miłosz</strong> [00:10:23]: And usually the real world is messy in itself. Right. So we have no easy way to compare if something works or not, like with the technical parts. We often need to experiment, see if something sticks, see how users use it.</p> <p><strong>Robert</strong> [00:10:47]: Basically, the feedback loop is much, much longer and it&rsquo;s not that obvious. Like you said with SQL query, it&rsquo;s easy to measure if SQL query is faster or not. But obviously you can measure some customer satisfaction or some feature, but it&rsquo;s more indirect and it&rsquo;s usually taking a bit longer to get back to you. And it&rsquo;s also a bit harder to understand sometimes how it&rsquo;s happening because you can change some functionality. And notice that, okay, some behavior changed, but sometimes you may have that many changes around it—so it&rsquo;s hard to judge if it&rsquo;s because of it, actually.</p> <p><strong>Miłosz</strong> [00:11:28]: Yeah, so it&rsquo;s much more vague. And if you consider most of applications, like SaaS apps, they are usually very similar. If you change jobs, let&rsquo;s say you used to work on web application using Go and TypeScript.</p> <p><strong>Miłosz</strong> [00:11:50]: You can end up at another completely different company and use the same tech stack. It&rsquo;s not much different to use the same programming language.</p> <p><strong>Miłosz</strong> [00:12:00]: Same framework, basically, protocols, databases are very similar. They don&rsquo;t change much, but on the other hand, what the company does can be completely different, and this is the No.</p> <p><strong>Miłosz</strong> [00:12:13]: the differentiating factor between them. It&rsquo;s sometimes funny how companies try to make their technical choices something that makes them distinct. So I don&rsquo;t know if you remember— About 10 years ago, there was this hype on MongoDB, for example. Oh yeah.</p> <p><strong>Miłosz</strong> [00:12:32]: And some companies were very open about it.</p> <p><strong>Robert</strong> [00:12:39]: Proud. So when you&rsquo;ve been sending CVs and it was the job posting, it was &lsquo;Yes, we are using MongoDB. Yes, we are web-scale, but of course, it did not really matter in the end. It&rsquo;s so absurd. But it&rsquo;s also interesting because during that time, some people were saying that it&rsquo;s absurd, but from other sites, many people were following that and saying, &lsquo;Yeah, it&rsquo;s so great.&rsquo; You&rsquo;re using MongoDB, your web scales.</p> <p><strong>Robert</strong> [00:13:08]: Let&rsquo;s see what we&rsquo;ll see 10 years.</p> <p><strong>Miłosz</strong> [00:13:12]: You can just as well use JSON columns in Postgres and achieve pretty much the same thing, except for some differences in very specific cases.</p> <p><strong>Robert</strong> [00:13:22]: Or losing changes because of inconsistencies between nodes.</p> <p><strong>Miłosz</strong> [00:13:28]: Yeah, but basically, if you take two companies that do the same thing and one uses one database and the other MongoDB, whatever.</p> <p><strong>Miłosz</strong> [00:13:39]: It probably won&rsquo;t impact that much the end result. How well they do in business, how many users they have, how good the product is.</p> <p><strong>Miłosz</strong> [00:13:49]: There&rsquo;s a lot more factors in it.</p> <p><strong>Robert</strong> [00:13:52]: Okay. In other words, we can say that many companies, many people maybe solve on the wrong thing because when starting your project, they&rsquo;re thinking, &lsquo;Okay, this time we&rsquo;ll use this framework, this database.&rsquo;</p> <p><strong>Miłosz</strong> [00:14:06]: Yeah, very different this time.</p> <p><strong>Robert</strong> [00:14:08]: Yeah, yeah.</p> <p><strong>Robert</strong> [00:14:10]: This time everything will be great. And again, we are in the cycle of starting in the greenfield and ending up with legacy, like: It was done five years ago, 10 years ago. So probably.</p> <p><strong>Robert</strong> [00:14:28]: The technology that we&rsquo;re using— the framework, database, maybe not the most important indicator if project is successful or not, and by &lsquo;successful&rsquo; I mean multiple things. So, if it&rsquo;s maintainable, if it has users, if it makes money— probably from perspective of developers, the maintenance is the most important part. But again, it&rsquo;s not the only thing.</p> <p><strong>Miłosz</strong> [00:14:54]: Yeah, but we treated that as something that&rsquo;s a core thing we need to solve. Sometimes. Right.</p> <p><strong>Miłosz</strong> [00:15:03]: I remember some companies with the idea that we will dedicate top talent to this core platform framework— or something— and once we solve it, then adding new features and apps to it will be trivial. Just hire anyone who&rsquo;s able to code in this language, and they will just use this great framework to implement a new feature in one day.</p> <p><strong>Miłosz</strong> [00:15:34]: I don&rsquo;t know where this idea comes from, but I think it&rsquo;s quite common.</p> <p><strong>Robert</strong> [00:15:40]: And I think one part of the problem may be that if you are hiring specialists in many fields, the sentence &lsquo;if you have hammer in your hand, you will see nails everywhere&rsquo; may happen. I mean, if you find hiring specialists from multiple areas, they will spend a ton of time on solving solving problems that may be not the problem in this company yet. So, for example, this startup and it&rsquo;s starting. They don&rsquo;t have scale yet. And probably it could be run on a single VM. And later, you have, for example, some Kubernetes specialists. So what they can do, they can create multiple Kubernetes clusters for multiple environments and later serve. Requests per second and even 10 requests per second. It&rsquo;s like nothing. You can do it with a single VM, but it&rsquo;s not for free. Obviously, you are using your top talents, but for solving problems that are not important.</p> <p><strong>Miłosz</strong> [00:16:43]: And I remember myself falling into this trap, probably multiple times. But one story that I remember very well is how I used to run some game jams with friends, just making game in two days, right? And one time we wanted to create a story-driven game with a lot of dialogues, so you can choose dialogue options and it leads you to a different part of the game. Somehow we came up with the idea that we will do it event-driven. So each option will be an event that sends and triggers something. We had this idea that, now, this would be a lot of events, which will be hard to manage. So we will need this even during the framework. So, if only we get this framework right, all that&rsquo;s left is to implement the dialogues.</p> <p><strong>Miłosz</strong> [00:17:30]: That would be it. So we spent many hours creating this messaging framework using generics.</p> <p><strong>Miłosz</strong> [00:17:40]: C# and I didn&rsquo;t work very well. So it was super fun to come up with it. And in the end, we had this good interface that, you know, we just, you can very easily publish an event. Very easily subscribe to it. And the result was that it was probably the only game jam we never finished. We had this technical solution, but we didn&rsquo;t progress with the game at all.</p> <p><strong>Robert</strong> [00:18:05]: So in other words, you are not the best specialist to be hired to build event-driven games. Exactly. Exactly.</p> <p><strong>Robert</strong> [00:18:12]: I can create a framework for you.</p> <p><strong>Miłosz</strong> [00:18:14]: I think it shows. It&rsquo;s like a very condensed, you know. Scenario that I think happens on bigger scale as well, and I don&rsquo;t even know where it comes from, but somehow we tend to think, if only we can create this framework first, and everything will be easy.</p> <p><strong>Robert</strong> [00:18:36]: And sometimes, if you succeed in creating this framework or whatever it is, this prerequisite, and you&rsquo;re starting to rework on the things that matter.</p> <p><strong>Robert</strong> [00:18:47]: You&rsquo;re starting to notice that, okay, actually, it&rsquo;s totally not needed.</p> <p><strong>Robert</strong> [00:18:52]: We just spent two months on that. And you know what&rsquo;s the worst thing? The worst thing about that is sunk cost fallacy. So you&rsquo;re spending more and more time on maintaining that, losing a lot of resources, time, and nerves on maintaining that. But it&rsquo;s no longer needed.</p> <p><strong>Miłosz</strong> [00:19:12]: Yeah, and you don&rsquo;t want to get rid of it because you invested so much time. Yes, I think we&rsquo;ve seen many examples like that, right? I think it&rsquo;s very common when you have a platform team in a company that is supposed to make all teams work together. Very similar ways this is exactly the idea of having one great system that you just add tiny pieces to, and of course it makes sense for a bigger company. But sometimes you can get go too far. I remember one time we had the platform team create service mesh for developers. To use it, you have service discovery so you can communicate between services using just service names. Sounds great, and they spent half a year working on it. But first of all, no one asked for it.</p> <p><strong>Miłosz</strong> [00:20:07]: And we really didn&rsquo;t have any use for it later. It just looked great on paper.</p> <p><strong>Robert</strong> [00:20:13]: I think we even mentioned that. Yeah, but maybe we don&rsquo;t need that because we already are doing it in Middleware.</p> <p><strong>Miłosz</strong> [00:20:21]: Maybe if you have five services, maybe you can just call it with the URL of Kubernetes and wow, it&rsquo;s enough.</p> <p><strong>Robert</strong> [00:20:29]: Yeah, and all the cross-cutting concerns could just handle it with common library and middlewares. That&rsquo;s it. It&rsquo;s probably again, it depends on context. If you have a super complex project with multiple programming languages, maybe. But in this case, we just have one technology there and it solved the problem already.</p> <p><strong>Miłosz</strong> [00:20:52]: So this is all the first part, the technical part you mentioned. Sometimes it&rsquo;s easier than the domain part, the product part.</p> <p><strong>Robert</strong> [00:21:00]: And it&rsquo;s very often. You can move it between projects as you said earlier. So it can be very similar across and you don&rsquo;t need to reinvent the wheel basically. So we already know that this technical part is very often similar between the projects and the thing that is different is the domain.</p> <p><strong>Robert</strong> [00:21:24]: part of the project. And we already mentioned once domain-driven design, that it&rsquo;s supposed to be helping us with this.</p> <p><strong>Robert</strong> [00:21:33]: Think that is unique to our company. and many of you, maybe heard about this earlier, but it&rsquo;s not.</p> <p><strong>Robert</strong> [00:21:43]: wide load up. do you have some ideas why it&rsquo;s like that? and actually, what the point of indesign at the first point?</p> <p><strong>Miłosz</strong> [00:21:51]: maybe it&rsquo;s even difficult to define what the approach really is in a very short description. and i remember before i knew about the dd. i think my first contact was when my manager wanted to have the entire team join the training for off-domain design.</p> <p><strong>Miłosz</strong> [00:22:17]: and they sent me over like a pdf description of it with lots of details what the training is about and i remember reading through the document.</p> <p><strong>Miłosz</strong> [00:22:29]: and at the end i was like, what is this? i have no idea. How is this supposed to help me with my day-to-day work? I just completely had no idea what.</p> <p><strong>Robert</strong> [00:22:41]: What&rsquo;s the idea? So it was more covering some non-technical things and at this point you were thinking, &lsquo;Okay, it doesn&rsquo;t matter&rsquo; or &lsquo;it was not sold properly,&rsquo; let&rsquo;s say.</p> <p><strong>Miłosz</strong> [00:22:51]: Yeah, I think it was missing the &lsquo;why.&rsquo; And I think also DDD has a lot of unique names or terminology that you didn&rsquo;t hear before. So if someone tells you, you will learn about bounded contexts. You&rsquo;re like, &lsquo;What?&rsquo;</p> <p><strong>Robert</strong> [00:23:10]: I know about design patterns, but what is this? Or maybe aggregate and, oh, okay, so I can aggregate some data.</p> <p><strong>Robert</strong> [00:23:20]: Okay, another reason why it&rsquo;s not that widely adopted, and maybe now it&rsquo;s very often missing interpret. It&rsquo;s also the reason that point-driven design is out there already for a pretty long time. So the blue book, so the book that was introduced it. Introducing Domain Driven Design was released almost 25 years ago and the programming world 25 years ago was totally, totally different.</p> <p><strong>Miłosz</strong> [00:23:54]: And it was based on years of experience from Eric Evans, so I guess you can say it was about software written in the 90s. So it sounds like a different era today, kind of.</p> <p><strong>Miłosz</strong> [00:24:09]: But on the other hand, you can still hear about the DD a lot. Maybe not every company practices it, but there are conferences and books dedicated to it.</p> <p><strong>Miłosz</strong> [00:24:23]: So, probably suggest to you that there are many good ideas. That&rsquo;s how evolution works. Good ideas tend to survive for longer.</p> <p><strong>Robert</strong> [00:24:32]: Unfortunately, there&rsquo;s also a side that many good ideas are misunderstood with time. So some good ideas are useful at the beginning. More people are trying to adopt them. But unfortunately, if those good ideas are not straightforward, but require bigger investment to understand them. Some people tend to get shortcuts and not use them as intended or use them everywhere and it&rsquo;s quick ending up with some kind of caricature of some techniques, tools. So in other words, many techniques or tools at the beginning were good ideas and were solving some problems, but with time, when people were adopting that, they were starting to adopt some caricature of this technique and it&rsquo;s often started to do more harm than good. It was visible in discussions that we have in many teams that some people have seen domain-driven design in some projects, but it wasn&rsquo;t really domain-driven design what they see.</p> <p><strong>Robert</strong> [00:25:38]: They&rsquo;ve seen some overengineering, maybe, just doing it for the sake of doing it. Yeah, so using it in places when it wasn&rsquo;t needed. So it was just too simple application maybe to use that. And to make it worse, it wasn&rsquo;t used in there. Right way, because the initial investment was just a big end. People tend to not spend enough time to learn something and, Well, it&rsquo;s easy to imagine that after 25 years, many, many of projects like that was created and it created a lot of bad PR.</p> <p><strong>Miłosz</strong> [00:26:17]: And you can hear people saying, &lsquo;I don&rsquo;t want to work like this ever again,&rsquo; because it was a disaster. It was a super complex project that was made overcomplicated.</p> <p><strong>Robert</strong> [00:26:28]: The similar situation is also, for example, with clean architecture, when we sometimes hear that, &lsquo;ah, no, clean architecture doesn&rsquo;t work,&rsquo; and later we hear about seven layers and.</p> <p><strong>Miłosz</strong> [00:26:39]: Yes, but on the other hand, if you read the introduction to the Global. I think the core ideas are still very relevant. The introduction describes the state of most software projects. How they get stuck, what are the issues. And now it&rsquo;s almost 25 years later, everything pretty much in the same spot.</p> <p><strong>Miłosz</strong> [00:27:08]: Sense. We got better with frameworks and some technical stuff, but the core idea of working with people on the product.</p> <p><strong>Miłosz</strong> [00:27:21]: Not that much different.</p> <p><strong>Robert</strong> [00:27:22]: Are you suggesting that we are reinventing the wheel in software every 10 years? No, it&rsquo;s not possible. And it&rsquo;s 25 years during. since releasing the book. I can imagine that we reinvented the wheel at least twice since that time.</p> <p><strong>Miłosz</strong> [00:27:41]: microservices and then ditched them. Twice. It was a full cycle. For example, in the Eric Evans book, he says that XML would be a promising technology to communicate between services. Yeah, it&rsquo;s something fresh to send XML over RPC. Yeah, so since then we probably reinvented this idea a few times. We&rsquo;ve.</p> <p><strong>Miłosz</strong> [00:28:10]: microservices and protocol buffers over gRPC. It&rsquo;s still the same. Coming back to the question why it&rsquo;s difficult to understand what DDD is, it&rsquo;s very broad. It&rsquo;s like an entire book of many, many ideas. and from design patterns to communication between teams. So it&rsquo;s very difficult to say what doing GDD means. It&rsquo;s just very broad method.</p> <p><strong>Robert</strong> [00:28:43]: We can also say that it may be philosophical in some way. Hmm, hmm.</p> <p><strong>Miłosz</strong> [00:28:48]: Yeah, and you can interpret the patterns in many ways.</p> <p><strong>Robert</strong> [00:28:54]: The domain part of projects can differ between projects. So it&rsquo;s hard to give one solution that fits all. So it&rsquo;s probably because of that, but it&rsquo;s also creating a lot of space to interpret things. Hmm. It remains one thing. If you are with us for longer, you maybe already know that some time ago we decided to leave our daily jobs. And now we are doing our own business. But it&rsquo;s not the first time when we&rsquo;ve been trying to do that. And we&rsquo;ve been trying to do it multiple times earlier. But I think. One thing that probably didn&rsquo;t allow us to succeed earlier was that we didn&rsquo;t spend enough time to understand actually how to run business.</p> <p><strong>Robert</strong> [00:29:43]: How it actually worked that you&rsquo;re running a successful business. So maybe you had some idea that if you would like to have a successful business, you need to sell things. Uh, do some marketing, do some sales, and earn more than you are spending.</p> <p><strong>Miłosz</strong> [00:29:58]: Easy, but just— you just need to read one business book, yeah, which is called &lsquo;Business&rsquo; and explains how to do business.</p> <p><strong>Robert</strong> [00:30:07]: Yeah, but in practice, it&rsquo;s not that simple and it&rsquo;s more sophisticated, let&rsquo;s say. And if you will not spend enough time to understand how it works deeper.</p> <p><strong>Robert</strong> [00:30:20]: The chance that your business will fail is much, much higher. I think the statistics are like in five years, 50% of businesses are failing, and in terms of startups, it&rsquo;s probably 80% in Twitter or something like that. So it&rsquo;s a crazy number actually.</p> <p><strong>Robert</strong> [00:30:38]: And on the other side, it&rsquo;s something that you can learn, but you need to spend a bit more time to learn those principles and to not do. Caricature of your business.</p> <p><strong>Miłosz</strong> [00:30:50]: It&rsquo;s similar a bit to DDD. It&rsquo;s a very broad term and a general approach to solving and you need to learn many patterns.</p> <p><strong>Robert</strong> [00:31:00]: And it&rsquo;s hard to give one advice that will fit all cases. But it&rsquo;s doable, but you need to spend some time and keep in mind that many, many, many people are not doing this exercise and are later surprised that it doesn&rsquo;t work.</p> <p><strong>Robert</strong> [00:31:18]: Like with DDD, you can also see that many people are saying that running business is super hard.</p> <p><strong>Robert</strong> [00:31:23]: So I would say that it&rsquo;s hard, but I think it&rsquo;s also not. That hard that people think. It&rsquo;s just about knowing the principles and spending enough time on it.</p> <p><strong>Miłosz</strong> [00:31:36]: Right, so if you were to boil down DDD to one primary theme, I would say something like it&rsquo;s about understanding the domain you work with and then modeling it well in the code. Whether you call it DDD or not, it&rsquo;s like a mindset. More than patterns, maybe?</p> <p><strong>Robert</strong> [00:31:57]: In other words, you should always do that. I mean, if somebody will tell you that, okay, you should not think about the domain, what problems you are solving in your software and how you are writing in an account like it sounds crazy.</p> <p><strong>Miłosz</strong> [00:32:12]: Yeah, it&rsquo;s like you&rsquo;re deciding. Yeah, I will create a website. I just don&rsquo;t know which one. So it sounds like you do something for the sake of learning the technology, which is fine. Sure. But if you want to create a product, it&rsquo;s not really helpful.</p> <p><strong>Robert</strong> [00:32:32]: My observation is actually that dark.</p> <p><strong>Robert</strong> [00:32:37]: Two kinds of people that are often visible in the area of techniques like domain-driven design, architecture, take anything.</p> <p><strong>Robert</strong> [00:32:46]: There are two comps that are totally opposites. One comp is saying that you should always use that. Because it&rsquo;s great, etc. And the second is you should never use it. That is the worst idea in the world. And I think it&rsquo;s pretty interesting that even if they are opposite, they have many in common because they are extreme in some way.</p> <p><strong>Miłosz</strong> [00:33:06]: So they treat it more like kind of religion rather than something you want to use to deliver a product.</p> <p><strong>Robert</strong> [00:33:17]: And in practice, probably you should be somewhere in the middle.</p> <p><strong>Robert</strong> [00:33:23]: It may sound obvious, but you should use proper technique for a proper problem.</p> <p><strong>Robert</strong> [00:33:29]: That&rsquo;s it.</p> <p><strong>Robert</strong> [00:33:31]: It&rsquo;s important when you see some religious people about techniques or tools that are against them or love to use it always that it&rsquo;s the same coin, but just other sides of it.</p> <p><strong>Robert</strong> [00:33:46]: I think it— gets into one thing that you should watch out to not use any techniques or tools like DDD just for sake of using it. So you should identify the problems that you have and use the right tools for that. And the Winterwind design have couple tools that you can take out of that like a tool from a toolbox and just use what you need from that.</p> <p><strong>Miłosz</strong> [00:34:12]: There&rsquo;s probably no project that will use every pattern described in the book.</p> <p><strong>Robert</strong> [00:34:16]: Yes, but probably you agree that every project should use some parts of domain-driven design. So it may sound controversial, but it&rsquo;s getting into a point that you may be using some parts of domain-driven design. Even without realizing that. It may be because of reinventing the wheel. Again, it&rsquo;s probably easier to just see that, okay, you have some tools like that, but many of them, some people just can find them, yeah, probably except for very trivial projects, but we don&rsquo;t care about them here.</p> <p><strong>Miłosz</strong> [00:34:51]: Like, if you create something that&rsquo;s very trivial and doesn&rsquo;t need any kind of design and architecture, and you probably don&rsquo;t need to figure out how to do it better.</p> <p><strong>Miłosz</strong> [00:35:05]: So we don&rsquo;t worry about this here.</p> <p><strong>Miłosz</strong> [00:35:09]: Maybe also something very technical like Linux kernel. I don&rsquo;t know. But probably also there should be at least one idea from DDD that applies there. Maybe value object.</p> <p><strong>Miłosz</strong> [00:35:21]: Yeah, but in general.</p> <p><strong>Miłosz</strong> [00:35:24]: If you boil DDD down to some very key ideas, I think you should be able to use them if you know what you&rsquo;re looking for.</p> <p><strong>Robert</strong> [00:35:34]: I think it&rsquo;s a similar situation like with the book Design Patterns. So pretty old one. I don&rsquo;t remember actually how many years. It&rsquo;s older than DDD. But I think 96 or 97. So still, pretty old book, but still pretty relevant and many of us may be using patterns from this book. Even without knowing that those are patterns from this book, because it was used by somebody else and we learn those patterns by replicating them. But still. We may be missing some of the context maybe of them. And if we read that, maybe we could understand them a bit more and apply them in the better situations also.</p> <p><strong>Miłosz</strong> [00:36:15]: I remember there was a joke or people making fun of people in interviews. When asked about design patterns, they will tell something like, &lsquo;Oh, yeah.&rsquo; There&rsquo;s the singleton, and that&rsquo;s it. But that&rsquo;s also kind of the point. You know, there&rsquo;s an entire book, and doesn&rsquo;t mean you need to know all the patterns and use them all in a project— even if you learn about some, like maybe you can use a strategy here or decorator in another place. I think it&rsquo;s still a good result. You don&rsquo;t need to force them and rewrite your entire project to use, I don&rsquo;t know, abstract factory method or something.</p> <p><strong>Robert</strong> [00:37:00]: It&rsquo;s always about identifying some problem that is really a problem, and not solving.</p> <p><strong>Robert</strong> [00:37:08]: Some imaginary problems. Yeah.</p> <p><strong>Miłosz</strong> [00:37:10]: Especially if you force the patterns. I think that&rsquo;s the worst thing that can happen because it just leads to overengineering. You just read the book and you like the ideas, so you use them everywhere in your next projects and of course they don&rsquo;t apply to all of them.</p> <p><strong>Robert</strong> [00:37:27]: And I think it. It plays nicely with a couple of things that we covered in the previous episode that we&rsquo;ve mentioned: that being a principal engineer doesn&rsquo;t mean that you&rsquo;re forcing to use some patterns. You should instead be able to convince people what value those patterns give and and convincing them that it&rsquo;s worth using some patterns there. Hmm. And at the end of the day, it&rsquo;s also a matter of solving your problems, not imaginary problems.</p> <p><strong>Miłosz</strong> [00:37:59]: There is no one solution to suggest. Because there are so many factors in play, like the size of your team. If you are a 20-person team in a corporation, a developer working on open source software, or maybe you are a five-person startup, you will probably use a very different approach to software.</p> <p><strong>Robert</strong> [00:38:22]: And also, what&rsquo;s the timeline for the project? Because if you are in a startup that you&rsquo;re starving for, if you don&rsquo;t know if you will survive half a year, and you&rsquo;re doing refactoring, it&rsquo;s probably not the best idea, but I mean, it depends, obviously, but it&rsquo;s again going back to solving real problems that you have. So, if you are a startup that has a half year of runaway and you&rsquo;re fighting for survival, you should choose your fight wisely, basically, and not put maybe refactor some code. That you didn&rsquo;t touch for a year and put design patterns there. But maybe you have some part of the application that, for example, you are doing a pivot now and you need to— it quickly, but you&rsquo;re wasting a ton of time to modify this code, maybe doing this refactoring and putting those design patterns may be good at the other, even if you&rsquo;re fighting for survival.</p> <p><strong>Robert</strong> [00:39:26]: Because maybe it may allow you to build this new functionality or do this pivot within this time that you have left.</p> <p><strong>Miłosz</strong> [00:39:33]: Have some idea that this project will be for you longer. You probably need some better maintenance here. But but maybe some some areas are more less important. Maybe you will get rid of them in two months if it doesn&rsquo;t work out. But because it&rsquo;s sometimes difficult to decide.</p> <p><strong>Miłosz</strong> [00:39:57]: This might be a bit controversial, maybe, but I think you don&rsquo;t need to go all in. Most of the patterns from DDD you could decide to use them in a more, let&rsquo;s say, shallow version, other than deep version. So you basically think of the the first principles, the big idea behind the pattern. You don&rsquo;t need to implement all of it and still get some good results. Because you often don&rsquo;t know if the project you work on will be with you for longer, or maybe it&rsquo;s just a prototype.</p> <p><strong>Miłosz</strong> [00:40:33]: But I think there&rsquo;s a balance between having some very scrappy code and something that&rsquo;s very enterprising. While design software.</p> <p><strong>Robert</strong> [00:40:44]: It&rsquo;s actually an interesting thing because it&rsquo;s one of the things that Domain Driven Design covers with core and supporting domains.</p> <p><strong>Robert</strong> [00:40:56]: It&rsquo;s kind of forcing you to think, &lsquo;Okay, is this code really important to my business, or is it less important?&rsquo; And it can kind of affect how we are thinking about this code.</p> <p><strong>Robert</strong> [00:41:09]: I know that many people like consistency in the code, like, &lsquo;Okay, let&rsquo;s do.&rsquo; Something in the consistent way everywhere. End. It may sound like a decent idea, let&rsquo;s say, but it may be also dangerous because you at some point start to over-engineer some parts because some parts of the application are more important, more often touched, more complicated, and some are simpler. You may just lose a lot of time for maintaining this consistency for sake of nothing. Basically.</p> <p><strong>Miłosz</strong> [00:41:44]: So this episode is more like an introduction, but in the next episodes we want to cover more patterns from both shallow and deep end, so you can better decide what to use.</p> <p><strong>Robert</strong> [00:41:58]: But I think we should put big emphasis on the thing that people are very often missing. So the strategic part, because Tactical patterns— those strategic domain-driven design— are the patterns that are not that much about how you write the code, but it&rsquo;s more about strategic thinking, like how the modules are interacting, how modules are split. Very often cross-team collaboration.</p> <p><strong>Robert</strong> [00:42:28]: How important are multiple parts of the code? And what&rsquo;s interesting, everything what I just said, I think you should probably think about that in any project that you are doing. You&rsquo;re using domain-driven design or not. It&rsquo;s cool to have some tools that are helping you to do that, and some frameworks— let&rsquo;s say it&rsquo;s not a framework, but some way of thinking how to approach this. And from other side, you have technical patterns that you&rsquo;re using in the code, and those ones are not always needed. And you don&rsquo;t need to always apply them.</p> <p><strong>Miłosz</strong> [00:43:01]: So we won&rsquo;t be diving deep into patterns today, but let&rsquo;s look at the core idea of DDD.</p> <p><strong>Miłosz</strong> [00:43:12]: The domain model or model-driven design or knowledge crunching, as it&rsquo;s specified by the book. What is it about?</p> <p><strong>Robert</strong> [00:43:23]: There&rsquo;s one thing in multiple projects that is a bit like running business, like we said earlier. So it&rsquo;s obvious that you should earn more money than you are spending. so you have a successful business. In software, it&rsquo;s a matter of understanding what kind of problem you are solving and how it should work.</p> <p><strong>Robert</strong> [00:43:45]: It makes all of us like, yeah, we are starting a project. We should understand how it works and how we should implement that. From other sites It&rsquo;s not happening.</p> <p><strong>Miłosz</strong> [00:43:59]: yeah because sometimes the business is complex by itself and even for simpler ones like consider e-commerce seems like a simple domain it&rsquo;s just people People buy products and that&rsquo;s it. But if you dive deeper, there&rsquo;s capturing payments and delivering the products and getting the inventory somehow. So it&rsquo;s more complex than it seems.</p> <p><strong>Robert</strong> [00:44:24]: How it looks in practice? People are thinking, &lsquo;Oh, okay, it&rsquo;s simple.&rsquo; We know what to do. Let&rsquo;s go to implement that. Exactly. Two years later.</p> <p><strong>Miłosz</strong> [00:44:33]: Consider, if you wanted to to open an online shop.</p> <p><strong>Miłosz</strong> [00:44:38]: Oh. If you ask a software developer, they will probably tell you, &lsquo;Yeah, you just need to host this WordPress website. With an e-commerce plugin and you&rsquo;re done.&rsquo;</p> <p><strong>Robert</strong> [00:44:52]: I think many software developers will say, &lsquo;No, no, we need to implement it from scratch.&rsquo;</p> <p><strong>Miłosz</strong> [00:44:57]: Oh yeah, that&rsquo;s even better. But my point is, they probably won&rsquo;t think about that— that you need to. Order the inventory from somewhere, store it somewhere, and then somehow send it out to people, which is the hard part of the business and your software probably needs to work with it somehow.</p> <p><strong>Robert</strong> [00:45:16]: It actually works nicely with this part of strategic patterns of domain-driven design when we are considering what&rsquo;s our core domain and what&rsquo;s the value for our company.</p> <p><strong>Miłosz</strong> [00:45:27]: And many projects are much, much more complex than e-commerce. If you consider accounting, for example, you can get into some very very specific edge cases depending on the country and if you do do it globally, then it&rsquo;s yeah, it&rsquo;s crazy amount of knowledge you need to understand and to implement.</p> <p><strong>Miłosz</strong> [00:45:48]: Complexity in those projects doesn&rsquo;t really come from the technical challenges, but from the business itself. It&rsquo;s just complex to run this kind of business, so the software becomes also complex.</p> <p><strong>Robert</strong> [00:46:03]: So, in other words, you have multiple dimensions, complexity dimensions, let&rsquo;s say, to this product, and you can add more and more and more. So, for example, you&rsquo;re starting with one country. Right. We need to scale globally. Okay. We need to support multiple countries, multiple continents. Great. Later, we need to support multiple types of product. So it can have multiple tax rates and multiple rules. It&rsquo;s getting more and more and more complex.</p> <p><strong>Miłosz</strong> [00:46:28]: And it&rsquo;s always complex even before you introduce any kind of technical challenges. Just not considering software at all. This is already complicated.</p> <p><strong>Miłosz</strong> [00:46:39]: So you want to model it well in the code, so at least you don&rsquo;t need to deal with the additional complexity.</p> <p><strong>Robert</strong> [00:46:51]: And to make it worse, often the initial idea is also changing with the time. So I have two good examples of the companies that were like that. So Netflix.</p> <p><strong>Robert</strong> [00:47:01]: Maybe many of you know that Netflix at the beginning was an extreme platform. At the beginning they were just renting DVDs. So it&rsquo;s interesting how it changed over time, how they were making business, and probably how much it changed how they&rsquo;re operating. The other interesting business that changed over time is Instagram. Instagram wasn&rsquo;t this kind of social media platform that you know now; it was more checking location applications. So I guess you were able to send people information like, &lsquo;Hey, I&rsquo;m here&rsquo; or something like that, not share pictures.</p> <p><strong>Miłosz</strong> [00:47:40]: Yeah. And so imagine in both cases.</p> <p><strong>Miłosz</strong> [00:47:45]: At some point, developers complained that the requirements weren&rsquo;t clear. We didn&rsquo;t know this component would become this thing it is now. Of course, it wasn&rsquo;t clear because no one knew about it back then and they had to adapt somehow.</p> <p><strong>Robert</strong> [00:48:00]: Those examples were more extreme, let&rsquo;s say, but most companies are changing their initial assumptions pretty much, so it&rsquo;s also adding this complexity there.</p> <p><strong>Robert</strong> [00:48:14]: I also like example of the cyberpunk game that the cars are horses from Witcher 3 because they&rsquo;re using the same game engine.</p> <p><strong>Miłosz</strong> [00:48:25]: Yeah, so that goes back to the discussion of evolving software. The first version is usually simple, right? Everything works fine. But then someone tells you, actually, we&rsquo;re not renting DVDs. We&rsquo;re going to be streaming videos.</p> <p><strong>Miłosz</strong> [00:48:42]: And at this point, you have to change your software to a completely different model sometimes.</p> <p><strong>Miłosz</strong> [00:48:52]: It&rsquo;s great if you could do it easily, if you don&rsquo;t need to spend one year to rebuild everything. What helps is also what we mentioned in the last episode about developers working directly with domain experts.</p> <p><strong>Robert</strong> [00:49:06]: So even if it&rsquo;s a bit controversial, so we&rsquo;ve seen how it can change things and again affect how you are implementing things.</p> <p><strong>Miłosz</strong> [00:49:17]: So we don&rsquo;t need a product owner or product manager acting as a proxy?</p> <p><strong>Robert</strong> [00:49:22]: Always, because sometimes it makes sense, but sometimes it makes sense to just go direct.</p> <p><strong>Miłosz</strong> [00:49:27]: But the idea of the domain model is to create a model that represents the problem domain as well as you can at the moment.</p> <p><strong>Miłosz</strong> [00:49:37]: Developers can understand how it works, the more information they get from the people who work directly with the business. The better. The model doesn&rsquo;t need to be physical, so it&rsquo;s not a document or diagrams or anything else, although it helps, but it&rsquo;s more abstract. It&rsquo;s about understanding how it should work. And.</p> <p><strong>Miłosz</strong> [00:50:03]: The ultimate artifact is the code.</p> <p><strong>Robert</strong> [00:50:06]: And it also doesn&rsquo;t need to cover everything. So I always like to compare this model part to a map. So map is not a perfect representation of real world, but it&rsquo;s a useful representation of the real world. So you can look on the map and see the details that you care about and do the job that you need.</p> <p><strong>Miłosz</strong> [00:50:28]: And it&rsquo;s also not a data model, which is a common misconception.</p> <p><strong>Miłosz</strong> [00:50:35]: Very often in software you can see models used for both the domain logic and some storage, for example, or transport.</p> <p><strong>Miłosz</strong> [00:50:49]: For developers this is unnatural.</p> <p><strong>Miłosz</strong> [00:50:52]: No. thing to do treat them as the same so we often start implementation with database schema, for example, because it feels natural to think of storage.</p> <p><strong>Miłosz</strong> [00:51:03]: But here we think about the more abstract domain model, how the thing works, what behaviors it has.</p> <p><strong>Robert</strong> [00:51:11]: So this is pretty big part. Paradigm shift and it&rsquo;s also not consistent with how we&rsquo;ve been learning earlier. So this is also the theme that was pretty common in the last two episodes. But how we are learning to code is very far away from the real world. And we are not prepared actually to work in the companies. And later it&rsquo;s ending up with the.</p> <p><strong>Robert</strong> [00:51:37]: golden cage concept from the previous episode. Getting out of that is not simple, but I think it&rsquo;s mandatory if you would like to be successful and survive on the tough market and work in the better places.</p> <p><strong>Miłosz</strong> [00:51:52]: Another tool from the previous episode is event storming, which is also closely related to figuring out the domain.</p> <p><strong>Robert</strong> [00:52:01]: So today we&rsquo;re more focusing focusing on giving you some introduction.</p> <p><strong>Robert</strong> [00:52:09]: To— Maybe not Domain Driven Design really, but some parts of Domain Driven Design that we&rsquo;ve been using in the project. Sometimes we&rsquo;ll name it Domain Driven Design, sometimes not, because again, its patterns are secondary. I think it&rsquo;s more important to focus on problems, not on the patterns, because when you are focusing on patterns, you are just with this hammer in your hand and looking for nails everywhere. So I think it&rsquo;s more important to focus on problems that we&rsquo;re solving and how we can do that. And yeah, this is something that we&rsquo;ll be doing in the next couple episodes and it&rsquo;s connected to the fact that now we are working on our new training the domain engineer and that will be available at the beginning of next year in January.</p> <p><strong>Miłosz</strong> [00:52:56]: Yeah, so in the next episodes we will look at more use cases for the strategic patterns.</p> <p><strong>Robert</strong> [00:53:02]: Yeah, so we&rsquo;ll give you some more practical tools also, how to approach that.</p> <p><strong>Miłosz</strong> [00:53:08]: So let us know in the comments if you have any topics you would like to hear about. You can also check our ebook, &lsquo;Go With the Domain,&rsquo; which is about working with domain-driven ideas.</p> <p><strong>Miłosz</strong> [00:53:22]: In Go specifically.</p> <p><strong>Robert</strong> [00:53:24]: Yeah, so we will be preparing agenda for next episode soon. So let us know on YouTube comments or Spotify comments. So read all of them and it will be. Cool to help you with some projects or problems that you have. We have many, many, many stories from projects that we&rsquo;ve been working in, but we&rsquo;d also love to introduce a bit more there.</p> <p><strong>Miłosz</strong> [00:53:50]: So that&rsquo;s it for today.</p> <p><strong>Miłosz</strong> [00:53:53]: Subscribe to our channel so you can see the new episodes as they come.</p> <p><strong>Miłosz</strong> [00:53:59]: And hit that like button.</p> <p><strong>Robert</strong> [00:54:01]: And if you don&rsquo;t want to miss when the domain engineer will be available, just Google the domain engineer and join our newsletter if you are not there. Yet. So we&rsquo;ll send you an info when it will be ready. And you will also notified when new episodes will be out, because we know that sometimes notifications from multiple platforms are not sent. So when you&rsquo;re in our newsletter, you are sure that you will always get the notification.</p> <p><strong>Robert</strong> [00:54:29]: Okay, so I think that&rsquo;s all for today. So thank you, Miłosz, for your time.</p> <p><strong>Miłosz</strong> [00:54:33]: Thank you, Robert. See you next time.</p> <p><strong>Robert</strong> [00:54:35]: See you in two weeks. Bye-bye.</p>Becoming a Product Engineer: First Stepshttps://threedots.tech/episode/becoming-a-product-engineer/Wed, 26 Nov 2025 12:00:00 +0000https://threedots.tech/episode/becoming-a-product-engineer/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Developers are often kept in &ldquo;golden cages&rdquo;</strong> - treated as coding monkeys receiving tasks without understanding the broader context or product vision</li> <li><strong>Requirements are usually incomplete or wrong</strong> - understanding the business context helps developers spot issues early and deliver what&rsquo;s actually needed</li> <li><strong>Collaboration with stakeholders is crucial</strong> - developers have valuable product insights and should be involved in planning, not just implementation</li> <li><strong>Event Storming is a powerful collaboration tool</strong> - a lightweight workshop technique that brings developers, product managers, and stakeholders together to align on complex topics</li> <li><strong>Change takes time and trust</strong> - transforming company culture requires starting small, showing results, and convincing people through solving their actual problems</li> </ul> <h2 id="introduction">Introduction</h2> <p>In this episode, we talk about why software projects in regular jobs are delivered much slower compared to side projects, and what you can do about it.</p> <p>We share our journey from building hobby projects as teenagers to working in professional environments, and the differences we encountered.</p> <p>Developers are often kept isolated from product decisions and treated as &ldquo;coding monkeys in golden cages&rdquo; - just receiving tasks without understanding the why behind them.</p> <p>We discuss techniques like Event Storming that can help break down these barriers, and improve collaboration between developers and product managers.</p> <h2 id="notes">Notes</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.eventstorming.com%2Fbook%2F" target="_blank">Event Storming</a> - a workshop technique created by Alberto Brandolini for collaborative domain modeling</li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>Five days of coding can save one day of planning.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>If you want to be promoted, for example, delivering is the very best way to do it. So instead of just waiting for requirements and complaining that it doesn&rsquo;t work, you can try to help whoever needs what they need.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>How it often looks like: it&rsquo;s like companies are trying to make the programmers coding monkeys in some golden cages, basically.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>Instead of complaining that we never get anything done, you tell them, yeah, this feature could have been one week, but we spent two months.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>The worst thing that we noticed is that the performance of how fast we&rsquo;ve been able to deliver new stuff was much worse.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>But what if instead, you have this domain expert, this stakeholder meet with developers directly and tell them what they need. And then usually developers will tell them, oh my god, this is so complicated, it will take two months. But if we drop this one button from the dashboard, it will take one week.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D0s">00:00:00 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D71s">00:01:11 - Our story</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D306s">00:05:06 - Slow projects</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D430s">00:07:10 - Golden cage problem</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D570s">00:09:30 - Incomplete requirements</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D794s">00:13:14 - Developer time optimization trap</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D915s">00:15:15 - Level 2: Collaboration</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D1149s">00:19:09 - MVP and simplification</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D1413s">00:23:33 - Talking to stakeholders</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D1720s">00:28:40 - Levels of engagement</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D1829s">00:30:29 - Why you should care</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D2070s">00:34:30 - Event Storming</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D2677s">00:44:37 - Getting started</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D3017s">00:50:17 - Practical advice</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQXZMMyIf4Ik%26amp%3Bt%3D3742s">01:02:22 - Changing culture</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Robert</strong> [00:00:59]: I&rsquo;m Robert.</p> <p><strong>Miłosz</strong> [00:01:00]: And I&rsquo;m Miłosz. And this is No Silver Bullet Podcast, where we discuss mindful backend engineering. In this show, we share takes on software engineering that will help you grow into a principal engineer.</p> <p><strong>Robert</strong> [00:01:11]: In this episode, maybe let&rsquo;s start with a bit of our story, because I think it&rsquo;s pretty important to understand that, how we started to build some production systems and from where our experience came. So probably some of you know that we met with Miłosz in the high school, so a pretty long time ago. We were just talking before recording. That it was, I think, 17 years ago, so a pretty long time ago. Actually, I know Miłosz two months longer than he knows his wife.</p> <p><strong>Miłosz</strong> [00:01:42]: That&rsquo;s pretty long, and the reason I know you longer is because we are already working on projects before everybody started. So we started with some toy websites and then grew into more complex projects.</p> <p><strong>Robert</strong> [00:01:59]: And many of those projects actually became pretty successful, so many people were using them. The downside was that, well, there wasn&rsquo;t great business sense at the time, and well, a lot of people used that, but it wasn&rsquo;t enough for making for a living of that. So we decided, okay, let&rsquo;s go to university. Maybe we&rsquo;ll learn something useful. Long story short, we did it. So we left the university at some point. So we quickly dropped out. Yeah, because, well, it was probably a bit boring since we&rsquo;ve been already coding for multiple years and we&rsquo;ve seen that, OK, maybe we&rsquo;ll learn physics and mathematics, but&hellip; That&rsquo;s not probably relevant in the software that we are writing.</p> <p><strong>Miłosz</strong> [00:02:41]: We still work on our projects in the meantime, but there was almost no money coming from it. We had to just start regular jobs, which was fun at the time, because we didn&rsquo;t have this professional experience, let&rsquo;s say that&rsquo;s our own.</p> <p><strong>Robert</strong> [00:02:55]: I just remember that. During the time, the market also wasn&rsquo;t that easy. I mean, we&rsquo;ve been over decoding for a couple of years already, and I was already afraid that I will need to go back to my parents&rsquo; home because I I struggled a bit to find a job, despite having a couple years of experience already.</p> <p><strong>Miłosz</strong> [00:03:11]: Even the basic wage we made back then was great for our student life, compared to hobby projects.</p> <p><strong>Robert</strong> [00:03:21]: That&rsquo;s true. But I think it wasn&rsquo;t worth thinking about that because, since we&rsquo;ve been working already on multiple projects, we&rsquo;ve been working, so we have two people— team basically, so me and Miłosz. It was a bit shocking to start to work in the normal job, let&rsquo;s say, let&rsquo;s name it in this way. So, you know, in the way that you are no longer your boss, but instead you are getting requirements from somebody else. You are not inventing the project and product that you are building. It&rsquo;s totally different dynamics. And for some people, I know it&rsquo;s fine because they don&rsquo;t need to touch this ugly business and product they can just code. But we&rsquo;ve been different. We&rsquo;ve always wanted to create something and invent something. And it was a bit shocking.</p> <p><strong>Miłosz</strong> [00:04:08]: The upside is you don&rsquo;t need to worry about&hellip; If the mana is coming through. Someone else handles that for you. But alongside this, someone tells you, here&rsquo;s this system that has been around for 10 years and you need to add some feature to it. And usually it&rsquo;s not an easy task.</p> <p><strong>Robert</strong> [00:04:27]: Yeah, yeah, exactly. And you know, you have other challenges, like you need to collaborate with more people. So it&rsquo;s no longer two of us, but the team is bigger, you need to take maintenance into account more because there&rsquo;s 50% of a chance that it will be the person that will be maintaining it later. In this case, it&rsquo;s totally different because you&rsquo;re working with someone&rsquo;s legacy code that some person who worked here 10 years ago wrote. This person no longer works there. Code that we have written 10 years ago is still running out of there.</p> <p><strong>Miłosz</strong> [00:04:58]: You have to agree with other people in the team how to do it, probably, because you are not the expert anymore.</p> <p><strong>Robert</strong> [00:05:04]: So it&rsquo;s creating a lot of challenges.</p> <p><strong>Robert</strong> [00:05:06]: Those challenges are pretty important because the worst thing that we noticed is that the performance of how fast we&rsquo;ve been able to deliver new stuff was much worse. And in this episode, we will discuss some tools and techniques that should help with that.</p> <p><strong>Robert</strong> [00:05:27]: Can give you some tools to handle situations when you are not working in a solo project, but in a team and you need to collaborate in some way and create software that is maintainable. In longer term than one month. So can you keep the same pace as when working with two people or just a solo developer? So if those problems sound familiar for you, please stay with us. So we will give you some hints how to overcome those problems when you are working in a daily job. So at the beginning, let&rsquo;s think why projects in your daily job are delivered slower. So in other words, why, when you are working with your friends after work or on your pet project, you are much more productive? What do you think it is? So we already mentioned one thing: when you&rsquo;re working on the project alone, you are basically your own boss and you&rsquo;re working on your ideas, which is great. But the downside is that it&rsquo;s also much harder to make business out of that. I mean, we are software engineers, we are good problem solvers, but from our experience, very often we are not best businessmen.</p> <p><strong>Miłosz</strong> [00:06:32]: Yeah and we often start side projects and you know, some websites maybe web apps, but often it&rsquo;s just something for fun because it&rsquo;s for learning. And this is how we often are learning, and it&rsquo;s great. Yeah, but making it into a business is a much different story.</p> <p><strong>Miłosz</strong> [00:06:52]: Which might be also why developers usually in companies don&rsquo;t have that much impact on the product side. So very often they are kept in a silo of software development.</p> <p><strong>Robert</strong> [00:07:10]: Yeah, I actually like to name it a bit more brutal— and they need a golden cage. So and how it often looks like: it&rsquo;s like we are companies are trying to make our programmers coding monkeys in some golden cages, basically. I know it sounds brutal, but this is how it looks in multiple components, basically. So what do you mean by coding monkeys? So there are a couple elements of this problem, let&rsquo;s say, and also a couple of levels of how companies are approaching this. So the most extreme scenario is when you&rsquo;re this coding monkey in your golden cage and you&rsquo;re just receiving tasks to code. I even heard about worse cases when some developers even received empty classes and they were diagrams, empty classes and some UNLs and just please feel the methods and don&rsquo;t care about anything else. It&rsquo;s similar to LLMs today, just like a human instead. Yeah, yeah.</p> <p><strong>Robert</strong> [00:08:13]: And funny thing, if many people were working like that entire life, well, I can imagine that they may struggle now a bit because&hellip; So this is actually, it&rsquo;s not very performant at the end because the software that is created in this way, obviously the chance of working is very, very low and making some&hellip; value. But still, it can be replaced in some way.</p> <p><strong>Miłosz</strong> [00:08:36]: So engineers in this scenario are just kind of implementators of some idea. From the outside. I think especially in product companies it might be an issue because developers usually have great insight into the product area as well.</p> <p><strong>Miłosz</strong> [00:08:53]: It can work better in some very technical domains where you need developers just to work on some specialized algorithms or something like that. I guess even waterfall can work in such a scenario.</p> <p><strong>Miłosz</strong> [00:09:09]: But if it&rsquo;s a product company, it&rsquo;s much different.</p> <p><strong>Robert</strong> [00:09:12]: Some of you may wonder, &lsquo;Okay, why is that?&rsquo; I can just sit in my nice cage and code. It&rsquo;s great. But&hellip; Unfortunately, it&rsquo;s pretty unproductive. If you&rsquo;re working with software a bit longer, you probably already know that requirements that you receive, they&rsquo;re in most cases&hellip; Maybe not wrong, but&hellip;</p> <p><strong>Miłosz</strong> [00:09:30]: Incomplete. It&rsquo;s probably the biggest issue developers have in their day-to-day work. I can&rsquo;t work on this. This is not clear. This is missing some requirements. And it&rsquo;s discouraging because you can&rsquo;t do anything with it. You can just, you know&hellip; tell your product owner or manager or whoever that, sorry, this is not complete. I can do anything with it. And then the product owner needs to go back to whoever knows what to do and figure it out and come back to you with clear requirements, which is never the end of it.</p> <p><strong>Robert</strong> [00:10:08]: And needless to say, it&rsquo;s super unproductive. When you&rsquo;re working alone, you can ask yourself, &lsquo;Ah, okay, it doesn&rsquo;t make sense.&rsquo; Maybe let&rsquo;s do it in this way. When you have proxy&hellip; Project manager well, it&rsquo;s very ineffective because probably this project manager also doesn&rsquo;t know. So this project manager needs to go to another person, that person ask another person, and it can also probably explain why often those requirements are not perfect.</p> <p><strong>Miłosz</strong> [00:10:36]: And worst case scenario is probably when the product owner also decides who gets to do what.</p> <p><strong>Miłosz</strong> [00:10:45]: Split the tasks between developers so the workload is even across the team. So that&rsquo;s probably the biggest anti-pattern you can see in project management.</p> <p><strong>Robert</strong> [00:10:57]: Yeah, exactly. Because, well, if the project manager is assigning tasks to each developer, probably he may not or she may have not enough context to know. Which person has most knowledge there, or maybe some other things that if team will be responsible for assigning those tasks. It will be much more effective because they have more context to do this decision better.</p> <p><strong>Miłosz</strong> [00:11:23]: Yeah, that&rsquo;s a better approach because the team knows what they specialize in, so they can share the knowledge as well between the team members. Everyone knows a bit about what&rsquo;s going on in the project. It&rsquo;s not like there&rsquo;s one person responsible for everything.</p> <p><strong>Robert</strong> [00:11:44]: And I think it&rsquo;s quite interesting because it&rsquo;s based on crazy and pointless &lsquo;why&rsquo; to work in this way. But there are multiple reasons why, in many companies, it&rsquo;s the default mode. I remember that.</p> <p><strong>Robert</strong> [00:11:58]: In many components, when I was starting at the beginning, it was unfortunately the default mode of working, and it was often some work needed to change that. I mean, it was possible and it wasn&rsquo;t that hard at the end when you&rsquo;ve shown what problems it&rsquo;s generating, but&hellip; Yeah, unfortunately, it&rsquo;s often the default way of working. Do you have some ideas maybe why companies are doing it in this, well, probably crazy way of assigning tasks to people and closing them into the cages?</p> <p><strong>Miłosz</strong> [00:12:32]: I guess maybe it&rsquo;s just the way things always used to be.</p> <p><strong>Miłosz</strong> [00:12:38]: Just the default mode of, you know, we have those developers who are implementers and product people who come up with ideas. So the development team becomes the downstream of those ideas.</p> <p><strong>Miłosz</strong> [00:12:56]: Ping pong between each other.</p> <p><strong>Robert</strong> [00:12:59]: Yeah, I also found one interesting reason for that. And the reason was that developers are expensive. So their time is expensive. So it means that we should use as small amount of their time as we can.</p> <p><strong>Miłosz</strong> [00:13:14]: Yeah, basically use as much as we can for all the tasks we need.</p> <p><strong>Robert</strong> [00:13:20]: So basically, let&rsquo;s optimize developers&rsquo; time so they can code as much as they can because, well, we&rsquo;re paying them for coding. This is why we&rsquo;re paying them that much.</p> <p><strong>Miłosz</strong> [00:13:28]: It&rsquo;s basically like in a factory. You want to utilize all the devices to do the work for you.</p> <p><strong>Robert</strong> [00:13:37]: And you&rsquo;re trapped, because, okay, from one side you are&hellip; ensuring that those developers are using 100% of their time on coding. But if you think about that a bit for a bit longer, you will notice that, okay, maybe they&rsquo;re coding more. But useless things, because even if we will force them to code some stuff, and they will write them after it will be shipped to production after in this kind of companies, half year let&rsquo;s say, you&rsquo;ll see that, okay, it&rsquo;s actually not what we needed. The worst part is it all feels like a good job. Yeah, and for the developers. You may also feel very productive, like I&rsquo;ve been coding for 12 hours and it&rsquo;s so great.</p> <p><strong>Robert</strong> [00:14:18]: Yes, but there is nothing to show for it. You more like wasted the time. Probably one sentence here will be very accurate. And I really like to remind it over and over. So five days of coding can save one day of planning. And this is basically that. Yes. So how&hellip;</p> <p><strong>Miłosz</strong> [00:14:35]: talking about or coming up with ideas how something should work feels much less productive than just typing away and creating this. stuff. So we are very eager to jump to implementation, maybe build some prototype. Sometimes it&rsquo;s useful, but very often it would be better to spend some time. Trying to understand what you really need to build, what will be useful for the users, for example.</p> <p><strong>Miłosz</strong> [00:15:06]: But it doesn&rsquo;t feel as productive as this starting coding. So developers don&rsquo;t really have an issue with this most of the time.</p> <p><strong>Robert</strong> [00:15:15]: So let&rsquo;s all go maybe one level up in our ladder of how good or bad putting developers into cages can be. So the second level. That I noticed in some companies is some a bit better situation. So in this situation, product owner is going to the team. With some requirements gathered from stakeholders. And in this case, the product manager or product owner is not the person who is creating the tasks, but it&rsquo;s the person who discusses basically what we need with the developers and he&rsquo;s working or she&rsquo;s working with developers to learn how to implement that.</p> <p><strong>Miłosz</strong> [00:15:57]: So they present some high-level idea and then developers split it into smaller tasks or come up with implementation, maybe propose some solutions.</p> <p><strong>Robert</strong> [00:16:06]: So we no longer receive tasks. It&rsquo;s no longer assigned to people. We can talk to our closest stakeholder and discuss what we would like to implement.</p> <p><strong>Miłosz</strong> [00:16:17]: You get even better knowledge sharing this way. So everyone is on the same page. Also easier to work with code later.</p> <p><strong>Miłosz</strong> [00:16:24]: Because everyone has insight how this will impact the end product. You don&rsquo;t just work on some obscure subtasks on the other end of it. You see the full picture. But there&rsquo;s also a challenge here, right? It&rsquo;s not really common knowledge how to do it.</p> <p><strong>Miłosz</strong> [00:16:44]: It can take long hours to come up with the final solution.</p> <p><strong>Robert</strong> [00:16:49]: And can go into never-ending meetings.</p> <p><strong>Robert</strong> [00:16:55]: My observation, obviously, it&rsquo;s always about balance. But my observation is that often it&rsquo;s when you have hours of discussion. It&rsquo;s basically alternative because sometimes even better alternative. I mean, the worst alternative can be that, okay, you are going to saying that, okay, &lsquo;We are discussing already for too long.&rsquo; Let&rsquo;s go. Let&rsquo;s do what we think should be done. Yes. And later, you&rsquo;re not spending hours on planning, but you&rsquo;re spending weeks on implementing something just later to see that why we did it in this way. It&rsquo;s crazy. It doesn&rsquo;t make any sense. And nobody&rsquo;s using that.</p> <p><strong>Robert</strong> [00:17:33]: So I think it&rsquo;s one popular trap that we&rsquo;re trying to optimize this discussion part. Sometimes, as you said, we don&rsquo;t have good tools how to discuss requirements with product owner and/ or product manager. We&rsquo;ll get into that soon. So we&rsquo;ll give you some tools.</p> <p><strong>Robert</strong> [00:17:50]: Yeah, it can lead to that, but it&rsquo;s, I think, important to ask ourselves the question, what&rsquo;s the alternative here? And often, the alternative is not the best.</p> <p><strong>Miłosz</strong> [00:18:00]: outcome of this can be simplification.</p> <p><strong>Miłosz</strong> [00:18:03]: I think you need skilled developers that can have this. Product vision a bit and can talk with product owner, for example, and suggest some solutions. But because often the reason the meetings drags on so long is that we talk about two big problems at once. So we want to create this full system or big feature up front. And there are many edge cases. Developers love poking those edge cases and they will come up with many many issues. So then you have like 20 things you need to tackle before you start the development because there are so many edge cases to cover. And very often, if you start with a simpler version of the same feature, it may be much, much easier to discuss and start. And also very often you&hellip; you later realize and actually, you know this is like 80% good, maybe we don&rsquo;t need most of what we thought we needed in the beginning.</p> <p><strong>Robert</strong> [00:19:09]: I think it&rsquo;s some kind of meta advice that it&rsquo;s already coming in multiple episodes that we had earlier. So some golden standards that we have tried to have in teams that we&rsquo;ve been working in. To be able to deliver each task within one day and to be able to deliver some, let&rsquo;s name it, story within one week. Because, if you&rsquo;re implementing, you&rsquo;re creating some functionality into something existing. But it helps us to&hellip; Simplify those discussions because, if you are discussing some feature for multiple hours, and you would like to implement within one week, you already see that okay it cannot happen. Because, if you are discussing some feature for multiple hours, the implementation of that will just take much longer. So it&rsquo;s good because you can start a discussion like, &lsquo;Okay, it&rsquo;s already scope creep. Let&rsquo;s try to figure out what we can do. Some kind of how we can make some kind of MVP out of that, and start with that, and iterate on that. I said that it&rsquo;s working for features that you&rsquo;re already building on already existing functionality.</p> <p><strong>Robert</strong> [00:20:17]: Because, in this case, you can do it faster. It&rsquo;s a bit different when you are doing kickoff of new project. Because, if you are doing kickoff of some project, sometimes you need a bit more time to have something that is functional. So, but I would say that you should also watch out to not exceed, let&rsquo;s say, one month. For example, and have in one month some MVP that already delivers something, and somebody can already use that, and you can already receive some feedback on that. And again, I think it may sound very obvious, but&hellip;</p> <p><strong>Robert</strong> [00:20:48]: It just works, basically. So it&rsquo;s one of those advices that are too simple to be true, but this is how it works.</p> <p><strong>Miłosz</strong> [00:20:57]: Splitting work is not easy, for sure. We&rsquo;ll probably need another episode on this. But coming back to this&hellip; with product owner. This is probably an upgrade on the previous approach.</p> <p><strong>Miłosz</strong> [00:21:09]: Product owner deciding on everything themselves, but it&rsquo;s still&hellip; not perfect, right? Because this is a single point of failure. This one person, they understand the domain, they talk with stakeholders or users.</p> <p><strong>Miłosz</strong> [00:21:24]: But they still don&rsquo;t know everything. So they are kind of a proxy between the development team and whoever needs the product built.</p> <p><strong>Robert</strong> [00:21:34]: And if you are working this way, probably more than once you have situation like you are asking some question to your product owner, he or she is seeing you for a while and I don&rsquo;t know, I need to ask this stakeholder. I will go back to you in a week.</p> <p><strong>Robert</strong> [00:21:52]: And it&rsquo;s good because if this question is there, because maybe it will save you one week of implementation that it will be useless at the end. So that&rsquo;s good. But from other side, you need to often wait for some time for having those.</p> <p><strong>Miłosz</strong> [00:22:06]: Yeah, I remember those planning sessions where we were deciding what to&hellip; Take on for the sprint for the next two weeks, and of course, we poke holes in some plant features. We see some magic cases that are not solved, and the product owner says, &lsquo;Yeah, you&rsquo;re right.&rsquo; So yeah, I need to go figure out what to do. Meanwhile, here are some well-defined but very unimportant tasks you can do in your sprint. So you postpone the important work this way, and because you don&rsquo;t have anything better to do, you start working on something smaller but probably something that&rsquo;s not really game-changing for the company.</p> <p><strong>Robert</strong> [00:22:50]: But sometimes it&rsquo;s good, because very often you have this backlog of things that we should do later, a . k . a.</p> <p><strong>Miłosz</strong> [00:22:57]: never. Sure, but this way you never do these game-changing features. Probably the product needs the most. That&rsquo;s true.</p> <p><strong>Miłosz</strong> [00:23:08]: Because they are so important and risky, no one wants to just&hellip; start doing them with some simpler approach. It feels like we need this perfect implementation or specification for it. So you wait until you have it planned perfectly.</p> <p><strong>Robert</strong> [00:23:28]: So what&rsquo;s the crazy alternative for that?</p> <p><strong>Miłosz</strong> [00:23:30]: Because I know— because we tried this crazy alternative.</p> <p><strong>Miłosz</strong> [00:23:33]: Yeah, so if you go one step further, I guess you could have&hellip; developers talk with stakeholders directly? Oh, no, it sounds so expensive.</p> <p><strong>Miłosz</strong> [00:23:44]: Or, you know, just domain experts in general, whoever this is, I don&rsquo;t know, maybe your users or your CEO or whoever.</p> <p><strong>Robert</strong> [00:23:54]: That are often the source of those requirements. So there are people with knowledge, there are people with some interest, let&rsquo;s say, but basically&hellip; those are people who give you the direction of work that you are working on. And those are the people to whom your product owner is going to ask how to do something. Exactly.</p> <p><strong>Miłosz</strong> [00:24:15]: So it might be your VP of sales or your operations specialist in another team.</p> <p><strong>Robert</strong> [00:24:23]: Or even your CEO. Depends on the company.</p> <p><strong>Robert</strong> [00:24:27]: Um, Or it may be also an operation theme. So it really depends on the functionality that you&rsquo;re implementing.</p> <p><strong>Robert</strong> [00:24:34]: Usually, if you&rsquo;re implementing some functionality, there is some person that is mostly interested in that and the idea maker.</p> <p><strong>Miłosz</strong> [00:24:44]: go to your product owner and they will tell them I need this new dashboard built and they will reply okay I will talk with my developers in two weeks later.</p> <p><strong>Miłosz</strong> [00:24:58]: I will let you know two weeks later what we think. But what if instead, no, you have this&hellip; This domain expert, this stakeholder meet with developers directly and tell them what they need. And then usually developers&hellip; will tell them, oh my god, this is so complicated, it will take two months. But if we drop this one button from the dashboard, it will&hellip; take one week. And then the stakeholder says, oh, really? I don&rsquo;t need this button at all.</p> <p><strong>Miłosz</strong> [00:25:31]: Wow, impossible. Now we save two months of development and also two weeks of planning to figure this out.</p> <p><strong>Robert</strong> [00:25:39]: And this is not a story that we invented. It&rsquo;s actually what happened multiple times for us when, basically, we could spend ages to implement. Something that the stakeholders would say that it&rsquo;s not that important. But if we have proxy as product owner, it will never happen because the communication between developers and stakeholders will never happen. And to be exact, so it&rsquo;s not about gathering all stakeholders in the conference room and discussing. Everything because it will be super inefficient. We&rsquo;ll go into tools very soon. It&rsquo;s more about having them meet with developers in some way. At some point. And also not doing it for every feature. So to be exact here. So we&rsquo;ve been doing it for some bigger project kickoffs. So we&rsquo;ve been not doing it for&hellip; every story that we&rsquo;re implementing because it would be super expensive, especially that our stakeholders were living in a different country. So it meant that gathering, for example, five stakeholders, and discussing everything with them, would cost flights, hotels, etc.</p> <p><strong>Robert</strong> [00:26:46]: So it was pretty expensive, but still it will work for us and we&rsquo;ve been doing it multiple times because, if it wouldn&rsquo;t, it will just&hellip;</p> <p><strong>Miłosz</strong> [00:26:55]: Yeah, but this is a bit extreme scenario, right? When we fly over people to meet in the conference room. But this same idea can also be applied on a more simple level.</p> <p><strong>Robert</strong> [00:27:04]: It&rsquo;s easier if you have stakeholders nearby, let&rsquo;s say.</p> <p><strong>Miłosz</strong> [00:27:09]: Yeah, but even if you have someone in another team that uses your software and they have some issue, you can just have a call with them and have them show you directly what&rsquo;s the problem they have. It&rsquo;s the same idea applied to a more shallow level, so you don&rsquo;t need your&hellip; Thank you. product owner to be a proxy between you two. You just talk directly to them and you understand, okay— this is the issue they have. So now I will figure out how to fix it instead of waiting for perfect requirements to figure out what to do. But I think the basic idea here is that it&rsquo;s difficult to come over, having developers think about the solution and offer some simplification. And I remember some product owners trying to do it themselves. They somehow&hellip; In a way they knew the software developers were working on and they tried to figure out themselves how complex it would be to add a feature here or there. It&rsquo;s a disaster always. I guess today they will ask AI which feature is the most complex and how I can simplify it.</p> <p><strong>Miłosz</strong> [00:28:27]: Because it won&rsquo;t work, because you need this expertise to understand what&rsquo;s going on really. That&rsquo;s why you need to close the gap, have developers in a discussion.</p> <p><strong>Robert</strong> [00:28:40]: Yeah. So to maybe recap this. So I would say that there are three or four levels of how you can basically be engaged in developing the feature.</p> <p><strong>Robert</strong> [00:28:56]: The top level is pretty close to how you are working on this feature alone. In this situation you are together working with stakeholders— people that basically can make decisions if the feature can look in this way or not, because this is very important. So to have people that you can ask, okay, can we&hellip; skip this and they can say yes, no, and you can go further and not wait one week for decision. It doesn&rsquo;t need to be like that for every story. It&rsquo;s probably better for kicking off a big feature. So when we&rsquo;ve been working on some features like some bigger epics, let&rsquo;s say, that we&rsquo;ve been working for Hazier, for example, or some new functionality. This is what we&rsquo;ve been using.</p> <p><strong>Robert</strong> [00:29:41]: One lever lower, you have situation when your product owner, product manager, is gathering requirements and you are working on them together.</p> <p><strong>Robert</strong> [00:29:51]: In case of doubt, the product owner can go and ask some stakeholders if it&rsquo;s problematic or not. It works nicely for some stories because usually you can wait for a while and you&rsquo;re planning it in advance, so it&rsquo;s not an issue that it can be a bit delayed.</p> <p><strong>Robert</strong> [00:30:09]: I would probably not recommend the &lsquo;golden cages&rsquo; approach when basically you are receiving some specification or some task from somebody else and you are just just writing this without really affecting how it&rsquo;s implemented because it&rsquo;s very ineffective.</p> <p><strong>Miłosz</strong> [00:30:24]: So we said that it&rsquo;s better from product perspective.</p> <p><strong>Miłosz</strong> [00:30:29]: Work directly with domain experts and so on. But why should developers care? Because, for many developers, they are fine being just an engineer and waiting for requirements. Is there anything for them in this scenario?</p> <p><strong>Miłosz</strong> [00:30:48]: What do you think?</p> <p><strong>Robert</strong> [00:30:50]: So, I think that there are two things that you should keep in mind. And, as you said, so&hellip; Many developers, most, hard to say, but many developers don&rsquo;t care that much about, let&rsquo;s say, component success. They are just here working to code and they don&rsquo;t care that much. But I think it&rsquo;s not that simple because&hellip; You could be such this kind of developer in the market when everybody could get a job easily. But as we already said, it&rsquo;s not like that anymore. And you need to be a bit more outstanding to get job easily. And to understand job market now, it&rsquo;s probably important to understand about what companies&hellip; care when they&rsquo;re hiring developers. And well, they care about hiring people that will be useful for them. Let&rsquo;s put it in this way.</p> <p><strong>Miłosz</strong> [00:31:44]: So basically, if you are able to deliver stuff that they need, they&rsquo;re kind of happy and you need to make help them make more money, basically, so they can share some of it with you. Now you know there&rsquo;s less funding than 10 years ago, it was very easy to Oh.</p> <p><strong>Miłosz</strong> [00:32:03]: get VCs to invest in whatever idea you have. So you need to actually deliver something more often. And it&rsquo;s quite a rare skill to work directly with product. And I think for me the funny thing is: We somehow don&rsquo;t test this on interviews very often. Also, no one teaches this.</p> <p><strong>Miłosz</strong> [00:32:27]: Universities or even your job, you&rsquo;re mostly taught to write code. I would even say that we are probably more prepared to be those coding monkeys. Yeah, exactly. If you go for a tech interview, you will encounter the slit code exercises or you will be asked to write some algorithm.</p> <p><strong>Miłosz</strong> [00:32:51]: On a piece of paper or on a whiteboard. Perfect for coding monkeys.</p> <p><strong>Robert</strong> [00:32:54]: Those are requirements. Don&rsquo;t ask questions, just implement them.</p> <p><strong>Miłosz</strong> [00:32:59]: Yeah, the technical details. But then you start&hellip; Working and turns out those things are really not that much used. They are not that important. You will spend more time figuring out what to build and what not to build.</p> <p><strong>Robert</strong> [00:33:15]: And often you&rsquo;re hiring this kind of people that are great in algorithms. And later you are finding out that they are expecting them to give them tasks, requirements, and they can just code that. And obviously there are companies where it works, but in the product companies&hellip; We already covered why it&rsquo;s not working.</p> <p><strong>Robert</strong> [00:33:36]: It&rsquo;s also pretty hard to change your mindset if you&rsquo;ve been working for entire career in this way. So this is obviously the important thing to think about. I mean, if you&rsquo;re working this way.</p> <p><strong>Robert</strong> [00:33:50]: And you will be working this way for your entire&hellip; Okay, maybe not your entire life, but for longer time, it&rsquo;s harder later to switch. And companies are also— now better in picking, if people actually have some skills to be a bit more engaged in the in the way how product is developed.</p> <p><strong>Miłosz</strong> [00:34:13]: It&rsquo;s harder to hide your incompetence behind some algorithm. and technical stuff. For many product companies, it&rsquo;s not that important. For the technical skills you have, I ignore it.</p> <p><strong>Miłosz</strong> [00:34:30]: You know, data structures you can implement on a whiteboard. They are not that much relevant. They need to deliver something quickly to the market, and iterate over it, and do it over and over. So, if you can communicate well, you can talk with domain experts, you can figure out what they need to build, and you can deliver it. And that&rsquo;s someone companies probably want to hire.</p> <p><strong>Robert</strong> [00:34:54]: And even if somebody will manage to be hired, many companies are also pretty open to let some people go if they will not meet some requirements of how they expect that some people will be operating.</p> <p><strong>Miłosz</strong> [00:35:10]: And on the other hand, if you want to be promoted, for example, delivering is the very best. Probably why to do it. So instead of just waiting for requirements and complaining that it doesn&rsquo;t work, you can try to help. Whoever needs what they need. And in many companies, probably in most companies, this will help you get promoted because they will tell your manager or whoever that you do a great job. It&rsquo;s really quite simple.</p> <p><strong>Miłosz</strong> [00:35:45]: I think we can say it works.</p> <p><strong>Robert</strong> [00:35:49]: And we mentioned earlier that nobody is actually teaching us how to do that. So how to work with requirements of product owner.</p> <p><strong>Robert</strong> [00:36:01]: Nobody&rsquo;s giving us some tools how to do it efficiently. I mean, There are a lot of resources how to build classes, how to do architecture.</p> <p><strong>Robert</strong> [00:36:12]: How to, I don&rsquo;t know, use vMotion to be super fast in your IDE. And it&rsquo;s great, but from other sides, I&rsquo;m not sure if it&rsquo;s giving that much return on investment. It&rsquo;s easier to learn, for sure.</p> <p><strong>Robert</strong> [00:36:26]: And you know, it&rsquo;s coding. It&rsquo;s—uh— more cool definitely than working with product. But from other sides, I know that many people are also complaining how much time they were spending on meetings.</p> <p><strong>Robert</strong> [00:36:41]: Actually, okay, maybe if you&rsquo;re spending a lot of time on meetings, maybe it&rsquo;s worth to spend some time to understand what you can do to do it more efficiently because it&rsquo;s also a skill and often you may say, &lsquo;Okay, it&rsquo;s the responsibility of the product owner, I don&rsquo;t care.&rsquo; Yes, but unfortunately it often doesn&rsquo;t work this way. And in many companies that we worked earlier, if we wouldn&rsquo;t initialize to do some changes if it would work like it was working earlier, like people will be coding monkeys in case. The default mode. Yeah, yeah. So I think it&rsquo;s important to get some initiative and to go outside of this comfort cage or comfort zone. Oh, it sounded so coaching.</p> <p><strong>Robert</strong> [00:37:30]: Thank you. Yeah, it is what it is. I mean, sometimes you need to do some things that you are not that comfortable with, and it&rsquo;s fine.</p> <p><strong>Miłosz</strong> [00:37:37]: Yeah, but I think it&rsquo;s also important to mention it&rsquo;s not for&hellip; for company&rsquo;s sake or just because it&rsquo;s some moral thing to do. It&rsquo;s basically how you can grow your career this way. Because you become someone who can deliver stuff, and this is what companies want to pay for. Exactly. So I would say do it for your own sake, for your career&rsquo;s sake. And it will affect your productivity much, much more.</p> <p><strong>Robert</strong> [00:38:08]: I can attest that you can have a bit less of bad meetings that you have. And if you have some tools how to do that. Okay, so we&rsquo;ve already mentioned a couple of times about that we have some tools, techniques earlier, and probably you are waiting to learn what those tools or techniques are. are. So you can try to use them, experiment with them, and maybe improve a bit how you can do that.</p> <p><strong>Miłosz</strong> [00:38:35]: So, is there any tool you recommend for this?</p> <p><strong>Robert</strong> [00:38:39]: I think I&rsquo;m not sure if you agree, but I&rsquo;m quite sure that one of the tools that created most impact in the teams that we&rsquo;ve been using was brainstorming. When we&rsquo;ve been working some things that at the beginning was working this default mode close to golden cages, it helped us to&hellip; to trigger some discussion between developers and non-developers and to think more how to implement those features.</p> <p><strong>Miłosz</strong> [00:39:11]: Yeah, so maybe if someone doesn&rsquo;t know event storming, we can&hellip; Uh, give a quick TLDR: this is a brainstorming session where you gather developers and domain experts in one room and have them use sticky notes. I know how it sounds—yeah, but basically, the idea is to have this lightweight conversation about complex topics so everyone is on the same page.</p> <p><strong>Robert</strong> [00:39:42]: Even if it sounds, again, maybe I like, again, some coachy, agile, Scrum Master-ish thing.</p> <p><strong>Robert</strong> [00:39:52]: It&rsquo;s actually a very good technique created by Alberto Brandolini, who is a software engineer. He&rsquo;s not some agile coach or whatever.</p> <p><strong>Robert</strong> [00:40:02]: software engineer, so it&rsquo;s very close to implementation. So the artwork that you&rsquo;re creating is very close to implementation and it can be mapped to the code. Nicely and it gives us some nice framework to discuss some functionalities that we&rsquo;d like to implement. And we&rsquo;ve been using it in multiple contexts. So we&rsquo;ve been using it for this smaller planning for these weekly stories, but we&rsquo;ve been using it also for some bigger strategic features that we&rsquo;ve been spending, for example, here. And it&rsquo;s not about creating a big upfront design. It was rather to have some high-level idea on how everything interacts, how we would like to interact with other teams, to see complexity where it&rsquo;s there. Because very often&hellip; There is one problem with stakeholders. They are going to you with some functionality and they are saying that &lsquo;Why so long?</p> <p><strong>Miłosz</strong> [00:40:57]: Yeah.&rsquo;</p> <p><strong>Miłosz</strong> [00:40:58]: At least you have a way to have everyone start to discuss a topic in a different way than just sitting and looking at a presentation. And at first everyone is a bit confused, but then very often you have these lively discussions about the product.</p> <p><strong>Robert</strong> [00:41:20]: I can also recommend you to maybe Google event storming so you can see how the artifact at the end looks like. But the idea is that basically you are using notation of domain events.</p> <p><strong>Robert</strong> [00:41:32]: In the simplest kind of session and trying to model how our system works. And it&rsquo;s nice because it&rsquo;s creating some visible artifact that&rsquo;s showing the interactions in the system, but it&rsquo;s also showing the complexity there. I mean, if you&rsquo;re working on this artifact together, with product owner or stakeholders, it&rsquo;s kind of closing all those questions: &lsquo;why is it that complicated because if system is complicated, it&rsquo;s usually visible in the board— that&rsquo;s a very visual representation.</p> <p><strong>Miłosz</strong> [00:42:04]: And this is not a technical representation, right? So it&rsquo;s not about diagrams or database schemas.</p> <p><strong>Robert</strong> [00:42:09]: It&rsquo;s something that stakeholders can also understand, but it&rsquo;s also cool because you can also translate it later to your code very directly.</p> <p><strong>Miłosz</strong> [00:42:17]: So very often, if you do it for an existing system that has been around for a while, you will see how developers use a bit different names than other people in the company and start to close this gap.</p> <p><strong>Miłosz</strong> [00:42:33]: It&rsquo;s very interesting when you ask your colleagues from other teams to map out how it works from their perspective. Hmm-mmm. It&rsquo;s like you see a bit different system than you are used to. It&rsquo;s the same thing, but they use a bit different names. They don&rsquo;t know all the details. And if you do it together, you get a lot of insight from this.</p> <p><strong>Robert</strong> [00:43:01]: And as I said, so for us, it was game changer in multiple things that we&rsquo;ve been in. And from my perspective, it&rsquo;s really a shame that it&rsquo;s not adopted much wider. So, the domain-driven design community is probably best in adaptation of that, so a lot of domain-driven design practitioners are using that by default. I would say that outside of this cycle, it&rsquo;s not that widely adopted. Again, I think it&rsquo;s a shame. I&rsquo;m not sure actually why, but maybe it&rsquo;s the thing of the forum that for a lot of people it&rsquo;s like, &lsquo;ah, some agile or coaching stuff, sticky notes again.&rsquo; Maybe it&rsquo;s&hellip; because of some cynism. I don&rsquo;t know.</p> <p><strong>Miłosz</strong> [00:43:43]: You also need someone who facilitates this session so it doesn&rsquo;t end up with&hellip; People discussing stuff in smaller groups and not participating together or just going to some off-topic. So it&rsquo;s not that easy. Always to run it you have to figure out how to do it a bit, but I think it&rsquo;s worth it. In a way, it&rsquo;s probably better than having one person speak and everyone sleep on the chairs in the conference room. After the meeting, you decide to run another meeting because you don&rsquo;t know what to do next.</p> <p><strong>Robert</strong> [00:44:20]: I think you need to also have some more open-minded people for that. It&rsquo;s great if you would like to try that, but you&rsquo;re not always working in teams when you have good climate for running it. I know how hard it sometimes can be.</p> <p><strong>Robert</strong> [00:44:37]: It can be the reason. And in this episode, we&rsquo;ll not cover into details how to run this kind of sessions, because probably it will be&hellip; pretty long. There&rsquo;s also one very good resource from which we&rsquo;ve been learning how to do that. This is the ebook from the creator of the technique. It&rsquo;s named Introducing Event Storming by Alberto Brandolini. We&rsquo;ll link this in the episode notes.</p> <p><strong>Robert</strong> [00:45:07]: This is what I recommend you to check. But we have some tips how to try to convince your teammates, your product manager and people around to try that. Probably the first tip will be to try it maybe on a smaller scale. So don&rsquo;t start with this big session of planning for a half year, getting your stakeholders from abroad. And don&rsquo;t start with booking.</p> <p><strong>Miłosz</strong> [00:45:38]: plane tickets?</p> <p><strong>Robert</strong> [00:45:39]: Yeah, so I would recommend to start with maybe those planning sessions that you are doing with the rest of your team, maybe with product manager. And try to learn how to run those sessions. Because we&rsquo;ve been running sometimes some bigger session with 20, 30 people. It&rsquo;s much, much harder than with just your team.</p> <p><strong>Miłosz</strong> [00:45:57]: That&rsquo;s for sure. Especially if you have someone who&rsquo;s skeptical of this idea, they will just try to discourage everyone to continue because, oh, we know how everything works already. Why do you do it again?</p> <p><strong>Miłosz</strong> [00:46:14]: I remember sessions like this.</p> <p><strong>Robert</strong> [00:46:16]: But also don&rsquo;t try to look for excuses like, &lsquo;I&rsquo;m introvert, I don&rsquo;t like to do that.&rsquo; I&rsquo;m also, and I did that, and you can just do it. No worries.</p> <p><strong>Miłosz</strong> [00:46:25]: It can be tiring, but there is some end goal in all this. You want to figure out the domain. It&rsquo;s not just a meeting for the meeting&rsquo;s sake.</p> <p><strong>Robert</strong> [00:46:36]: It&rsquo;s tiring, but from other side, I think it&rsquo;s also very satisfying when it was successful and later you are sitting to implement that and you can later feel how this implementation is just flowing. And it&rsquo;s also causing a lot of questions and solving a lot of problems that we&rsquo;ve been mentioning at the beginning of the episode.</p> <p><strong>Robert</strong> [00:46:57]: So, if you would like to start with brainstorming, the first good place to try to adopt that is within your team. So, run some smaller sessions. We recommend in-person sessions since the dynamics are a bit better, but I know that also a lot of people doesn&rsquo;t have this privilege, let&rsquo;s say, to work in the office. So, if you&rsquo;re working in a hybrid way, we would recommend to gather and do this in person because it&rsquo;s working just much better. Because if you&rsquo;re doing it on Miro or any other online tool, it&rsquo;s just harder to maintain everybody&rsquo;s attention because everybody will try to open some other tab. You can do that, but it&rsquo;s super hard.</p> <p><strong>Miłosz</strong> [00:47:40]: It also happens in in-person meetings when people come to the session with their laptops. Yes, yes, and they are always very confused when you ask them to close them and stand up.</p> <p><strong>Miłosz</strong> [00:47:53]: The first sessions are always fun.</p> <p><strong>Robert</strong> [00:47:54]: But this is mentioned in the ebook, so we&rsquo;ll not go super deep into that. And it makes sense. So the first thing you should also discuss it with your team. So you should have some better understanding of what problems you may have with delivering and planning.</p> <p><strong>Robert</strong> [00:48:13]: You can use them to try to convince people that. Let&rsquo;s try to maybe do it in a different way and try to get buy-in from your team to try that.</p> <p><strong>Miłosz</strong> [00:48:23]: I know it&rsquo;s often difficult, and we often get asked how to convince my team to start using and driven design. This is a similar idea here.</p> <p><strong>Miłosz</strong> [00:48:35]: I think maybe you can find, if it&rsquo;s not your entire team, a people like likely-minded as you who want to do something better and try to figure out together how you can improve how you deliver software, basically, and like we&rsquo;ve been mentioning at the beginning—so, in our podcast, we would like to give you some tools that can help you to be promoted to principal engineer, for example.</p> <p><strong>Robert</strong> [00:49:04]: So getting buy-in of people is a very important skill to be principal engineer.</p> <p><strong>Robert</strong> [00:49:11]: We believe that if you&rsquo;re a principal engineer, you&rsquo;re not the person that is forcing people to do something. It&rsquo;s rather showing people some benefits of your ideas. They will just commit to your ideas and they will be happy because it&rsquo;s giving some solution for the problems that you have.</p> <p><strong>Miłosz</strong> [00:49:28]: Yeah, so if you want to start, you can just take your team, invite them to a session, try to figure it out together, or you can lead if you feel like it.</p> <p><strong>Miłosz</strong> [00:49:44]: Hopefully, you come at something that&rsquo;s better than your current way of working and this way they see the value. And similar with your&hellip; or product person, product owner, or product manager. You can invite them to the session. They should be probably also eager to find better ways to work for all of you because it&rsquo;s also there in their interest, unless they are, you know, kind of micromanagement freak.</p> <p><strong>Robert</strong> [00:50:17]: But from other side, this micromanagement is often coming out of something, and at the end of that, if you&hellip; go deeper, it&rsquo;s ending up that they would like to deliver stuff faster.</p> <p><strong>Miłosz</strong> [00:50:28]: If you use this card, it&rsquo;s nice to yeah and it&rsquo;s it&rsquo;s usually quite easy to point out the issues you already have. So you can look back at what happened in your recent plannings, let&rsquo;s say. You can point that out. But this feature took us two months because of all the issues we didn&rsquo;t know, of all the gaps in the requirements and so on. Maybe we could do it faster the next time. Maybe we can learn from it.</p> <p><strong>Robert</strong> [00:51:07]: It&rsquo;s also one important argument that is pretty close to the time. So it&rsquo;s money. I mean, you can say, &lsquo;Okay, last time we one month, which is this amount of money, because this is the average salary of the developer in our teams, if we could spend maybe this next time on planning it better, we could just save this money at the end.&rsquo;</p> <p><strong>Miłosz</strong> [00:51:34]: Probably the best way to introduce new concepts is to build. Trust first. So, if you just just join the team and you see how that everything is not done the way you would do it— Probably telling the team on the third day that they should reorganize is not a good idea because you don&rsquo;t know anything about the team and the company. But if you deliver consistently, you build trust, and then people will be more eager to listen to your ideas. You can gradually introduce new ways of working.</p> <p><strong>Miłosz</strong> [00:52:16]: So I would start with this. One common issue with meetings or sessions is the never-ending discussions.</p> <p><strong>Miłosz</strong> [00:52:26]: They tend to make the meetings hours long.</p> <p><strong>Robert</strong> [00:52:30]: And there are some tips about that in the ebook, but I think it&rsquo;s also worth mentioning because, even if you will read this ebook, it&rsquo;s easy to get into some never-ending discussions anyway.</p> <p><strong>Miłosz</strong> [00:52:44]: Especially if you have&hellip; If you run this big picture strategic sessions with 10 or more people in the room and you start mapping the entire flow on the board, people tend to run into discussions because you know, for every system you have in your company, there will be someone who says, &lsquo;Oh yeah, this is the issue I need this fixed&rsquo; and</p> <p><strong>Miłosz</strong> [00:53:08]: It can take a lot of time and it will derail you from the main topic. So it&rsquo;s useful to have someone act as facilitator and cut that. the discussions early.</p> <p><strong>Robert</strong> [00:53:20]: I&rsquo;d probably use better work to maybe park it because, if you cut it, somebody may feel like, &lsquo;why are we cutting that?&rsquo; It&rsquo;s probably better. So, what we&rsquo;ve been doing usually during the session was using the red sticky note. So, we&rsquo;ve been writing down the problems that we&rsquo;d like to park, maybe later during the session, maybe after the session, and put it to the board. So, the person that has this problem will feel that, &lsquo;okay, it&rsquo;s also something that it&rsquo;s not disappearing, it&rsquo;s there.&rsquo; We won&rsquo;t forget about it. Yes, yes. But important. Let&rsquo;s go back to that. Because we&rsquo;ve already mentioned the trust earlier. So, if you will be parking things and not going back to that later, you may also have problems with this trust part.</p> <p><strong>Miłosz</strong> [00:54:07]: Yeah, so later we had separate discussions, maybe in smaller teams. Maybe not everyone needs to be involved in all of the red cards. Maybe some of them we will&hellip; decide just are not important and forget about them or ignore them. But for most, we need to revisit and discuss this again. But this way, we will focus on one thing at a time.</p> <p><strong>Robert</strong> [00:54:31]: Another thing that I think is also in the ebook, but worth mentioning. So this is the time boxing problem. We&rsquo;ve already mentioned it earlier, but I think it&rsquo;s one of the biggest issues. So it&rsquo;s important to emphasize it and mention it again.</p> <p><strong>Robert</strong> [00:54:48]: Getting some balance between cutting discussions but also not skipping discussions that are important. And it&rsquo;s hard to give some good algorithm how to park some discussion and when to follow it. It&rsquo;s probably some skill that you need to develop with time, but, as we said earlier, sometimes cutting some discussion may mean that it will go back to you during the implementation and making, changing some. During implementation, it&rsquo;s much, much more expensive than during planning because you need to go back to planning. Maybe something that you already implemented is not compatible with that and you need to undo that. It&rsquo;s important to keep it in mind. Experiment. So maybe sometimes try to discuss a bit longer.</p> <p><strong>Robert</strong> [00:55:39]: Maybe sometimes try to cut, and also set expectations that, usually, when we&rsquo;ve been doing some planning sessions, some vendor storming sessions, we&rsquo;ve been like, &rsquo;excuse.&rsquo; After that, because it&rsquo;s it&rsquo;s been sometimes hours to do that, but at the end it was worth— I mean, very rarely— we had like, &lsquo;okay,&rsquo; we&rsquo;ve been spending on even storming too much, and we designed it too well.</p> <p><strong>Miłosz</strong> [00:56:04]: We could better deliver something this time.</p> <p><strong>Miłosz</strong> [00:56:08]: This comes a bit back to the topic of cutting scope. If you plan huge features, it will be very difficult to discuss all of it. Yes, but again, cutting scope is not always easy, depending on the project.</p> <p><strong>Robert</strong> [00:56:24]: And again, it&rsquo;s mentioned in the e-book, but in the&hellip; book, you can read that event storming has multiple phases. So at the beginning, you&rsquo;re generating a ton of ideas. And with the time you&rsquo;re removing more and more of the things that are not relevant. And usually, at the end, maybe not at the end, but close to the end of the session, you should do some reality check and to think: &lsquo;Okay, is what we plant here deliverable? In depends if you&rsquo;re adding some feature or doing something big, maybe a week, in worst case one month.&rsquo;</p> <p><strong>Robert</strong> [00:57:04]: If it&rsquo;s one month, probably it will be in practice a bit longer. But if it&rsquo;s longer, probably you should look at it and see what we can cut out of that.</p> <p><strong>Robert</strong> [00:57:15]: To make it functional, usable, let&rsquo;s say, and work on this basically. It&rsquo;s not obvious, but we&rsquo;re often forgetting about that and we are trying to implement a feature that It should take half a year, and it&rsquo;s going to be two years later.</p> <p><strong>Miłosz</strong> [00:57:34]: Yeah, because no one challenges it. You start planning. Some feature that comes from someone from another team or your CEO or whoever and no one challenges you.</p> <p><strong>Miłosz</strong> [00:57:47]: You need the entire thing. Maybe you need just 80% of it or 50%, and it will be much simpler to deliver.</p> <p><strong>Robert</strong> [00:57:55]: Okay, so we&rsquo;ve given you some, let&rsquo;s say, brief introduction to event storming. Again, it&rsquo;s not something for episode because I think even one episode will be not enough for that. But if it sounds interesting for you, please leave us a comment. Maybe we&rsquo;ll do some episodes just about event storming. But. Yeah, I hope that it will give you some overview why it makes sense to try to think out of this thing out of the cage.</p> <p><strong>Miłosz</strong> [00:58:29]: Maybe let&rsquo;s reiterate some ideas. What can someone do if they see this is an issue, if they want to work better with the product?</p> <p><strong>Robert</strong> [00:58:40]: Yeah, so I think at the beginning, you should assess in what level of cage you are, let&rsquo;s name it. So, if you are receiving tasks and it&rsquo;s assigned to you, or you are maybe closer to the way when you&rsquo;re planning tasks with your product owner but you&rsquo;re never discussing with stakeholders.</p> <p><strong>Robert</strong> [00:59:05]: Tchum! Think on which level you are. Mm-hmm. Uh, later. Discuss with your teammates if they also see those problems. You should be able to identify the problems with feature planning already. On this episode, so it should give you some weapons.</p> <p><strong>Miłosz</strong> [00:59:24]: Yeah, try to figure it out together. Just not try to force. Your teammates to do some of your ideas right away. Try to think together what are the issues you want to improve.</p> <p><strong>Miłosz</strong> [00:59:41]: Can write down specific problems you have or even situations. So I remember we had a very issues like this— one task has been on hold for one month because of something, I don&rsquo;t know, some miscommunication or whatever.</p> <p><strong>Robert</strong> [01:00:00]: Or maybe you lost a lot of time on implementing something that used to be useless at the end and your stakeholders said&hellip; This is not what we asked about.</p> <p><strong>Miłosz</strong> [01:00:10]: Yeah, we delivered something and then it was scraped because no one needed it. And&hellip; probably the more specific it is the better because you can then rise it with your manager or product owner or whoever is in charge Instead of complaining that we never get anything done, you tell them, yeah, this feature could have been one week. but we spent two months.</p> <p><strong>Robert</strong> [01:00:38]: In other words, use the language and arguments that other side care about. You know about what product managers will care. your teammates probably they care about their car so you can also say that okay maybe would like to if would like to get better job later or be promoted, maybe we should try to think about that. And again, if this convincing people to change approach to use your approach. It&rsquo;s a very important skill of principal engineers.</p> <p><strong>Robert</strong> [01:01:10]: Being principal engineer doesn&rsquo;t mean that you are forcing people to do something. It means that you are convincing people that your solution will help them with their issues. And that&rsquo;s it, basically.</p> <p><strong>Miłosz</strong> [01:01:21]: And if it really does, then you get trust.</p> <p><strong>Miłosz</strong> [01:01:26]: You need to accept that some people just won&rsquo;t care, and it&rsquo;s also fine. Don&rsquo;t try to force them to do anything, whether they are developers or not. And I would instead try to find people who care and try to work with them. So if your product owner doesn&rsquo;t care, doesn&rsquo;t want to change how they work, maybe to go to their manager and tell them there&rsquo;s an issue. Basically, find someone who cares about work getting done in this company.</p> <p><strong>Robert</strong> [01:01:58]: If it&rsquo;s not possible due to political reasons, just maybe do it within your team.</p> <p><strong>Robert</strong> [01:02:05]: We don&rsquo;t recommend that, but it&rsquo;s better to do event storming session within your technical team without product owner than not doing it at all.</p> <p><strong>Miłosz</strong> [01:02:16]: There&rsquo;s also the last advice we didn&rsquo;t mention in this episode yet, which is change your job.</p> <p><strong>Miłosz</strong> [01:02:22]: It&rsquo;s always the solution.</p> <p><strong>Robert</strong> [01:02:26]: But again, please don&rsquo;t use it also as an excuse to not include your product manager, because you would be surprised. I mean, how often they care more than you. You think. They just have a bit different goals, but&hellip;</p> <p><strong>Miłosz</strong> [01:02:39]: And they also might have been burned working with developers in the past. Exactly. If they worked with someone who&rsquo;s just like, yeah, I don&rsquo;t care. I will code and give me what to do. They might not be eager to work with you directly. So this goes both ways.</p> <p><strong>Robert</strong> [01:02:54]: Yeah, but from our experience, it&rsquo;s not the case in 96% of cases. It&rsquo;s very, very rarely a case that it&rsquo;s&hellip; those are people that I don&rsquo;t care. So please try and you will be really, really, really surprised. Also, I think it&rsquo;s important to set expectations.</p> <p><strong>Robert</strong> [01:03:16]: There are no magic techniques that will make all the planning accurate because ideas will always change during implementation. We are going closer to planning better.</p> <p><strong>Robert</strong> [01:03:31]: It will also always take some time to do planning. It&rsquo;s not magic tool. There is no magic tool that will totally remove meetings. It&rsquo;s just about making it more efficient. Also be prepared that, okay, if you, for example, decide to use event storming. It will be not super successful from the first session, but it&rsquo;s also a pretty simple technique, so you&rsquo;ll be surprised that the result will be pretty good anyway. But also set some expectations. Be prepared to experiment and also improve after each session. Maybe do some evaluation after every session to ask people, okay, did you like that? What you didn&rsquo;t like? And improve, improve, and improve.</p> <p><strong>Miłosz</strong> [01:04:13]: Especially if you change company culture, which is very, very hard to change. And especially if it&rsquo;s been like this for a long time.</p> <p><strong>Robert</strong> [01:04:22]: But it&rsquo;s possible. We did it more than once and later people were eager to work in this way. So it was cool. And it was also cool later to transform it to other companies and also have some success stories that helped us too.</p> <p><strong>Robert</strong> [01:04:39]: Be in different places later. Okay, so I think it will be all for today. So if you have any questions about windstorming, about getting out of the cage or any similar topics, let us know in comments on Spotify.</p> <p><strong>Miłosz</strong> [01:04:54]: Yeah, also share your stories maybe. I&rsquo;m sure there are a lot of stories how projects are run. Let us know in the comments how it works. Works for you.</p> <p><strong>Robert</strong> [01:05:05]: If you have any questions, just ask us. We are answering all the comments, so we&rsquo;ll be also happy to help you.</p> <p><strong>Robert</strong> [01:05:14]: Would be interested in listening about event storming, for example. Let us know so we can maybe also record entire episode just about event storming.</p> <p><strong>Miłosz</strong> [01:05:23]: Hit that like button and subscribe to the channel so you don&rsquo;t miss the next episodes. We plan to release a new episode every two weeks now.</p> <p><strong>Robert</strong> [01:05:32]: Yep. So, thank you, Miłosz, for your time. And see you in two weeks.</p> <p><strong>Miłosz</strong> [01:05:35]: Thank you, Robert. See you.</p> <p><strong>Robert</strong> [01:05:36]: Bye.</p>Durable Background Execution with Go and SQLitehttps://threedots.tech/post/sqlite-durable-execution/Mon, 20 Oct 2025 00:00:00 +0200https://threedots.tech/post/sqlite-durable-execution/<p><strong>A DNS outage is like a flash flood. It hits you and disappears as if it were never there.</strong> &ldquo;It was probably the DNS&rdquo; became a meme. But, half the time, we do not know the exact cause. The outage is seldom long enough to diagnose, sequester, replicate, simulate, and test the failure conditions. How can we be certain that this type of failure will not compromise our systems again in the future? The DNS can fail spectacularly in a variety of ways.</p> <p>A better way to address intermittent failures, such as DNS outages, is to accept them as unavoidable occasional execution disruptions. We might not be able to address each of them exhaustively, but we can design our system to handle almost any interruption of its internal logic. <strong>We can harden the system against the disruptions to attain <em>durable execution</em>.</strong></p> <h2 id="what-is-durable-execution">What is Durable Execution?</h2> <p><em>Durable execution</em> is a guarantee that a program output is correct regardless of failures anywhere in the execution process.</p> <p>Competitive business applications require <em>durable execution</em>. When it is absent, an employee will spend an hour or two fixing each incorrect output when it is detected. In large business applications, incorrect output often goes undetected for weeks, resulting in expensive chaos around holidays.</p> <p>Are we talking about bugs? No. Bugs are the program. Some intermittent bugs erode durability, but the durability loss is most often a result of an interruption of the execution process. The interruption can be caused by a network outage, service failure, security breach, or a delinquent update. All of those reasons are common. <em>Durable execution</em> does not care what caused it. It is a promise to recover from almost any failure or combination of failures and deliver the correct output.</p> <p>Event-Driven Architecture (EDA) is the industry best practice for supporting <em>durable execution</em>. However, most EDA production systems are not provably durable. What happened?</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>In the second guest post on our blog, we welcome Dima Kotik, who contributed the SQLite Pub/Sub backend to Watermill. Please give Dima a warm welcome! 🎉</p> <p><em>Miłosz &amp; Robert</em></p> </p></div> </div> <h2 id="durable-execution-requires-immediate-input-storage">Durable Execution Requires Immediate Input Storage</h2> <p>There is a difference between a guarantee and a wish. A guarantee has some proof, a history of recovery from a variety of system crashes. It begins with emulating crash conditions in integration tests. After writing many such tests, you will realize a simple but profound principle: <strong><em>durable execution</em> begins with writing execution state into storage</strong>. There is no other way to proceed, because anything present in random-access memory or ephemeral storage is liable to disappear if the system crashes.</p> <p>The first step of a durable system is to stash its input <strong>before any processing takes place</strong> after parsing and validation. Security is more important than durability, so input validation takes precedence.</p> <h2 id="stash-the-input-after-parsing-and-validation">Stash the Input After Parsing and Validation</h2> <p>Wait, what? I have to commit all the system input immediately into storage? Yes. Is that even possible? Not all at once, but this is not as daunting a task as one might imagine.</p> <p>If you are using event-driven architecture with an publisher/subscriber management library like <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2F" target="_blank">Watermill</a>, you are already halfway there. Watermill is one of the most popular event buses in the Go ecosystem.</p> <p>Watermill is an unusually flexible library in that it can bridge events from a large number of different backends. Your core business logic may stream events to a PostgreSQL database, and your input may initially be stored in a SQLite databases of your edge nodes to achieve durability without sacrificing performance. Watermill can link the two to tango together seamlessly.</p> <p>Unfortunately, many business applications that utilize Watermill continue to use the Go channel ephemeral backend here and there despite the documentation urging developers not to do it. This backend is a basic in-memory implementation based on Go channels intended for testing purposes. The events are queued into an asynchronous channel. They will disappear when the system crashes. They will clog up the server with a long queue if the execution process slows down for any reason. They will compete for resources with a critical server process.</p> <p>In other words, do not write input into random-access memory. You are asking for trouble.</p> <p>There is an easy persistent drop-in replacement for an ephemeral backend. It is not a silver bullet, but it is an easy first step towards durable computing. Try the new <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Fsqlite%2F" target="_blank">Watermill SQLite backend</a>. It does not require setting up any additional infrastructure, and it is more performant and reliable than any other storage method because it does not need a network connection or a custom transport protocol. It writes to a file on a disk. <strong>It does not require CGO</strong>.</p> <p>Suppose we are processing book orders, and we would like to store the orders as events in an SQLite database:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">db</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">sql</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="s">&#34;sqlite&#34;</span><span class="p">,</span> <span class="s">&#34;orders.sqlite3?journal_mode=WAL&amp;busy_timeout=1000&amp;cache=shared&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="k">defer</span> <span class="nx">db</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nf">SetMaxOpenConns</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="c1">// driver limitation </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">publisher</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">wmsqlitemodernc</span><span class="p">.</span><span class="nf">NewPublisher</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">wmsqlitemodernc</span><span class="p">.</span><span class="nx">PublisherOptions</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">InitializeSchema</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">msg</span> <span class="o">:=</span> <span class="nx">message</span><span class="p">.</span><span class="nf">NewMessage</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">watermill</span><span class="p">.</span><span class="nf">NewUUID</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="c1">// serialized order event </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">`{&#34;order_id&#34;: &#34;1&#34;, &#34;title&#34;: &#34;1984&#34;, &#34;customer&#34;: &#34;Queen Elizabeth II&#34;, &#34;address&#34;: &#34;Buckingham Palace&#34;}`</span><span class="p">),</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">publisher</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="s">&#34;book_orders&#34;</span><span class="p">,</span> <span class="nx">msg</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>For cloud instances, ensure that you are writing the SQLite file to a persistent volume, which is mounted explicitly. The root file system is typically ephemeral. You may want to eventually migrate the collected input into your main database after thinking about performance implications.</p> <p>The matching subscriber is not any different from normal Watermill usage other than the backend initialization:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">subscriber</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">wmsqlitemodernc</span><span class="p">.</span><span class="nf">NewSubscriber</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">wmsqlitemodernc</span><span class="p">.</span><span class="nx">SubscriberOptions</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">InitializeSchema</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">messages</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">subscriber</span><span class="p">.</span><span class="nf">Subscribe</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="s">&#34;book_orders&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">msg</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">messages</span> <span class="p">{</span> <span class="c1">// consume messages from the channel </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">processBookOrder</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="nx">msg</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>The SQLite publisher and subscriber are drop-in replacements. <strong>They can be also used without Watermill router.</strong> In other words, you can just use low-level <code>Publish</code> and <code>Subscribe</code> methods.</p> <p>Full examples are provided in <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Fsqlite%2F" target="_blank">documentation</a>. Nothing else needs to change in your code to achieve this first step towards <em>durable execution</em>: write the input events to persistent storage.</p> <p>For complete durability, however, the event handlers for each step of processing the book orders must have two provable qualities:</p> <ol> <li><strong>Idempotency</strong>: the handler can consume the same event multiple times without generating more than one downstream event.</li> <li><strong>Atomicity</strong>: the handler either completes successfully or fails completely. It must never succeed partially.</li> </ol> <p>The combination of these properties with persistent storage ensures <em>durable execution</em>. When the a step in business logic fails for any reason, it is attempted again from the last safe system state.</p> <h2 id="prove-idempotency-with-event-duplication">Prove Idempotency with Event Duplication</h2> <p>Idempotent event handlers must not change system state when processing identical events. In other words, any repetition of input any number of times makes no difference to the system. We can achieve this effect by de-duplicating events, but it is best that the program logic itself tolerates repetition.</p> <p>First, ensure that the event message is acknowledged only after the all the business logic has been executed successfully:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">processBookOrder</span><span class="p">(</span><span class="nx">db</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DB</span><span class="p">,</span> <span class="nx">msg</span> <span class="o">*</span><span class="nx">watermill</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// use only one database transaction per event handler </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">tx</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">BeginTx</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="o">&amp;</span><span class="nx">sql</span><span class="p">.</span><span class="nx">TxOptions</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Isolation</span><span class="p">:</span> <span class="nx">sql</span><span class="p">.</span><span class="nx">LevelReadCommitted</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">_</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nx">msg</span><span class="p">.</span><span class="nf">Nack</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Commit</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="c1">// acknowledge the message ONLY AFTER successful transaction commit </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// what happens if the the execution fails here? </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">msg</span><span class="p">.</span><span class="nf">Ack</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">msg</span><span class="p">.</span><span class="nf">Nack</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// process input using the business domain logic </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>What happens if the transaction succeeds but the message acknowledgment fails on line 16? If the handler is idempotent, the event will be reprocessed, but the system state will not change. An easy way to achieve this is to constrain the database table field <code>order_id</code> to be unique. Then, drop the error if it is caused by a unique constraint violation:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Example for SQLite3 &#34;modernc.org/sqlite/lib&#34; </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">liteErr</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">err</span><span class="p">.(</span><span class="o">*</span><span class="nx">sqlite</span><span class="p">.</span><span class="nx">Error</span><span class="p">);</span> <span class="nx">ok</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">code</span> <span class="o">:=</span> <span class="nx">liteErr</span><span class="p">.</span><span class="nf">Code</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">code</span> <span class="o">==</span> <span class="nx">lib</span><span class="p">.</span><span class="nx">SQLITE_CONSTRAINT_PRIMARYKEY</span> <span class="o">||</span> <span class="nx">code</span> <span class="o">==</span> <span class="nx">lib</span><span class="p">.</span><span class="nx">SQLITE_CONSTRAINT_UNIQUE</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// duplicate order id is normal behavior </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// it means that the event was already processed </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// ignore this error </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// Example for PostgreSQL&#39;s &#34;github.com/lib/pq&#34; driver </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">const</span> <span class="nx">UniqueViolationErr</span> <span class="p">=</span> <span class="nx">pq</span><span class="p">.</span><span class="nf">ErrorCode</span><span class="p">(</span><span class="s">&#34;23505&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">UniqueViolationErr</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// duplicate order id is normal behavior </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// it means that the event was already processed </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// ignore this error </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span></code></pre></div><p>What if there are multiple SQL table key constraints? The best solution is to have an idempotency test in place. Then, methodically groom the business logic until it tolerates repetition and passes the test. Use <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fmiddlewares%2F%23duplicator" target="_blank">duplicator</a> handler middleware to clone messages and check that the final program state is the same as running the test without the duplicator.</p> <h2 id="prove-atomicity-with-chaos-engineering">Prove Atomicity with Chaos Engineering</h2> <p>Once we achieve idempotency, how can we prove atomicity for every step of the business logic process?</p> <p>At-least-once atomic execution is theoretically facilitated by Watermill, but there are subtle ways of subverting it. If you rely on low-level primitives, always acknowledge the message <strong>after</strong> all the business logic finishes. And, ensure that business logic is granular enough that <strong>all of it</strong> runs anew in case of an interruption.</p> <p>A simple rule to follow is that <strong>all state changes in one event handler must apply within the same database transaction</strong>. If you have two database transactions, you will never achieve <em>durable execution</em>. If you are publishing another event inside the event handler, that event publisher must operate within the same transaction as the event handler. The <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdistributed-transactions-in-go%2F%23the-outbox-pattern" target="_blank">outbox pattern</a> is essential when making any additional changes to the database together with publishing the event.</p> <p>How do you prove atomicity with a test? Write a chaos middleware to pair it with the retry middleware for your testing setup:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">ApplyChaos</span><span class="p">(</span><span class="nx">h</span> <span class="nx">message</span><span class="p">.</span><span class="nx">HandlerFunc</span><span class="p">)</span> <span class="nx">message</span><span class="p">.</span><span class="nx">HandlerFunc</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">failNextOperation</span> <span class="o">:=</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kd">func</span><span class="p">(</span><span class="nx">msg</span> <span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="p">([]</span><span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">result</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">h</span><span class="p">(</span><span class="nx">msg</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">failNextOperation</span> <span class="p">=</span> <span class="p">!</span><span class="nx">failNextOperation</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">failNextOperation</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;world chaos cancelled the event handler&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">result</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This middleware will break the event handlers half the time. The retry middleware should ensure that the operations complete even with half of the system experiencing outages.</p> <p>How do you prove that an event handler is atomic? Write another middleware that pollutes the message context:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">PolluteContextWithChaos</span><span class="p">(</span><span class="nx">h</span> <span class="nx">message</span><span class="p">.</span><span class="nx">HandlerFunc</span><span class="p">)</span> <span class="nx">message</span><span class="p">.</span><span class="nx">HandlerFunc</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kd">func</span><span class="p">(</span><span class="nx">msg</span> <span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="p">([]</span><span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">polluted</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithCancel</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nf">Context</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="nf">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="o">&lt;-</span><span class="nx">time</span><span class="p">.</span><span class="nf">After</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">cancel</span><span class="p">()</span> <span class="c1">// simulate premature cancellation </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">}()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">msg</span><span class="p">.</span><span class="nf">SetContext</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">h</span><span class="p">(</span><span class="nx">msg</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>If all your input and output operations are context-aware (they should be - it is best practice), the premature context cancellation will simulate an execution interruption inside the event handler. This simple example cancels after one millisecond.</p> <p>Simulate a variety of pseudo-random cancellation patterns. Never use true random, because it will make the tests brittle. Check that the output is the same, and you are well on your way to a durable system.</p> <h2 id="conclusion">Conclusion</h2> <p>Business applications need <em>durable execution</em>. Programs that have an innate ability to recover from almost any error or crash are a joy to work with. Durability can be achieved with proper system design and a rigorous testing discipline.</p> <p>You can see how <em>durable execution</em> can get complicated quickly, but the first step is the easiest. <strong>Write the input event to a persistent volume.</strong> It will give you some durability right away. You can work out the rest of it later. <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Fsqlite%2F" target="_blank">SQLite Pub/Sub backend</a> for Watermill is a great choice to try this. This new driver works <strong>without CGO</strong> and, like Watermill, is free and open source.</p> <p>Cloud providers have begun to offer distributed SQLite clusters for use cases that offer some durability and the fastest possible read speeds. Those projects are a strong indicator that SQLite-powered storage is a viable and increasingly desirable option for <em>durable execution</em>:</p> <ol> <li><a href="proxy.php?url=https%3A%2F%2Ffly.io%2Fdocs%2Flitefs%2F" target="_blank">LiteFS</a> keeps all the classic SQLite conventions like the single-writer constraint. This gives good reasons to anticipate that in the future <em>durable execution</em> could also become fast even with global transactions.</li> <li><a href="proxy.php?url=https%3A%2F%2Fturso.tech%2F" target="_blank">Turso</a> comfortably runs on edge or mobile devices with poor connectivity.</li> <li>Cloudflare <a href="proxy.php?url=https%3A%2F%2Fblog.cloudflare.com%2Fsqlite-in-durable-objects%2F" target="_blank">durable objects</a> are an SQLite persistence companion to Cloudflare Workers.</li> </ol> <p>There are two disadvantages to those commerical offerings compared to Watermill over SQLite Pub/Sub: (1) the vendor lock-in and (2) some difficulties in binding database transactions to event handlers reliably. This means that Watermill might get you farther past persistence towards idempotency and atomicity on the journey to provable <em>durable execution</em>. Give it a try!</p>SQLite Pub/Sub, Quickstart, and more — Watermill 1.5 Releasedhttps://threedots.tech/post/watermill-1-5/Wed, 24 Sep 2025 00:00:00 +0200https://threedots.tech/post/watermill-1-5/<p>It&rsquo;s been almost a year since the last Watermill release post, and we have a bunch of news to share!</p> <p>In case you&rsquo;re new here, <strong><a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">Watermill</a> is a Go library for building event-driven applications the easy way.</strong> It&rsquo;s like an HTTP router but for messages. Watermill is a library, not a framework, so there&rsquo;s no vendor lock-in.</p> <p>Watermill is now close to <strong>9k GitHub stars and 79 contributors,</strong> just on the main repository. It supports <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2F" target="_blank">13 Pub/Subs</a> including Kafka, Redis Streams, NATS, Google Cloud Pub/Sub, Amazon SQS/SNS, SQL, and RabbitMQ.</p> <p>See <a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">the documentation</a> for more details.</p> <p>Let&rsquo;s look at what&rsquo;s new.</p> <h3 id="sqlite-pubsub">SQLite Pub/Sub</h3> <p>Watermill now officially supports the SQLite Pub/Sub. The messages are moved between publishers and subscribers using tables in an SQLite database.</p> <p>A huge thank you to <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdkotik" target="_blank">Dima Kotik</a> who contributed the new package: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-sqlite" target="_blank">github.com/ThreeDotsLabs/watermill-sqlite</a>. See the <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Fsqlite%2F" target="_blank">documentation</a>.</p> <p>SQLite Pub/Sub is simple to set up, as you don&rsquo;t need to run any external services: everything is just a file on disk. It supports persistence, so you don&rsquo;t lose messages when your application restarts.</p> <p>So far, we recommended PostgreSQL or MySQL Pub/Sub for when you don&rsquo;t want to set up a separate message broker. Now it&rsquo;s even easier to start with SQLite.</p> <p>It can also be a great alternative to using the GoChannel Pub/Sub in smaller projects.</p> <p><strong>We&rsquo;ll soon publish Dima&rsquo;s article about when it shines.</strong> Stay tuned!</p> <p>The implementation has two packages: <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmodernc.org%2Fsqlite">modernc.org/sqlite</a> and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fzombiezen.com%2Fgo%2Fsqlite">zombiezen.com/go/sqlite</a>. You can choose either one when creating the Pub/Sub.</p> <p>Here&rsquo;s an example of creating a Subscriber with the modernc driver:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span> <span class="s">&#34;github.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="nx">subscriber</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">wmsqlitemodernc</span><span class="p">.</span><span class="nf">NewSubscriber</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">wmsqlitemodernc</span><span class="p">.</span><span class="nx">SubscriberOptions</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">InitializeSchema</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Logger</span><span class="p">:</span> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">messages</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">subscriber</span><span class="p">.</span><span class="nf">Subscribe</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;messages&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">msg</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">messages</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;Received message: %s, payload: %s\n&#34;</span><span class="p">,</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">UUID</span><span class="p">,</span> <span class="nb">string</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">Payload</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">msg</span><span class="p">.</span><span class="nf">Ack</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>And then publishing messages:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span> <span class="s">&#34;github.com/ThreeDotsLabs/watermill-sqlite/wmsqlitemodernc&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="nx">publisher</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">wmsqlitemodernc</span><span class="p">.</span><span class="nf">NewPublisher</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">wmsqlitemodernc</span><span class="p">.</span><span class="nx">PublisherOptions</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">InitializeSchema</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Logger</span><span class="p">:</span> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">msg</span> <span class="o">:=</span> <span class="nx">message</span><span class="p">.</span><span class="nf">NewMessage</span><span class="p">(</span><span class="nx">watermill</span><span class="p">.</span><span class="nf">NewUUID</span><span class="p">(),</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">`{&#34;message&#34;: &#34;Hello, world!&#34;}`</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">err</span> <span class="o">:=</span> <span class="nx">publisher</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="s">&#34;messages&#34;</span><span class="p">,</span> <span class="nx">msg</span><span class="p">)</span> </span></span></code></pre></div><p><strong>We consider this a beta release:</strong> the full test suite passes (and it&rsquo;s pretty extensive!), but we want to gather more feedback from real-world usage. If you try it, please share your experience. You can use the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues" target="_blank">main repository&rsquo;s issues</a> or <a href="proxy.php?url=https%3A%2F%2Fdiscord.gg%2FQV6VFg4YQE" target="_blank">join our Discord</a>.</p> <h3 id="watermill-quickstart">Watermill Quickstart</h3> <p>We recently launched <strong><a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Flearn%2Fquickstart%2F" target="_blank">Watermill Quickstart</a></strong>, <strong>a free hands-on training</strong> that teaches you how to use Watermill in your project.</p> <p>We keep looking for the most effective way to learn programming topics. It&rsquo;s clear that reading docs and books is not enough: you learn best when you actually practice by writing code, failing, and fixing it.</p> <p>The common advice for newcomers in any technology is to &ldquo;create a pet project.&rdquo; But coming up with an idea and sticking with it long enough to learn something isn&rsquo;t trivial.</p> <p>The Quickstart guides you to build this kind of project. You code in your own IDE, so the next time you want to use what you learned, you already know the setup and have the experience.</p> <p>If you try it, let us know what you think!</p> <p><a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Flearn%2Fquickstart%2F" target="_blank"> <figure class="img-center" role="group" aria-describedby="caption-Watermill Quickstart"> <img title="" loading="lazy" decoding="async" class="img " width="1000" height="672" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-5%2Fimages%2Fquickstart_hu68d1533cd22219a6e9e9cc6530688d94_156090_1000x672_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwatermill-1-5%5C%2Fimages%5C%2Fquickstart_hu68d1533cd22219a6e9e9cc6530688d94_156090_1000x672_resize_lanczos_3.png"" /> <figcaption id="caption-Watermill Quickstart" class="caption-Watermill-Quickstart"> Watermill Quickstart </figcaption> </figure> </a></p> <h3 id="watermill-v15">Watermill v1.5</h3> <p>While there are no major new features in the main Watermill package, we reviewed a ton of PRs and merged many improvements and fixes. You can see the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Freleases" target="_blank">releases page</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fcompare%2Fv1.4.0...v1.5.1" target="_blank">the full changeset on GitHub</a>. Thank you to everyone who contributed, and please keep the PRs coming!</p> <p>Let&rsquo;s look at some API changes and deprecations.</p> <h4 id="cqrs-marshaler-updates">CQRS Marshaler updates</h4> <p><strong><code>cqrs.ProtobufMarshaler</code> is now deprecated and replaced with <code>cqrs.ProtoMarshaler</code>.</strong> Ideally, we&rsquo;d remove it completely to get rid of the <code>github.com/gogo/protobuf</code> dependency, but that would be a breaking change. We recommend using <code>ProtoMarshaler</code>, which uses <code>google.golang.org/protobuf</code> instead.</p> <p>There is now an easier way to decorate marshallers. If you want to add some extra logic when marshaling or unmarshaling commands or events, you usually wrap the marshaler in a custom struct.</p> <p>Now you can use <code>cqrs.CommandEventMarshalerDecorator</code> to do that with less boilerplate.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">cqrsMarshaler</span> <span class="o">:=</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">CommandEventMarshalerDecorator</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">CommandEventMarshaler</span><span class="p">:</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">JSONMarshaler</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">DecorateFunc</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">v</span> <span class="nx">any</span><span class="p">,</span> <span class="nx">msg</span> <span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">event</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">v</span><span class="p">.(</span><span class="nx">Event</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;%T does not implement Event and can&#39;t be marshaled&#34;</span><span class="p">,</span> <span class="nx">v</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">partitionKey</span> <span class="o">:=</span> <span class="nx">event</span><span class="p">.</span><span class="nf">PartitionKey</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">partitionKey</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;PartitionKey is empty&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">msg</span><span class="p">.</span><span class="nx">Metadata</span><span class="p">.</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;partition_key&#34;</span><span class="p">,</span> <span class="nx">partitionKey</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h4 id="small-api-changes">Small API changes</h4> <p>You can now create messages with context using <code>message.NewMessageWithContext</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">msg</span> <span class="o">:=</span> <span class="nx">message</span><span class="p">.</span><span class="nf">NewMessageWithContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">watermill</span><span class="p">.</span><span class="nf">NewUUID</span><span class="p">(),</span> <span class="nx">payload</span><span class="p">)</span> </span></span></code></pre></div><p>This is a trivial addition, but we hope it&rsquo;ll help make it clear that <strong>it&rsquo;s the message that carries the context</strong>. For newcomers, it can be confusing that <code>Publish()</code> takes no context argument. This API is similar to <code>net/http</code> with <code>NewRequestWithContext</code>. A tiny change that should improve the developer experience.</p> <p>Similarly, we deprecated the Router&rsquo;s <code>AddNoPublisherHandler</code> in favor of <code>AddConsumerHandler</code>. When we initially designed the Router, we assumed the default behavior of handlers would be to always publish messages. In practice, it&rsquo;s the opposite: most handlers consume messages and don&rsquo;t publish anything, or publish the message in other ways. <code>AddNoPublisherHandler</code> sounds a bit awkward, so we decided to rename it.</p> <h4 id="context-in-gochannel-pubsub">Context in GoChannel Pub/Sub</h4> <p>There is now an option to <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fpull%2F487" target="_blank">propagate the message context</a> to the handler when using the GoChannel Pub/Sub.</p> <p>This has been something many people have asked for, especially for passing some arbitrary values through the context. You can enable it by setting the <code>PreserveContext</code> field to <code>true</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">pubSub</span> <span class="o">:=</span> <span class="nx">gochannel</span><span class="p">.</span><span class="nf">NewGoChannel</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">gochannel</span><span class="p">.</span><span class="nx">Config</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">PreserveContext</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><p>Since GoChannel works in-memory, you can access the context you set when creating the message in the handler.</p> <p>However, <strong>keep in mind this is not how most Pub/Subs work.</strong> When using Kafka, Redis, or any other &ldquo;proper&rdquo; Pub/Sub, the context is lost when the message is sent over the network.</p> <p>Use it only if you know what you&rsquo;re doing! If you switch to another Pub/Sub later, this will break.</p> <h3 id="watermill-sql-v4">Watermill SQL v4</h3> <p>We finally released watermill-sql v4.0.0 after a long time as a release candidate.</p> <p>You can now use it with packages other than <code>database/sql</code>, like <code>pgx</code> (Postgres driver) or ORMs. This includes support for transactions, which is key for the Outbox pattern. Shoutout to <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjulesjcraske" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjulesjcraske.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @julesjcraske</a> for contributing these changes!</p> <p>Subscriber and Publisher constructors now take a <code>Beginner</code> interface argument instead of <code>*sql.DB</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">subscriber</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">sql</span><span class="p">.</span><span class="nf">NewSubscriber</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">sql</span><span class="p">.</span><span class="nf">BeginnerFromStdSQL</span><span class="p">(</span><span class="nx">db</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">sql</span><span class="p">.</span><span class="nx">SubscriberConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">SchemaAdapter</span><span class="p">:</span> <span class="nx">sql</span><span class="p">.</span><span class="nx">DefaultMySQLSchema</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">OffsetsAdapter</span><span class="p">:</span> <span class="nx">sql</span><span class="p">.</span><span class="nx">DefaultMySQLOffsetsAdapter</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">InitializeSchema</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><p><strong>Migration</strong>: Wrap your <code>*sql.DB</code> in <code>sql.BeginnerFromStdSQL(db)</code> when passing it to the watermill-sql API.</p> <p>Pgx has its own <code>BeginnerFromPgx(db)</code> constructor. When working with transactions, use <code>TxFromStdSQL(tx)</code> or <code>TxFromPgx(tx)</code> instead. If you use another driver or ORM, you can implement your own adapter that implements the <code>Beginner</code> or <code>Tx</code> interface. See the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-sql%2Fblob%2Fa57c44891ee34c2ce0a6e4ff9fc28147166e70cd%2Fpkg%2Fsql%2Fadapters_std_sql.go" target="_blank">implementation</a>.</p> <p>The other major change is that we decided to make <code>SchemaAdapter</code> and <code>OffsetsAdapter</code> interfaces more flexible. Instead of raw arguments, they now take a <code>params</code> struct and always return an error. We will stick to this approach for most APIs going forward. While more verbose, it&rsquo;s easier to extend without breaking changes.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">SchemaAdapter</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">InsertQuery</span><span class="p">(</span><span class="nx">params</span> <span class="nx">InsertQueryParams</span><span class="p">)</span> <span class="p">(</span><span class="nx">Query</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">SelectQuery</span><span class="p">(</span><span class="nx">params</span> <span class="nx">SelectQueryParams</span><span class="p">)</span> <span class="p">(</span><span class="nx">Query</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">UnmarshalMessage</span><span class="p">(</span><span class="nx">params</span> <span class="nx">UnmarshalMessageParams</span><span class="p">)</span> <span class="p">(</span><span class="nx">Row</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">SchemaInitializingQueries</span><span class="p">(</span><span class="nx">params</span> <span class="nx">SchemaInitializingQueriesParams</span><span class="p">)</span> <span class="p">([]</span><span class="nx">Query</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">SubscribeIsolationLevel</span><span class="p">()</span> <span class="nx">sql</span><span class="p">.</span><span class="nx">IsolationLevel</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">OffsetsAdapter</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">AckMessageQuery</span><span class="p">(</span><span class="nx">params</span> <span class="nx">AckMessageQueryParams</span><span class="p">)</span> <span class="p">(</span><span class="nx">Query</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">ConsumedMessageQuery</span><span class="p">(</span><span class="nx">params</span> <span class="nx">ConsumedMessageQueryParams</span><span class="p">)</span> <span class="p">(</span><span class="nx">Query</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">NextOffsetQuery</span><span class="p">(</span><span class="nx">params</span> <span class="nx">NextOffsetQueryParams</span><span class="p">)</span> <span class="p">(</span><span class="nx">Query</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">SchemaInitializingQueries</span><span class="p">(</span><span class="nx">params</span> <span class="nx">OffsetsSchemaInitializingQueriesParams</span><span class="p">)</span> <span class="p">([]</span><span class="nx">Query</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">BeforeSubscribingQueries</span><span class="p">(</span><span class="nx">params</span> <span class="nx">BeforeSubscribingQueriesParams</span><span class="p">)</span> <span class="p">([]</span><span class="nx">Query</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><strong>Migration:</strong> If you use the default schema or offsets adapters, you don&rsquo;t need to change anything. If you have custom implementations, update the method signatures to match the new ones.</p> <h3 id="watermill-googlecloud-v2">Watermill GoogleCloud v2</h3> <p>The Go Pub/Sub client library v1 <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgoogleapis%2Fgoogle-cloud-go%2Fblob%2Fmain%2Fpubsub%2FMIGRATING.md" target="_blank">will lose support next year</a>. We updated watermill-googlecloud to use the new v2 library.</p> <p>There&rsquo;s only one breaking change: <code>SubscriberConfig</code> no longer exposes the <code>SubscriptionConfig</code> field. Instead, it now has the <code>GenerateSubscription</code> function.</p> <p>The function returns <code>*pubsubpb.Subscription</code>, which mostly maps 1:1 with the old <code>SubscriptionConfig</code>.</p> <p><strong>Migration</strong>: Replace <code>SubscriptionConfig</code> with <code>GenerateSubscription</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> googlecloud.SubscriberConfig{ </span></span><span class="line"><span class="cl"> ProjectID: &#34;tests&#34;, </span></span><span class="line"><span class="cl"> GenerateSubscriptionName: subscriptionName, </span></span><span class="line"><span class="cl"><span class="gd">- SubscriptionConfig: pubsub.SubscriptionConfig{ </span></span></span><span class="line"><span class="cl"><span class="gd">- RetainAckedMessages: false, </span></span></span><span class="line"><span class="cl"><span class="gd">- EnableMessageOrdering: enableMessageOrdering, </span></span></span><span class="line"><span class="cl"><span class="gd">- }, </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+ GenerateSubscription: func(params googlecloud.GenerateSubscriptionParams) *pubsubpb.Subscription { </span></span></span><span class="line"><span class="cl"><span class="gi">+ return &amp;pubsubpb.Subscription{ </span></span></span><span class="line"><span class="cl"><span class="gi">+ RetainAckedMessages: false, </span></span></span><span class="line"><span class="cl"><span class="gi">+ EnableMessageOrdering: enableMessageOrdering, </span></span></span><span class="line"><span class="cl"><span class="gi">+ } </span></span></span><span class="line"><span class="cl"><span class="gi">+ }, </span></span></span><span class="line"><span class="cl"><span class="gi"></span> Unmarshaler: unmarshaler, </span></span><span class="line"><span class="cl"> }, </span></span></code></pre></div><p>We considered mapping the config to make this change non-breaking. While it&rsquo;s possible, we wouldn&rsquo;t get rid of the old dependency, and we&rsquo;ve seen reports that the old and new clients don&rsquo;t work well together. So this means a major version bump, but the migration should be straightforward.</p> <h3 id="hall-of-fame-">Hall of Fame 🏆</h3> <p><strong>We had 43 unique contributors to all Watermill repositories since <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-4%2F">the last release post</a>! 💪</strong> Thank you all for your help!</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FAbdullah-AlAttar" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FAbdullah-AlAttar.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @Abdullah-AlAttar</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fabramovd" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fabramovd.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @abramovd</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FAceFire6" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FAceFire6.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @AceFire6</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Faddshore" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Faddshore.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @addshore</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Falexandregv" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Falexandregv.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @alexandregv</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FAmr-Shams" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FAmr-Shams.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @Amr-Shams</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Favlajcic-axilis" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Favlajcic-axilis.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @avlajcic-axilis</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fchaindead" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fchaindead.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @chaindead</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fchengehe" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fchengehe.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @chengehe</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fco60ca" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fco60ca.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @co60ca</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdkotik" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdkotik.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @dkotik</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fedebernis" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fedebernis.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @edebernis</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fekazakas" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fekazakas.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @ekazakas</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FEvanMad" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FEvanMad.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @EvanMad</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FHansvdLaanNedap" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FHansvdLaanNedap.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @HansvdLaanNedap</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FHolyKingCrusader" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FHolyKingCrusader.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @HolyKingCrusader</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjgersdorf" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjgersdorf.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @jgersdorf</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjjcollinge" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjjcollinge.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @jjcollinge</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjlevesy" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjlevesy.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @jlevesy</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjpradass" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjpradass.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @jpradass</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjulesjcraske" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjulesjcraske.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @julesjcraske</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmarczahnfc" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmarczahnfc.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @marczahnfc</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmatdurand" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmatdurand.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @matdurand</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fminxinyi" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fminxinyi.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @minxinyi</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FNathanBaulch" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FNathanBaulch.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @NathanBaulch</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fnkonev" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fnkonev.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @nkonev</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FOrbsynated" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FOrbsynated.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @Orbsynated</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fpetergere" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fpetergere.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @petergere</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fpxwanglu" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fpxwanglu.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @pxwanglu</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Frhhammond" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Frhhammond.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @rhhammond</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsamartha-kar23" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsamartha-kar23.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @samartha-kar23</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fseamusv" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fseamusv.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @seamusv</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fstephanvanzwienen" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fstephanvanzwienen.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @stephanvanzwienen</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsysradium" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsysradium.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @sysradium</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fthpk" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fthpk.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @thpk</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Ftjnet" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Ftjnet.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @tjnet</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Funaik" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Funaik.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @unaik</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fweiye-Lee" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fweiye-Lee.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @weiye-Lee</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fwhosehang" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fwhosehang.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @whosehang</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fxico42" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fxico42.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @xico42</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fyashb042" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fyashb042.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @yashb042</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fygaros" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fygaros.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @ygaros</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fytnsym" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fytnsym.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @ytnsym</a> </li> </ul> <h3 id="docs-refresh">Docs refresh</h3> <p>We updated <a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">watermill.io</a> a bit, so it&rsquo;s easier to tell what the library is about. It should also be easier to choose the proper <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Flearn%2F" target="_blank">learning path</a>.</p> <p>Once again, give the <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Flearn%2Fquickstart%2F" target="_blank">Quickstart</a> a try and let us know how it went.</p> <p>We consider making free training like this for other open-source libraries as well. If you have any suggestions, please share!</p>Shipping an AI Agent that Lies to Production: Lessons Learnedhttps://threedots.tech/post/ai-agent-that-lies/Thu, 07 Aug 2025 00:00:00 +0200https://threedots.tech/post/ai-agent-that-lies/<p>The peak of hype isn&rsquo;t the best moment to reflect on AI. Will it take your job, or is it the next fad like NFTs? Are AI startups ridiculously overvalued, or are the companies that sleep on AI doomed?</p> <p>Time will tell. LLMs are <em>far</em> from perfect, but I&rsquo;m excited they&rsquo;re here anyway. Not because of a silly promise to make me 10x more productive, but because <strong>they can solve some problems that were previously unsolvable</strong>. If you enjoy building software like <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fabout">me and Robert</a>, it&rsquo;s a new tool worth trying. And there&rsquo;s a chance AI experience helps you create a better product or land a good job.</p> <p>Did you ever get stuck debugging your project at 2 AM, and went to sleep frustrated? We spent a few weeks adding an <em>AI Mentor</em> to our learning platform to help our students avoid this.</p> <p>As always, it wasn&rsquo;t as easy as most tutorials show. Trivial use cases, like a weather-checking assistant, are fun to make, but aren&rsquo;t much more useful than the default chat interface of any model.</p> <p>I&rsquo;ve heard before that <strong>implementing an AI feature is easy, but making it work correctly and reliably is the hard part</strong>. You can quickly build an impressive demo, but it&rsquo;ll be far from production grade.</p> <p>We can now confirm this is true. Plus, we&rsquo;ve seen something not talked about that much: <strong>how an AI feature can fail in production and mislead users</strong>.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1501" height="1071" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fmvp_hu98ed9b5603451ad8fa5b11bdd3143da2_106194_1501x1071_resize_q80_h2_lanczos_3.webp" alt="MVP" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fmvp_hu98ed9b5603451ad8fa5b11bdd3143da2_106194_1501x1071_resize_lanczos_3.png"" /> <p>I want to share what we learned in detail, hopefully saving you time on research. Let&rsquo;s set aside the world-changing stories for a moment and see <strong>what we can build with this stuff</strong>.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="if-youre-just-starting-out">If you&rsquo;re just starting out</h4> <p>I know many developers stay away from AI because they consider it overhyped, unethical, or useless. They also feel massive FOMO because there&rsquo;s currently no other topic in tech.</p> <p> </p> <p>If you feel this way, I&rsquo;m not trying to change your mind.</p> <p> </p> <p>I encourage you to approach it like an engineer trying out a new technology. Look for what problems it can solve, while most people are busy applying AI to everything.</p> <p> </p> <p>Once you use it in practice, you&rsquo;ll have a good idea where it&rsquo;s useful and where it falls short. You&rsquo;ll make up your mind much better this way than reading <em>hot takes</em> from CEOs and tech bros.</p> </p></div> </div> <h2 id="why-ai-mentor">Why AI Mentor?</h2> <p>We&rsquo;re not VC-backed and don&rsquo;t seek to sell our company, so there&rsquo;s little value in slapping <em>&ldquo;AI-First&rdquo;</em> on our homepage. <strong>We wanted to use LLMs to solve a real problem our users had.</strong></p> <p>Our Academy platform is all about learning by building real-life projects. One of our challenges is helping students finish the training. If you have ever tried an online course, you may know this problem — you put it off after a few days and never return, distracted by something else.</p> <p>One reason to put learning off is that you get stuck. While working on complete projects, it can be difficult to spot a bug. Example solutions don&rsquo;t always help, since each project is unique. We help students move forward by reviewing their solutions.</p> <p>Often, it&rsquo;s a small typo or mistake that&rsquo;s easy to catch with fresh eyes. <strong>There&rsquo;s no learning value in fighting with such bugs for hours.</strong> Frustrated and stuck, some people give up.</p> <p>Last year, we thought LLMs could help. You can feed them entire codebases and ask them to point out or even fix errors. And they are always available, in contrast to our limited time for customer support.</p> <p>Back then, the models were expensive, far less impressive (GPT-4 had just been released), and there were few sources to learn from. But most of all, we lacked time and postponed the project. We came back to it earlier this year.</p> <p>(If you want to see it live, the first few modules of <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F" target="_blank"><em>Go in One Evening</em></a> require no login.)</p> <h2 id="milestones">Milestones</h2> <p>We decided to start with an MVP and focus on the fundamentals, so we&rsquo;re not distracted by nice-to-haves. We picked a few milestones and planned to implement each one in production before moving on to the next one.</p> <ol> <li>A single &ldquo;Help me!&rdquo; button on the website for <em>Go in One Evening</em>. Users would receive a single message with a hint on what&rsquo;s wrong with their code.</li> <li>Conversations. While a chatbot wasn&rsquo;t our main goal, asking a follow-up question about the solution could be helpful.</li> <li>Enable the Mentor for <em>Go Event-Driven</em>.</li> </ol> <p><em>Go in One Evening</em> is a simpler training with shorter exercises. We assumed solving the issues found there would be easier compared to <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fevent-driven%2F" target="_blank"><em>Go Event-Driven</em></a>, where students build more complex projects. <strong>(The final project is about 4k lines of Go code.)</strong></p> <figure class="img-center" role="group" aria-describedby="caption-The mockup of our initial idea."> <img title="" loading="lazy" decoding="async" class="img " width="2944" height="1210" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fmockup_hu93775776af857315c83440d7de855867_212803_2944x1210_resize_q80_h2_lanczos_3.webp" alt="Mockup" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fmockup_hu93775776af857315c83440d7de855867_212803_2944x1210_resize_lanczos_3.png"" /> <figcaption id="caption-The mockup of our initial idea." class="caption-The-mockup-of-our-initial-idea"> The mockup of our initial idea. </figcaption> </figure> <figure class="img-center" role="group" aria-describedby="caption-The end result."> <img title="" loading="lazy" decoding="async" class="img " width="3308" height="1136" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fmentor_hu95bd51be7a6f09a95a66dde9afa96140_214675_3308x1136_resize_q80_h2_lanczos_3.webp" alt="mentor" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fmentor_hu95bd51be7a6f09a95a66dde9afa96140_214675_3308x1136_resize_lanczos_3.png"" /> <figcaption id="caption-The end result." class="caption-The-end-result"> The end result. </figcaption> </figure> <h2 id="the-help-me-button">The &ldquo;Help Me!&rdquo; Button</h2> <p>We created a proof of concept that should be easy to extend.</p> <p>We started with two HTTP endpoints:</p> <ul> <li>a <code>POST</code> for requesting help (for now with a constant &ldquo;Help me&rdquo; message)</li> <li>a <code>GET</code> streaming all messages over Server-Sent Events (SSE).</li> </ul> <p>In theory, a single endpoint could do both, but we wanted it to be <strong>asynchronous by default</strong>. Users can click the button and refresh the page without losing the response. Separating writes from reads is a good practice, especially for long-running tasks.</p> <p>The downside is a bit more complex setup, but we use proven tools that we know well. We prefer event-driven patterns for asynchronous architecture. We use <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill" target="_blank">Watermill</a> and Google Cloud Pub/Sub to trigger the Mentor and send updates back to the user.</p> <p>All of our events end up in BigQuery, so we get storage for conversations out of the box. This is important because we need the history of messages to include in the next prompts.</p> <figure class="img-center" role="group" aria-describedby="caption-High-level overview."> <img title="" loading="lazy" decoding="async" class="img " width="3594" height="1039" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fdiagram_hu5d3c468c31f6f8fa07cf15176c646613_244594_3594x1039_resize_q80_h2_lanczos_3.webp" alt="diagram" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fdiagram_hu5d3c468c31f6f8fa07cf15176c646613_244594_3594x1039_resize_lanczos_3.png"" /> <figcaption id="caption-High-level overview." class="caption-High-level-overview"> High-level overview. </figcaption> </figure> <p>This seemingly simple setup already has some challenges, even without considering the AI part:</p> <ul> <li>How to handle race conditions between the frontend app, the Pub/Sub, and the SSE handler?</li> <li>How to handle errors and retries?</li> <li>How to deal with BigQuery&rsquo;s insert and select latency?</li> <li>How to approach message ordering?</li> </ul> <h3 id="events">Events</h3> <p>Event-driven patterns turned out to be a good fit for working with LLMs. Behind the scenes, there are many moving parts, and most need no input from the user. Failures and retries often happen, but they are all hidden behind a single response.</p> <p>It&rsquo;s why we don&rsquo;t recommend using a single HTTP handler for the whole process. There are delays and interruptions, and you need to handle them, so your users don&rsquo;t see the mess.</p> <p>Most LLM APIs support streaming chunks of messages. You can show them on the UI before the model has finished generating the response. SSE is a perfect fit for this.</p> <p>The first response of the SSE handler is a list of all messages (for the given user and exercise). <strong>Then, we stream all chunks over the Pub/Sub to the SSE handler and then to the browser.</strong></p> <p>We had a great experience working with SSE before, and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-http" target="_blank">watermill-http</a> supports it out of the box, so it integrates nicely with our architecture. We&rsquo;ve improved the implementation by adding support for generics (although it&rsquo;s not public yet, we may release it at some point).</p> <p>It&rsquo;s all hidden behind a high-level interface. The HTTP handler receives a channel of incoming events and a channel to send responses back to the client.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Handler</span><span class="p">)</span> <span class="nf">HandleEventStream</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">events</span> <span class="o">&lt;-</span><span class="kd">chan</span> <span class="o">*</span><span class="nx">events</span><span class="p">.</span><span class="nx">MentorMessageSent</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">responses</span> <span class="kd">chan</span><span class="o">&lt;-</span> <span class="nx">any</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Send the initial response with all messages </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">messages</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nf">getAllMessages</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">msg</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">messages</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">responses</span> <span class="o">&lt;-</span> <span class="nx">msg</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Process incoming events </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">for</span> <span class="nx">event</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">events</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="nx">responses</span> <span class="o">&lt;-</span> <span class="nx">response</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If you want to learn more about Server-Sent Events, we have a dedicated post about implementing it in depth: <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2F">Live website updates with Go, SSE, and htmx</a>. </p></div> </div> <h2 id="calling-llms">Calling LLMs</h2> <p>We initially used <code>langchaingo</code> for calling the LLMs, but the API felt a bit bloated for our needs. We created a small wrapper on top of the official SDKs to be able to switch the models quickly.</p> <p>The design of most LLM APIs is very similar. You can describe a prompt call like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Model</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">Prompt</span><span class="p">(</span><span class="nx">history</span> <span class="p">[]</span><span class="nx">Message</span><span class="p">)</span> <span class="p">(</span><span class="nx">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>It&rsquo;s a function that accepts a chat history and returns the next message in the chat.</p> <p>In practice, we ended up with a few more details:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">l</span> <span class="o">*</span><span class="nx">LLM</span><span class="p">)</span> <span class="nf">Prompt</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">history</span> <span class="p">[]</span><span class="nx">Message</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">user</span> <span class="nx">User</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">opts</span> <span class="nx">PromptOpts</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Output</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">PromptOpts</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">MaxOutputTokens</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="nx">Model</span> <span class="nx">ModelName</span> </span></span><span class="line"><span class="cl"> <span class="nx">Tools</span> <span class="p">[]</span><span class="nx">ToolSchema</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">o</span> <span class="o">*</span><span class="nx">Output</span><span class="p">)</span> <span class="nf">Listen</span><span class="p">()</span> <span class="p">(</span><span class="o">&lt;-</span><span class="kd">chan</span> <span class="nx">Chunk</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">o</span> <span class="o">*</span><span class="nx">Output</span><span class="p">)</span> <span class="nf">WaitAndGetAll</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span></code></pre></div><p>This is a tiny abstraction over the LLM API, just enough to make it easy to switch between models.</p> <p><strong>Similarly to SSE, Go channels are the perfect API to stream chunks.</strong> It works well with the whole asynchronous model, and it&rsquo;s great to have support for it on the language level.</p> <h2 id="prompts--context-engineering">Prompts &amp; Context Engineering</h2> <p>We spent a lot of time iterating over the prompts and the context we fed to the model.</p> <p>For example, we wanted the Mentor to hint at the right direction, but not simply give the complete answer. We ended up with quite a long prompt for something as simple as &ldquo;Help with this exercise&rdquo;.</p> <p>We have many files related to each exercise (the user&rsquo;s solution, an example solution, and many others). Even though models now easily handle long contexts, we still need to be careful about how much we feed them.</p> <p><strong>Deciding what context to provide needs careful balance.</strong> If you dump too much and the context window grows, the prompts get more expensive, and the models are less likely to follow the instructions. If you provide too little, the model may not understand the problem and give a useless answer.</p> <p>There are really no rules that work for every case. One thing we found helpful is strong language in the prompt. The models seem to follow wording like &ldquo;ALWAYS&rdquo; or &ldquo;NEVER&rdquo; better than &ldquo;should&rdquo; or &ldquo;shouldn&rsquo;t&rdquo;.</p> <h2 id="free-form-chat">Free-form chat</h2> <p>After the initial response, users can ask about the solution or other things related to the exercise.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1836" height="1160" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fchat_hu953f2f7d4095385afd6a02abe612fd95_217266_1836x1160_resize_q80_h2_lanczos_3.webp" alt="chat" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fchat_hu953f2f7d4095385afd6a02abe612fd95_217266_1836x1160_resize_lanczos_3.png"" /> <p>This part was quite straightforward since we had already made the MVP ready to work with threads of messages. The key here is getting the historical messages to include in the future prompts.</p> <p>In our case, we fetch the chat history from BigQuery. We retrieve what we can concurrently, so the time to the first response chunk is short.</p> <h2 id="solving-complex-projects">Solving Complex Projects</h2> <p>This approach worked pretty well, even though we used a single prompt. For shorter code snippets, it could guide students on what&rsquo;s wrong with the solution.</p> <p>But for complex exercises, it failed miserably. <strong>With big projects, the model often made mistakes.</strong> It tried to fix issues that didn&rsquo;t exist or completely missed what was wrong.</p> <p>We improved it a bit with better prompts and more context about common concepts. But for the most complex examples, it wasn&rsquo;t enough. So we experimented with a more advanced approach, which you could consider an <em>agentic system</em>.</p> <h2 id="fixing-the-solution">Fixing the Solution</h2> <p>The tricky part of generating hints is that <strong>we can&rsquo;t be sure whether the model is right</strong>.</p> <p>It&rsquo;s how your AI feature can (and likely will) fail in production. To counter this, you need to be able to <strong>verify the model&rsquo;s output</strong>.</p> <p>In our case, we built a system similar to coding agents like Claude Code. We use it to fix the user&rsquo;s solution before it gives any hints. If it succeeds, we have some confidence that the model is on the right track.</p> <p>This approach solved the issue of the model making wrong assumptions. If it tried one approach, and it didn&rsquo;t work, it would realize the issue is elsewhere.</p> <p>Among other things, the agent can edit the user&rsquo;s submitted files. These changes happen only in the Mentor&rsquo;s context — the user doesn&rsquo;t see them. It also plans what to do, can summarize the changes, and a few other tricks. Different models excel at different tasks, so we can use the best one for each step.</p> <p>In the code, an agent is pretty much a <code>for</code> loop plus tool uses. It&rsquo;s a bit like a regular chatbot, except the model &ldquo;talks&rdquo; to itself. You keep the history and add messages with feedback on the environment. The context grows with each iteration, so it makes sense to limit the attempts.</p> <h3 id="parallel-runs">Parallel Runs</h3> <p>The new approach sometimes brilliantly succeeded. But other times it was stuck, failing to edit a file, or just not getting the original issue.</p> <p>The good part was that it could now tell the user it didn&rsquo;t know the solution, rather than making things up. That&rsquo;s a huge improvement — <strong>if someone is already stuck, we don&rsquo;t want to mislead them further.</strong> But it wasn&rsquo;t able to fix the solution on each attempt. You had to retry a few times if it failed.</p> <p>We noticed that if the model didn&rsquo;t succeed in the first few attempts, it would only get worse. It made more sense to start over.</p> <p>So we started <strong>running a few agents in parallel</strong>, each trying to fix the solution. Once one succeeds, we stop the rest.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="3608" height="1508" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fmodels_hua1f5fb93aba2813272628894820403c4_323407_3608x1508_resize_q80_h2_lanczos_3.webp" alt="models" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fmodels_hua1f5fb93aba2813272628894820403c4_323407_3608x1508_resize_lanczos_3.png"" /> <p>Managing concurrent code like this is a pretty common pattern in Go. We feel it&rsquo;s a great fit for AI engineering thanks to its concurrency model (compared to other languages, like Python).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">wg</span> <span class="o">:=</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="nx">wg</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="nx">workers</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="nx">workers</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="nx">wg</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">cfg</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">configsChan</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithTimeout</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">workerTimeout</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="nf">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">select</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="k">default</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">result</span> <span class="o">&lt;-</span> <span class="nf">Solve</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">mentorContext</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">wg</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nb">close</span><span class="p">(</span><span class="nx">result</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">select</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">():</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">ctx</span><span class="p">.</span><span class="nf">Err</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">r</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">result</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">ErrCantHelp</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">r</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">r</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>We use many combinations of models to increase the chances, hence the <code>configsChan</code>. Different models excel at different tasks, so we can use the best one for each step. Also, if a cheaper model manages to finish the task fast, we can save some money on the expensive ones.</p> <p>Often, a single call couldn&rsquo;t reliably figure out the solution. This <em>shotgun</em> approach improved the chances. <strong>The tradeoff is cost: we pay for all started parallel runs, even if we finish them early.</strong></p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/ai-agent-that-lies/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="ai;agents;go"> <input type="hidden" name="fields[blog_series]" value=""> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h2 id="qa-tests--evals">QA: Tests &amp; Evals</h2> <p>In many projects, testing is an afterthought. It turns out it&rsquo;s crucial when working with LLMs. And <strong>testing AI products can be a nightmare.</strong></p> <p>It doesn&rsquo;t seem that different at first. Calling a model is similar to using any other API, so you need to prepare test cases with inputs along with the expected outputs. But there are some caveats.</p> <p><strong>Challenge #1 is judging the responses.</strong> An optimistic scenario is when you make the model return a structured output, with a well-known list of valid values. Good old unit tests can work well if you have enough test cases.</p> <p>But if you let the model respond in free-form text, like in most chatbots, it quickly gets complex. You need an <em>evals system</em> where you judge if the output is correct. You could either use a human to do this or another LLM call.</p> <p>Whatever you choose, <strong>expect to read lots of data</strong>. You need a realistic set of inputs and outputs, and you need to review and label them.</p> <p>Once we kicked off the project, we spent a few days manually going through possible test cases based on production data. It seemed unproductive at first, but if we skipped this part, we would have no idea if what we built works. It also helped us understand the problems people actually face.</p> <p><strong>Challenge #2 is time.</strong> If you want to use the smartest or <em>thinking</em> models, a request can easily take tens of seconds. But even with faster models, running a few prompts in a sequence is far from unit test speed. <strong>This slow feedback loop is annoying to work with</strong>, especially if you tweak the prompt slightly and want to see the impact right away.</p> <p><strong>Challenge #3 is cost.</strong> The best models are also the most expensive. If your context grows (some of our evals have context with 100k+ tokens), each test run costs real money.</p> <p>Because of #2 and #3, <strong>forget about running a big set of evals in CI on every commit</strong> (unless you have too much VC money to burn). You can run some of them, or do it only once in a while. In our case, the trivial test cases (fast and cheap) are not that interesting, because even the less capable models solve them easily. The longest and the most expensive ones are what we care about the most.</p> <p>In regular tests, you can mock the expensive or slow resources. It makes little sense here, as you need to validate the same models you&rsquo;ll run in production.</p> <p>Since we run parallel workers for each mentor call, it also impacts the evals. While not a big difference in the production environment, it makes evals much more expensive. It was also easy to hit the API limits.</p> <p>So, in the evals, we run the workers sequentially, starting with the cheapest ones. It slows down the tests, but saves us some money (this makes a difference, especially for bigger contexts).</p> <p>On top of all this, LLMs are unpredictable, so you may want to run a few iterations of each eval for more realistic results. <strong>This multiplies both time and cost.</strong></p> <p><strong>You need to balance many factors.</strong></p> <p>We didn&rsquo;t use any framework, but a script written in Go that runs the evals and collects the results.</p> <p>Because evals take a long time to run and could be interrupted by some error, we implemented a cache and retry mechanism, similar to what <code>go test</code> does. It&rsquo;s frustrating to spend five minutes waiting for tests, only to realize you forgot a simple change and have to start over.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1962" height="640" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fevals_hudf4ca1d10670262ff59a2ab8d9097904_169025_1962x640_resize_q80_h2_lanczos_3.webp" alt="evals" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fevals_hudf4ca1d10670262ff59a2ab8d9097904_169025_1962x640_resize_lanczos_3.png"" /> <div class="code-ref"> A few of the evals we run for the Mentor. </div> <h2 id="failing-in-production">Failing in production</h2> <p>After rolling out the first version of the Mentor, we monitored the responses to see how it did. Shortly after, came this:</p> <figure class="img-center" role="group" aria-describedby="caption-Mentor hallucinating. This is wrong!"> <img title="" loading="lazy" decoding="async" class="img " width="1494" height="578" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fwrong_hufd949956e40af2caf7f618634af15c63_325013_1494x578_resize_q80_h2_lanczos_3.webp" alt="wrong" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fwrong_hufd949956e40af2caf7f618634af15c63_325013_1494x578_resize_lanczos_3.png"" /> <figcaption id="caption-Mentor hallucinating. This is wrong!" class="caption-Mentor-hallucinating-This-is-wrong"> Mentor hallucinating. This is wrong! </figcaption> </figure> <p>I started sweating when I saw this. People pay us to teach them, so it&rsquo;s a huge risk if they learn obviously wrong things.</p> <p>In this case, the student tried to understand if assigning a slice to another slice in Go results in a deep copy. The model happily agreed, even though <strong>it&rsquo;s not true</strong>.</p> <p>We take teaching people seriously. I almost wanted to scrape the entire feature when this happened. We contacted the student right away to clarify the issue. Thankfully, they appreciated the detailed explanation and weren&rsquo;t angry.</p> <p>To counter this, we switched to a better model for conversations and added more context to the prompts (details below).</p> <p>The worst part about hallucinations is that the model is very confident about being right. When you just start working with LLMs, you&rsquo;re surprised how often it happens. You need to be ready for it, because it WILL happen in production.</p> <p>This is why I&rsquo;m not comfortable learning something in-depth just from an LLM. If it&rsquo;s something you can quickly check, it&rsquo;s not a big deal. But learning fundamentals the wrong way is another story.</p> <h2 id="rag-and-sources">RAG and Sources</h2> <p>Initially, we assumed we didn&rsquo;t need to use RAG (Retrieval-Augmented Generation), and we simply always added some documents to the context.</p> <p>But after the first issues, we decided to implement a simple RAG with things like Go documentation, our blog posts, and other resources. We have a simple database of content in Markdown format.</p> <p>The RAG hype presented it as some mindblowing technology, but it was rather simple to implement. It took us just two days to have an MVP in production. It helped that we could split the work and implement two parts in parallel: building the database and querying it in the mentor code.</p> <p>As a bonus, we also link to the sources in the response, so students can read more about the topic.</p> <figure class="img-center" role="group" aria-describedby="caption-The list of useful links."> <img title="" loading="lazy" decoding="async" class="img " width="1922" height="348" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Frag_hu53b18d76d1750436a0389c3c8e80218a_92086_1922x348_resize_q80_h2_lanczos_3.webp" alt="rag" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Frag_hu53b18d76d1750436a0389c3c8e80218a_92086_1922x348_resize_lanczos_3.png"" /> <figcaption id="caption-The list of useful links." class="caption-The-list-of-useful-links"> The list of useful links. </figcaption> </figure> <h2 id="predictability">Predictability</h2> <p>If you expect a structured output from the model, like &ldquo;true&rdquo; or &ldquo;false&rdquo;, or a set of answers, testing is much easier. But dealing with free-form answers, like with most chatbots, is entirely different.</p> <p>One day, I started tweaking the <em>temperature</em> setting, assuming lower values should work well for our needs. Then, I realized there&rsquo;s no way to tell unless I compare multiple responses manually or write an eval that focuses on this difference. And even then, I&rsquo;m not sure if the reason was my change.</p> <p><strong>It&rsquo;s very difficult to get predictable results from LLMs.</strong> Small changes in the prompt have effects on the whole pipeline. The more complex the system, the less predictable the outcome, and the harder it is to test.</p> <p>After you change the prompt, you need to run the evals. But probably not all, since they are slow and expensive. But then, if you test only a few, you don&rsquo;t know if it covers all cases.</p> <p>If the response changes, you can&rsquo;t be sure it&rsquo;s due to your changes. Maybe it&rsquo;s just a different random response. Or maybe the model has been updated overnight (you have no visibility into it). To have a better idea, you could run more iterations of each eval, but this increases both time and costs.</p> <p><strong>The developer experience of changing the prompt and waiting for results is quite poor.</strong> It&rsquo;s nothing new: it&rsquo;s like working with a slow or flaky CI/CD pipeline, where you commit changes and can&rsquo;t spend the next five minutes on anything productive. The context switch is painful. You can counter this with parallel runs, but you can hit usage limits this way, and it&rsquo;s then easy to burn money within a few seconds.</p> <p>How you model your evals is key. We ended up with two kinds of evals:</p> <ul> <li><em>Strict</em> evals, where we expect a given percentage of scenarios to pass. We can use them reliably in the CI pipelines.</li> <li><em>Graded</em> evals, where we run a set of scenarios and manually compare how the results changed according to grading.</li> </ul> <h2 id="shifting-the-mental-model">Shifting the <em>Mental</em> Model</h2> <p><strong>How do you cope with software that doesn&rsquo;t follow instructions?</strong></p> <p>Often, you like the LLM&rsquo;s output 90% of the time, but the remaining answers are confusing or outright wrong. This can be frustrating if you&rsquo;re used to working with code.</p> <p>There are many ways to reduce hallucinations, but there&rsquo;s no way to avoid them completely. Realizing this helped me change how I think of LLMs.</p> <p>I see people go to two extremes:</p> <ul> <li>Treating LLMs as sentient beings, trusting the model to do exactly what they ask, if the prompt is good enough: &ldquo;Bro, I found a way to make GPT never lie to me, just use this prompt.&rdquo;</li> <li>Skeptical about using LLMs entirely, making fun of <em>prompt engineers</em> having to beg the model to do what they want.</li> </ul> <p>A middle ground I found is to <strong>treat an LLM as a function that is excellent at parsing vague inputs, and the output is often brilliant, but sometimes nonsense</strong>. I treat prompts as code, but with less strict rules than I was used to.</p> <p>With this mindset, it&rsquo;s easier to be pragmatic and remember not to blindly trust the output. But also not getting discouraged after something breaks.</p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Expect LLMs to fail</p> </div> <div class="notice-body"><p> <p>LLMs are impressive but unpredictable. Don&rsquo;t treat them like the code you&rsquo;re used to.</p> <p>Be ready for failures and hallucinations. If you can, verify the output.</p> </p></div> </div> <h2 id="agentic-systems-or-autonomous-agents">Agentic systems or autonomous agents?</h2> <p>The top 2025 trend is running autonomous agents that make decisions independently.</p> <p>But thinking of an agent as a sentient being that can figure out what to do in every scenario is a trap. <strong>Code always works in contrast to an unpredictable LLM.</strong></p> <p>We didn&rsquo;t need to go all in on the agent running all workflows by itself. For example, we can just decide (in code) what steps the coding agent should take next.</p> <p>When writing regular code, you need to control the agent loop more directly. It&rsquo;s tempting to think you can just include in the prompt what the agent should do in every scenario. But the reality is, you have no guarantee it will follow your instructions exactly.</p> <p>Our agent isn&rsquo;t super complex, but it doesn&rsquo;t need to be. If you can predict when to hand over to regular code or another LLM call, just do it in the code. No need for an agent framework where agents know about each other and can decide who to &ldquo;talk&rdquo; to.</p> <p>Especially if you can&rsquo;t verify the output, <strong>how can you trust the agent to make the right decisions?</strong></p> <p>This agentic hype feels like the early days of microservices when we told ourselves the story of independent entities talking to each other. Then everyone jumped to implement the solution, without considering whether it actually solved anything.</p> <p>Many teams learned the hard way that distributed systems are difficult to get right. Even with classic, perfectly predictable code. I&rsquo;m not eager to add unpredictable models to the mix.</p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Use LLMs where they shine</p> </div> <div class="notice-body"><p> <p>If you can handle the problem with regular code, there&rsquo;s no need to use LLMs.</p> <p>Don&rsquo;t think of agents as sentient beings that can figure everything out by themselves.</p> </p></div> </div> <h2 id="where-is-the-complexity">Where is the complexity?</h2> <p>The length of this post shows how much goes into one AI feature, and not all of this effort is related to LLMs. Most of it is good old software engineering and product design.</p> <p><strong>I will risk saying that building the Mentor was 80% regular software development and 20% AI-specific parts.</strong> And 80% of the latter was creating and running evals.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="3460" height="1346" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fai-engineering_hu51e549431edae178a912d18182e88532_955308_3460x1346_resize_q80_h2_lanczos_3.webp" alt="ai-engineering" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fai-engineering_hu51e549431edae178a912d18182e88532_955308_3460x1346_resize_lanczos_3.png"" /> <p><strong>Most of the complexity is in the orchestration.</strong> All well-known patterns and good practices still apply. This agent-orchestrating code is your domain logic.</p> <p><strong>Once again, Go shines for such use cases.</strong> It&rsquo;s trivial to run concurrent workflows, and channels are a great way to communicate between different parts of the system. The logic is easy to follow. (And <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F" target="_blank">Go is easy to learn!</a>)</p> <h2 id="models">Models</h2> <p>We accidentally picked a hot moment in model releases.</p> <p>We started with Gemini 2.0 Flash — it had a 1 million token context window, a generous free tier, and was relatively cheap overall. Then, Gemini 2.5 Pro came and made Flash look silly. It had a much better understanding of code. Then GPT-4.1-mini came and made Pro look expensive and slow.</p> <p>In retrospect, it&rsquo;s good we didn&rsquo;t implement this feature one year ago, since the models got so much better over time.</p> <p>Picking the right model is a balance between speed, cost, and quality. You need to be able to switch models easily for tests, so make sure your system is not tightly coupled to a specific model.</p> <p>We eventually landed on <a href="proxy.php?url=https%3A%2F%2Fopenrouter.ai" target="_blank">OpenRouter</a>. You can use the Go OpenAI SDK to call any provider&rsquo;s model, which is great by itself. It also has no strict rate limits (we hit limits running evals on OpenAI). Finally, it centralizes billing and lets you set limits and budgets (something unheard of for Gemini — crazy!). The downside is some extra cost and latency.</p> <h2 id="limits--costs">Limits &amp; Costs</h2> <p>One feature of our internal LLM library is limits.</p> <p>We designed it so that <strong>calling an LLM is not possible without using the limits.</strong> Even tests track limits in-memory.</p> <p><strong>It&rsquo;s too easy to lose track of how many tokens you use.</strong> One day, we spent $100 running evals. After that, we made the cost clearly visible in all of our tooling, including tests running in the CI.</p> <figure class="img-center" role="group" aria-describedby="caption-A cost spike because of running evals."> <img title="" loading="lazy" decoding="async" class="img " width="1328" height="522" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fgcp_hu0740c4a1810ab27cb27c539f86c833f2_21562_1328x522_resize_q80_h2_lanczos_3.webp" alt="gcp" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fgcp_hu0740c4a1810ab27cb27c539f86c833f2_21562_1328x522_resize_lanczos_3.png"" /> <figcaption id="caption-A cost spike because of running evals." class="caption-A-cost-spike-because-of-running-evals"> A cost spike because of running evals. </figcaption> </figure> <p>Gemini is an especially awful experience. There&rsquo;s no way to set budget limits, and the usage shows up with a delay. Thankfully, other providers offer basic services like budgets or prepaid payments. As mentioned above, OpenRouter also works great for this.</p> <p>We can set limits on a few different levels. The primary one is per user. It&rsquo;s not a perfect solution, since more complex projects can quickly exceed the limit. It&rsquo;s something we will try to improve in the future.</p> <p>The costs of running in production so far are just a tiny fraction of our infrastructure costs. The surprisingly expensive part has been running the evals. The faster you want them to run, the easier it is to burn money quickly. Thankfully, this happens only once in a while when we change the prompts or add new evals.</p> <h2 id="observability--tooling">Observability &amp; Tooling</h2> <p>As in any complex system, debugging AI features is difficult. Good tooling helps.</p> <p><strong>Tracing is a great way to understand how the system works.</strong> We didn&rsquo;t use anything AI-specific for tracing, as we like simple tech stacks and proven technology. We already use tracing in our project, so it was just a matter of adding spans and propagating them.</p> <figure class="" role="group" aria-describedby="caption-A cool thing about tracing is how it shows which parts run concurrently."> <img title="" loading="lazy" decoding="async" class="img " width="2468" height="1012" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Ftracing_hu978b15a390a34ef0b9a070a872c31e4b_320952_2468x1012_resize_q80_h2_lanczos_3.webp" alt=" tracing" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Ftracing_hu978b15a390a34ef0b9a070a872c31e4b_320952_2468x1012_resize_lanczos_3.png"" /> <figcaption id="caption-A cool thing about tracing is how it shows which parts run concurrently." class="caption-A-cool-thing-about-tracing-is-how-it-shows-which-parts-run-concurrently"> A cool thing about tracing is how it shows which parts run concurrently. </figcaption> </figure> <p>We also created a back-office dashboard to track the cost and usage, and a set of CLI tools to easily manage evals.</p> <h2 id="moderator">Moderator</h2> <p>We wanted the Mentor to avoid unrelated topics, as it was more likely to give incorrect answers then. We set some guardrails in the prompts, but it was easy to work around them.</p> <p>Our second approach was a separate <em>moderator</em> prompt.</p> <p>It has two functions: filter out irrelevant topics and decide if the user needs help with the exercise or just asks a question. If it&rsquo;s just a question, we don&rsquo;t need to fix their solution, so it&rsquo;s a much simpler and faster prompt.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="3610" height="1414" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fmoderator_hu72e8c4b3d7290baa00b2aec8bc090e43_287627_3610x1414_resize_q80_h2_lanczos_3.webp" alt="moderator" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fmoderator_hu72e8c4b3d7290baa00b2aec8bc090e43_287627_3610x1414_resize_lanczos_3.png"" /> <p>It&rsquo;s a classic example of how asking the model to do too much at once is problematic. Similar to classic software, the separation of concerns is a good idea. One prompt should focus on one task.</p> <figure class="img-center" role="group" aria-describedby="caption-Moderator stopping the conversation early."> <img title="" loading="lazy" decoding="async" class="img " width="1360" height="306" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fpizza_hu34c1071ce02bb7fec192c48481c7bfd4_53759_1360x306_resize_q80_h2_lanczos_3.webp" alt="pizza" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fpizza_hu34c1071ce02bb7fec192c48481c7bfd4_53759_1360x306_resize_lanczos_3.png"" /> <figcaption id="caption-Moderator stopping the conversation early." class="caption-Moderator-stopping-the-conversation-early"> Moderator stopping the conversation early. </figcaption> </figure> <h2 id="encouraging-students-to-ask-for-help">Encouraging students to ask for help</h2> <p>Implementing the Mentor was one thing, but it wouldn&rsquo;t be effective if students didn&rsquo;t know it existed.</p> <p>People are already fed up with companies shoving AI features their way, and since most chatbots are dumb, it&rsquo;s not tempting to click the button.</p> <p>We track the failures and show small hints in the CLI. Plus, we use the same SSE endpoint to stream hints and show an indicator on the website.</p> <p>Sometimes, students still ask us directly for help. We don&rsquo;t want to point them straight to the Mentor, as our support is part of the product. Now, we can use the Mentor ourselves to find what the issue is, and then we just need to explain it to the student. Finding a small typo is much easier this way.</p> <h2 id="the-ui">The UI</h2> <p>We&rsquo;re not frontend experts, so I will shortly mention the UI parts for completeness.</p> <p>We use React and have <em>vibe-coded</em> most of the Mentor&rsquo;s frontend code. We&rsquo;ve been working with frontend for many years, but it&rsquo;s not something we enjoy. Generating the code was a great way to start: we understand enough to know what we want, but we don&rsquo;t have to write it ourselves.</p> <p>We find AI-assisted coding works well for software that has no &ldquo;strict behavior&rdquo;, and you can quickly tell if it works, like UI and video games. For example, if the generated CSS is silly, but looks fine, it&rsquo;s not a big deal. Especially if it&rsquo;s just a single component. Of course, if you go too far with it, it can turn your project into a mess.</p> <p>We use a polyfill for SSE. It supports features not in the official spec, like passing custom headers in the API (<code>Authentication</code>), and it works out of the box.</p> <p>One thing to consider with a chat interface is that you need to animate the chunks when streaming (delay the letters slightly). If you make them appear as they come, it doesn&rsquo;t look as good.</p> <p>At some point, we added status messages to show that the Mentor is working on the response. Sometimes it takes a few minutes, so it&rsquo;s easy to lose patience and think it stopped working. These messages are not fake — we stream them from the backend via the same SSE endpoint.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="926" height="350" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fai-agent-that-lies%2Fimages%2Fthinking_hu0261b3dc914fd8ba0ccd9e90a41c3472_63986_926x350_resize_q80_h2_lanczos_3.webp" alt="thinking" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fai-agent-that-lies%5C%2Fimages%5C%2Fthinking_hu0261b3dc914fd8ba0ccd9e90a41c3472_63986_926x350_resize_lanczos_3.png"" /> <h2 id="outcomes">Outcomes</h2> <p>We ended up with something good enough to make it public and observe what happens. We see people use it, and most of the time, the response does the job.</p> <p>What we haven&rsquo;t tried yet:</p> <ul> <li>Running an open-source model on our own.</li> <li>Fine-tuning models.</li> <li>Judging the responses by another LLM on the fly.</li> <li>Pre-computing hints before someone asks for help.</li> <li>Partially reading files instead of dumping the whole codebase to the context.</li> </ul> <p>Even without these, we feel the result is pretty impressive compared to most chatbots.</p> <p><strong>The key lessons learned for us</strong> (hand-crafted by a human so you don&rsquo;t need to paste this post into an LLM):</p> <ul> <li>LLMs are not magic, but they can solve some problems that were previously unsolvable.</li> <li>Building an AI feature is easy, but making it work reliably is a whole different story.</li> <li>The complexity is in the orchestration, and the old software engineering practices still apply.</li> <li>You don&rsquo;t need a framework to get started. A tiny wrapper around the LLM API is enough.</li> </ul> <p><strong>You need to watch out for:</strong></p> <ul> <li>The hype stories don&rsquo;t mention the full picture. Try things yourself!</li> <li>LLMs WILL hallucinate in production, be ready to handle it.</li> <li>Testing AI systems (evals) is difficult to get right and can be expensive.</li> <li>It&rsquo;s easy to go over budget. Implement limits early.</li> </ul> <p>If you want to see the Mentor live, you can try the free part of <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F" target="_blank">Go in One Evening</a> (no login required).</p>AMA #1: Clean Architecture, Learning, Event-Driven, Gohttps://threedots.tech/episode/ama-1/Wed, 02 Jul 2025 16:00:00 +0000https://threedots.tech/episode/ama-1/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Go in the AI era:</strong> Go is excellent for AI applications, as the built-in concurrency makes orchestrating parallel API calls much easier than languages like Python</li> <li><strong>Architecture philosophy:</strong> Clean Architecture isn&rsquo;t always necessary. Start simple and add layers only when you feel the pain of complexity, not because someone said you should</li> <li><strong>Career transitions:</strong> Switching roles within tech is easier internally. Moving from sysadmin to developer works better within the same company where people already trust you</li> <li><strong>Go design patterns:</strong> Small interfaces near usage is the Go way. Duck typing allows you to define interfaces where they&rsquo;re used rather than in separate layers</li> <li><strong>Distributed systems:</strong> Async communication often solves sync timeout issues. When dealing with chains of service calls, consider using messages instead of increasing timeouts</li> </ul> <h2 id="introduction">Introduction</h2> <p>In this special 10th episode, we answer community questions in our first AMA format before taking a summer break.</p> <p>We discuss Go&rsquo;s role in AI development, Clean Architecture implementation, career transitions in tech, and distributed system timeouts.</p> <p>After the break, we&rsquo;ll switch to pre-recorded episodes with improved production quality. We still plan running some live episodes like this one, so stay tuned!</p> <h2 id="show-notes">Show Notes</h2> <ul> <li>The new <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-ddd%2F" target="_blank">Domain-Driven Design in Go training</a> we announced</li> <li>Team Topologies book mentioned for organizational architecture</li> <li><a href="proxy.php?url=https%3A%2F%2Fdiscord.gg%2FbTuDhyjE" target="_blank">Join our Discord community</a> for more technical discussions — we&rsquo;re happy to help!</li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>Architecture sounds like something complex that architects do. But it&rsquo;s basically stuff like &ldquo;if you have 10 services connected with synchronous calls, what happens with timeouts?&rdquo;</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>Five days of coding can save one day of planning. And it&rsquo;s often the case. And even if it&rsquo;s not visible on the first sight, it&rsquo;s often visible during review that is going for ages.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>I really like this idea of fixing something once you feel the pain. If something is not painful maybe it&rsquo;s fine even if it&rsquo;s not the best. Maybe it could be refactored, maybe it could be split to modules or whatever but if it&rsquo;s not painful you probably have more stuff in the backlog to take care of.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>In many cases we hired people that didn&rsquo;t write a line of code in Go earlier. It wasn&rsquo;t an issue in the end, because those people were productive very, very quickly.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>Often people are like, I am just a developer, give me the requirements and I will transfer them into code. So yeah that&rsquo;s not really helpful for most companies, because they need people who can understand the business domain, can help people figure out what the product needs.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>Go is also a great language to be generated, thanks to the syntax that is simple, But it&rsquo;s also very strict. So, for example, if you have some agent that is generating your code, it&rsquo;s easy to verify that.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DBSQRv_atYOw%26amp%3Bt%3D0s">00:00:00 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DBSQRv_atYOw%26amp%3Bt%3D212s">00:03:32 - Go in the AI world</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DBSQRv_atYOw%26amp%3Bt%3D900s">00:15:00 - TDD in Go</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DBSQRv_atYOw%26amp%3Bt%3D1424s">00:23:44 - Career switching</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DBSQRv_atYOw%26amp%3Bt%3D2315s">00:38:35 - Clean Architecture interfaces</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DBSQRv_atYOw%26amp%3Bt%3D2530s">00:42:10 - Distributed system timeouts</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DBSQRv_atYOw%26amp%3Bt%3D3211s">00:53:31 - Domain vs application logic</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DBSQRv_atYOw%26amp%3Bt%3D3849s">01:04:09 - Small teams architecture</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DBSQRv_atYOw%26amp%3Bt%3D4050s">01:07:30 - Under-engineering risks</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DBSQRv_atYOw%26amp%3Bt%3D4572s">01:16:12 - Future plans</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Robert</strong> [0:00]: All right, so welcome in our first AMA episode.</p> <p><strong>Miłosz</strong> [0:04]: Hello everyone.</p> <p><strong>Robert</strong> [0:05]: Last episode before short summer break. So we have a couple of questions to go over today. And just as a reminder, so if you have any extra questions, just feel free to leave it on the chat. We&rsquo;ll also be happy to answer that.</p> <p><strong>Miłosz</strong> [0:21]: This episode is a bit special because this is our 10th one and we will end the live format this way.</p> <p><strong>Robert</strong> [0:29]: Or at least partially. I mean, I think if you like this AMA format, there is a chance that we may have more episodes like that. So we&rsquo;ll ask real questions and we&rsquo;ll run it live. But in general, it will be no longer a live podcast, it will be a podcast. So we&rsquo;ll basically pre-record most of the episodes and you&rsquo;ll be able still to watch them on YouTube, so it will not change. And yeah i think it should improve the sound quality the video quality because well it&rsquo;s not the simplest way to make it live and to make it sound great because it&rsquo;s live so it cannot i mean it&rsquo;s easier to tweak sound after recording obviously it&rsquo;s a bit harder during the recording the second thing is obviously video uh the third thing is that a bit less obvious that is that we We don&rsquo;t have our own room to record, so sometimes we need to improvise a bit to record in different places. Like today, so when you&rsquo;re longer with us, you might notice that we&rsquo;re in a bit different background. This real background, so&hellip;</p> <p><strong>Miłosz</strong> [1:39]: I hope the next episode should be much more professional,</p> <p><strong>Robert</strong> [1:43]: Let&rsquo;s say. But you will see. It&rsquo;s a surprise after vacation, so after we&rsquo;ll be back, I guess, in September.</p> <p><strong>Miłosz</strong> [1:50]: Probably, yeah.</p> <p><strong>Robert</strong> [1:51]: Yeah, yeah. So this is the thing. So yeah. Improve and yeah it will be also a bit i think um content quality should be also higher because when we are recording it live you know it&rsquo;s also a bit challenging to go with the flow and try to avoid a lot of pauses and try to maintain the flow obviously when it&rsquo;s not live it&rsquo;s much much easier because we can pause we can just cut it later we can think about topics so but yeah i think it was It&rsquo;s a nice experiment to try with the live format and, again, it&rsquo;s not like we&rsquo;re dropping that. It&rsquo;s nice that we have already working setup for that. We have the space, but it&rsquo;s just a bit changing of the bars.</p> <p><strong>Miłosz</strong> [2:33]: We also received a lot of feedback. It would be useful to have some diagrams, some visual aid to what we are discussing. So maybe we&rsquo;ll also try something like this in the future. So it&rsquo;s not, you know, so you have a video with some helpers or something more interesting like coding, not only talking.</p> <p><strong>Robert</strong> [2:53]: Yeah, but no worries. So if you are listening for that in the podcast applications, also we don&rsquo;t want to make it hard to listen, just listen. So, you know, sound is always the highest priority here. What else? Yes, I think that&rsquo;s all about the format and I think we should be able to go to the questions.</p> <p><strong>Robert</strong> [3:16]: Or maybe you have something to add before.</p> <p><strong>Miłosz</strong> [3:18]: Yeah, if you have any questions, leave them in the chat so we can pick them up.</p> <p><strong>Robert</strong> [3:29]: All right so questions so the first question is what do you think the future holds golang developers in ai forward world so i think we can have i actually have pretty a lot to think to say about that because we have the mentor functionality in our online trainings and from what we see how people are using that it&rsquo;s super useful and especially compared to some other ai solutions because i think it will be not only my experience that very often when you see some ai functionality it&rsquo;s just unpolished i mean you can have impression that somebody just get the api of open api of open ai or cloud or whatever just put some empty simple prompt and that&rsquo;s it or like for example if you have iphone i mean it&rsquo;s not the best implementation in the world with everything and the AI implementation.</p> <p><strong>Miłosz</strong> [4:24]: As a user, it&rsquo;s hard to get right. You can guarantee the answers.</p> <p><strong>Robert</strong> [4:31]: I think it&rsquo;s also easy to have something that works, but it&rsquo;s much, much harder to have something that helps. But I think it&rsquo;s a bit off topic. The question is more about Golang developers in the AI of our world. So what do you think about how Golang fits with this AI world?</p> <p><strong>Miłosz</strong> [4:50]: I would expect most developers to use APIs, just call the RLMs over an HTTP API. And something that probably everyone does in Go anyway. So technically it&rsquo;s not that different. And if you write more agentic systems, like we did with Mentor, The hard part probably is the orchestration of it all. So there&rsquo;s a lot of loops and parallel function calls. And I think Go is actually great here. We&rsquo;ve seen it work well when we designed the mentor feature, because goroutines give you very easy-to-use tools to create parallel requests.</p> <p><strong>Robert</strong> [5:47]: And to cooperate with Python, so I think we are probably pretty good people to talk about that, because we professionally work with Python for some time, and doing this parallelization in Python is hell, in a very big simplification. And Go is basically created for that. So running something in the background is just a matter of using the Go keyword. And it&rsquo;s simplification obviously, but compared to Python, it&rsquo;s like deadly easy.</p> <p><strong>Miłosz</strong> [6:20]: Other than that, you will use probably the same tools you use now for development with LLMs, just calling them, and you know, it won&rsquo;t be that much different.</p> <p><strong>Robert</strong> [6:35]: But I think this parallelization, this is the key, because when you&rsquo;re building some agentic flow, usually you&rsquo;re doing some operations that are slow. They are not CTO-bound operations, but they are still calling some API, they are doing some operations, and just doing it synchronously, it&rsquo;s super, super slow. And this is why I think Go is just perfect language. I think it&rsquo;s not only my opinion that Go is a perfect language for building AI or LLM or Gen AI applications.</p> <p><strong>Miłosz</strong> [7:09]: Because it works. We will have a separate post on our blog soon. It writes up in detail how I do it. In short, there are a couple of complex parts, right, working with AI. One is prompt engineering. The hardest probably for me was writing evals, so testing the AI code. It&rsquo;s super difficult. And the last one probably is orchestration of it all, which is the same code you are used to writing. It&rsquo;s kind of like business logic for your AI agent.</p> <p><strong>Robert</strong> [7:46]: The only difference is that the result that you expect is not that strict. As you mentioned, the problem is that if you have written some prompt, it will maybe work in 90% of cases, but maybe just 10% is the hardest when it&rsquo;s doing some bad things that totally unexpected, and you need to spend a lot of time to convince it that, no, no, no, it&rsquo;s not about what&hellip; This is not Go-specific, but Go, I think, is helpful for that because, for example, the Go test package, it works pretty nice with that. So you can do some simple wrappers and write evals for that. So that&rsquo;s pretty cool.</p> <p><strong>Miłosz</strong> [8:28]: In general, expressing business logic in Go is quite easy, or at least works for us well. Because we have this explicit approach. You have no magic in place. You can just re-read code and you know what it does. So it helps when you define Domain objects, business logic and such, and similarly, when you write this agent logic, similar idea. So it&rsquo;s much easier this way.</p> <p><strong>Robert</strong> [8:56]: I think it&rsquo;s one important thing to mention here that if you are talking about, writing some Gen.AI applications, it&rsquo;s not data science. I mean, because often when somebody is talking about machine learning, AI, Python is mentioned, but Python is great for data engineering, for data science, but using general AI, I think it&rsquo;s pretty far away from data science. It&rsquo;s more backend at the end. I mean, you&rsquo;re doing some API, you obviously additionally have prompt engineering, but I mean, it&rsquo;s big simplification, but it&rsquo;s very, very close to backend in general. And a lot of good practices that you know from backend systems are applying one-to-one.</p> <p><strong>Miłosz</strong> [9:46]: You don&rsquo;t need much background about machine learning or RLMS in general to start.</p> <p><strong>Robert</strong> [9:52]: I think probably a lot of people can compare it to driving a car. It doesn&rsquo;t need to be mechanic to drive a car. Maybe if you would like to go to F1, maybe you need to have some mechanics background to understand very special details of the car, but it&rsquo;s just or top level.</p> <p><strong>Miłosz</strong> [10:14]: Can we comment on the audio? It&rsquo;s not clear. Pull your mic closer. Or even maybe using some specialized tools of API.</p> <p><strong>Miłosz</strong> [10:29]: It&rsquo;s basically the same idea.</p> <p><strong>Robert</strong> [10:38]: Okay, so we mentioned Go, how it&rsquo;s compatible with AI for our world. I think it&rsquo;s one more thing, because AI is very wide, ARIA, let&rsquo;s say. So we have building some JN AI applications. We have also Vibe coding. We have generating some code on production. I think it&rsquo;s also very interesting, we will think about Go, because Go is also a great language to be generated, thanks to the syntax that is simple. but it&rsquo;s also very strict. So, for example, if you have some agent that is generating your code, it&rsquo;s easy to verify that. There are no many options actually how it can be generated. Like other languages, you can do things in 10 ways. That&rsquo;s making generation a bit harder and it&rsquo;s also harder to get the syntax right. But in Go, thanks to having it pretty simple, I think I also seen even some studies based on that and covering that.</p> <p><strong>Miłosz</strong> [11:36]: You&rsquo;ve got types also in compiling, which is a big plus compared to dynamic languages.</p> <p><strong>Robert</strong> [11:43]: Yeah, yeah. So, and I think it&rsquo;s also out loud that, okay, you can probably easily generate JavaScript that will compile, let&rsquo;s say, but it doesn&rsquo;t mean that it will work properly. And for example, with Go, this is the language that for the first time I have the ceiling that, okay, I&rsquo;ve written something, it&rsquo;s compiled, and it works. With languages that are script languages, it&rsquo;s not that obvious, because it can execute, but at some point it can show exception that, okay, it doesn&rsquo;t make sense, sorry.</p> <p><strong>Miłosz</strong> [12:15]: Yeah, you&rsquo;re relying far more on testing.</p> <p><strong>Robert</strong> [12:18]: Yeah.</p> <p><strong>Miłosz</strong> [12:20]: That&rsquo;s why I remember working with Python. We need far more assertions about what, for example, what some method returns, which is just not an issue with strongly typed languages, which is, no, it&rsquo;s like nothing new, but if you generate the code, the less time you need to spend reading it, the better probably. So you don&rsquo;t need to analyze, you don&rsquo;t need to test such as edge cases, because the compiler does it for you. That&rsquo;s a B+.</p> <p><strong>Robert</strong> [12:52]: Yeah, so Tldr, in our opinion, and not only our Go is great for that. Oh, and maybe one last point about that, it&rsquo;s performance, because also some people might say, okay, Python is slow, but I know maybe Rust will be faster. But remember, so what&rsquo;s the battle like in your Gen.AI applications? It&rsquo;s not CPU. It&rsquo;s usually I.O. call done to some external server, so basically it&rsquo;s doing some network calls. And for that, actually not CPU matters that much. It&rsquo;s more about goroutines. And Go is I think even more efficient than Rust in running goroutines because it&rsquo;s not using OS, I mean, it&rsquo;s using operating system threads, but it has also its runtime.</p> <p><strong>Miłosz</strong> [13:38]: I guess you can do the same lightweight threads or coroutines in Rust but with probably not such a built-in API. But it&rsquo;s also something that many people overlook. They focus too much on the performance. I&rsquo;ve seen some AI frameworks that claim that they are the fastest framework in the market. And as I said, it makes zero sense because you will wait a couple of seconds for the alarm to respond anyway. So if you optimize nanoseconds or milliseconds of your framework, virtually no gain. It&rsquo;s similar to how people tend to benchmark Go routers for HTTP, which apart from extreme cases is not that important. If you if you talk about network and you use JSON Marshalling and you will be kind of slow but good enough anyway so the nanoseconds don&rsquo;t matter that much yep ah great</p> <p><strong>Robert</strong> [14:57]: Okay so the next question uh so the next question that you received is about how software development looks in go is it just a driven development uh so tdd yeah so it&rsquo;s interesting question so i would be interested to hear more about the background but probably will not know but yeah, I think it depends a lot and it&rsquo;s not like everybody does TDD in Go. I would even say that probably more people could do that.</p> <p><strong>Miłosz</strong> [15:34]: Yeah, I think TDD is very popular in Go, but probably similar to other high-level languages. I don&rsquo;t think there&rsquo;s a big difference there. It&rsquo;s just TDD is not, no, it&rsquo;s language agnostic, so it doesn&rsquo;t matter that much.</p> <p><strong>Robert</strong> [15:54]: And probably compiler adds one extra step because you need to make this code compiled at least, but it&rsquo;s a different level.</p> <p><strong>Miłosz</strong> [16:04]: Do you use TDD?</p> <p><strong>Robert</strong> [16:06]: It depends. So yeah, I think it depends on what kind of code I write. So for code that I know how it will behave, and it&rsquo;s kind of clearly defined what I expect for that, it makes sense, because I can start with tests earlier. One nice thing about TDD is also that it&rsquo;s helping to make nicer interfaces of the things, because, okay, you know what&rsquo;s the expected interface, and you&rsquo;re writing code for that.</p> <p><strong>Miłosz</strong> [16:40]: Or designing the API. Yeah, a nice tip.</p> <p><strong>Robert</strong> [16:44]: But, for example, when I am experimenting, or I don&rsquo;t know what I would like to achieve, or I know it may be a bit controversial, but I know that for this functionality I don&rsquo;t need tests, because it&rsquo;s just obvious enough, and it will not add enough value. It also doesn&rsquo;t make sense.</p> <p><strong>Miłosz</strong> [17:06]: I actually find it helpful when working with AI to generate code. Sometimes I will use AI to generate something like, you know, a very specialized algorithm. For example, the other day I had to process a file to, I don&rsquo;t remember, remove some lines or whatever in a very weird way, and this actually is pretty cool that you can define the function interface, write the tests for it, which are very simple because you have just input and output, then use AI to generate the body of the function.</p> <p><strong>Robert</strong> [17:43]: And AI has nice specification basically.</p> <p><strong>Miłosz</strong> [17:46]: Yeah, you can even give it the tests. And what I like about it is that if it&rsquo;s a small function and I can define the tests well, I don&rsquo;t really need to care if it&rsquo;s cover or corner cases. Basically, I don&rsquo;t need to care how the code works. I can treat it like an external library when you use an external library you don&rsquo;t read all of it to make sure it&rsquo;s nice and I don&rsquo;t know,</p> <p><strong>Robert</strong> [18:15]: More optimal sometimes better to not know yeah.</p> <p><strong>Miłosz</strong> [18:20]: Exactly and so this is similar so this is kind of TDD, I would say because I focus more on the tests and I can write down first and then generate it and now I can just take</p> <p><strong>Robert</strong> [18:33]: AI TDD.</p> <p><strong>Miłosz</strong> [18:37]: I can take a look on the generated code, but I&rsquo;m like, okay, if it works, I&rsquo;m fine. Whatever.</p> <p><strong>Robert</strong> [18:44]: I think also to make it super clear, so there are multiple, let&rsquo;s say, classes of tiers of code. So it&rsquo;s some component critical code. Probably this approach may be not best, but sometimes you have code that you don&rsquo;t care that much. It&rsquo;s some simple functionality.</p> <p><strong>Miłosz</strong> [19:01]: You don&rsquo;t want to maintain over time.</p> <p><strong>Robert</strong> [19:03]: Yeah, maybe you&rsquo;re just writing some ugly tool to do something once. And I would say that even just writing tests for that, it&rsquo;s a lot.</p> <p><strong>Miłosz</strong> [19:15]: For me, it&rsquo;s like algorithms in general. Like when you have to parse something, when you go line by line and save some results over time. I had a couple of examples like this recently. and this works pretty well because you can do it but it will take probably about an hour or an hour yeah unless you like doing this it&rsquo;s</p> <p><strong>Robert</strong> [19:37]: Also like a cool it&rsquo;s a strange hobby but I know that people like that.</p> <p><strong>Miłosz</strong> [19:42]: It&rsquo;s like a cool puzzle to solve although people complain about hacker rank exercises so maybe not everyone likes that but yeah it&rsquo;s But coming back to the question about what is the software development, how does software development look like in Go? I would say it&rsquo;s like any other high-level language probably. There&rsquo;s no specialized methods.</p> <p><strong>Robert</strong> [20:16]: But also, from my experience, it&rsquo;s also pretty, let&rsquo;s say, opinion, but I have also some feeling that compared to some other languages, in general, some library code quality is a bit higher than in other languages. So maybe it&rsquo;s also thanks to the language, to how strict it is, but this is my feeling that I have sometimes also. I don&rsquo;t know, maybe it&rsquo;s subjective, but it&rsquo;s something in that.</p> <p><strong>Miłosz</strong> [20:50]: Yeah, perhaps.</p> <p><strong>Robert</strong> [20:53]: Probably it&rsquo;s not related to this class-driven development.</p> <p><strong>Miłosz</strong> [20:56]: There are some approaches, but I&rsquo;m not sure if I would count them as a methodology, compared to the TDD. Like you know avoiding frameworks could be one example but it&rsquo;s more like what the community likes to do</p> <p><strong>Robert</strong> [21:11]: Not.</p> <p><strong>Miłosz</strong> [21:12]: Really a methodology</p> <p><strong>Robert</strong> [21:14]: But it&rsquo;s also coming from the language design yeah it&rsquo;s I mean language is designed in a way that it&rsquo;s, good that you don&rsquo;t need to go into frameworks because you have some interfaces already defined that you can integrate with so it&rsquo;s a nice thing It&rsquo;s some special thing about Go, but it&rsquo;s not a big deal for others. One good thing. I guess that the person that asked this question may not&hellip; The person mentioned that this person is learning Go, but also keep in mind that Go has one big nice advantage. You can learn Go very quickly and be productive pretty quickly. So when, for example, we had our teams, in many cases we hired people that didn&rsquo;t write a line of code in Go earlier.</p> <p><strong>Miłosz</strong> [22:05]: Because it was difficult back then to find people like that.</p> <p><strong>Robert</strong> [22:09]: Yeah, but from the other side, it wasn&rsquo;t an issue at the end, because those people were productive very, very quickly. And it&rsquo;s not like there is some magical technique that you need to learn to use Go. It&rsquo;s basically, you can learn the basics of the language and use the principles that you use in many other languages.</p> <p><strong>Miłosz</strong> [22:33]: Well, the risk here is that if you hire a team with people from different backgrounds, we have a diverse team, which is nice, but also people might start writing Go, like they used to write Python or Java or whatever. So they need to be kind of aligned. So we spent some time to help them learn.</p> <p><strong>Robert</strong> [22:58]: And I think this is an argument that we heard very often, that in terms of complex codebases that people were, copying some patterns blindly from Java or from C Sharp, but at the end it was very complex. So yeah, it&rsquo;s probably something to watch out.</p> <p><strong>Miłosz</strong> [23:17]: Yeah, it&rsquo;s a classic discussion around clean architecture. Is it still Go or is it Java? you should start doing it.</p> <p><strong>Robert</strong> [23:29]: Stop doing Java from Go.</p> <p><strong>Miłosz</strong> [23:35]: Well, I guess you can apply it to any language, really.</p> <p><strong>Robert</strong> [23:39]: That&rsquo;s true.</p> <p><strong>Miłosz</strong> [23:41]: Go too far.</p> <p><strong>Robert</strong> [23:44]: That&rsquo;s true. Okay, so another interesting question that we had was about switching a role. So the person was a sysadmin, and the question is actually how to&hellip; So we can probably generalize that. So how you can basically switch a role when you are, for example,</p> <p><strong>Robert</strong> [24:02]: sysadmin or maybe data scientist or something else and try to go into software engineer path. So maybe do you have some examples of people that did this transformation and how they approached it?</p> <p><strong>Miłosz</strong> [24:21]: Yeah, I&rsquo;ve seen it happen within teams. It&rsquo;s much easier to do it within the same company than just trying to be hired right away. It&rsquo;s basically like starting in software development in general. Everyone knows being a junior is not ideal because we need some skills, to take proven skills into a portfolio to be hired, and it&rsquo;s the same idea. But if you are already hired in some kind of team that works closely with software, it might be easier to start helping this team a bit, learning how they work, I might be doing some part-time work there for a time so I can see if you like it and if they like it. Yeah, I had a friend who was in the chat switch from front-end to back-end this way. Kind of similar, I guess. Like sysadmin to back-end and front-end to back-end I guess, pretty close, maybe front-end is a bit closer because it&rsquo;s software development, but still you need some skills you didn&rsquo;t have before.</p> <p><strong>Robert</strong> [25:49]: And I think, so what you said, it&rsquo;s some kind of internal team transfer and if you are kind of person that people know within the company that they can trust you and you are nice, it&rsquo;s a bit easier to do this transfer because if you are joining a new company, it&rsquo;s harder because it requires much more it involves much more risk for this company, so hiring, this kind of person let&rsquo;s say also it&rsquo;s possible, but I think it requires much more time to prepare portfolio, maybe some project that you work in and yeah, it&rsquo;s probably easier than just going outside of tech to tech obviously if you are within the tech.</p> <p><strong>Miłosz</strong> [26:32]: So it&rsquo;s much easier because you already have some contacts with the people you can spend some time with them see how they work I&rsquo;ve seen people do it from like a technical support role for example because this was area where they had to know the technology very well already to help clients and they were in close contact with developers so it was quite natural to just over time move towards development and remember,</p> <p><strong>Robert</strong> [27:03]: Be nice to other people because if you are not nice for those people, it&rsquo;s much harder to do this move. But unfortunately, sometimes if you are doing this kind of big change, sometimes you need to expect some salary downgrade.</p> <p><strong>Miłosz</strong> [27:20]: Unfortunately. Yeah, for sure. And title downgrade probably too.</p> <p><strong>Robert</strong> [27:26]: Yeah, but it may be worth sometimes, especially if you don&rsquo;t feel in your role that it&rsquo;s something that you would like to continue with. And yeah, I think also one nice tip maybe to try, okay, if you are looking for a company where&hellip; Try to find a company when you can use both parts of your skill sets, for example your sysadmin. So obviously it&rsquo;s not always easy to find this kind of company, especially if market is harder, but sometimes you may find a company when they are looking for a software engineer with sysadmin skills, for example, and it can be nice advantage. Or for example, if you&rsquo;re switching from front-end to back-end, you may look for a full stack role and try to do less and less.</p> <p><strong>Miłosz</strong> [28:14]: So this is the idea of this T-shaped skills, right? They are super proficient in one area and a few others, just a bit. But it&rsquo;s great if you have a team and you have someone with, for example, cloud skills or SysApp skills. So the rest of the team doesn&rsquo;t need to go that deep in this topic. And this person usually is happy to handle them.</p> <p><strong>Robert</strong> [28:39]: And yeah, this is the thing that also you recommend to do to basically go very wide with the skills. Obviously at the beginning you can focus on one area, but, At some point, I think it&rsquo;s nice to try other areas, for example, for a back-end developer, try a bit of front-end, a bit of data science, a bit of sysadmin, a bit of devops. Why devops is probably not the best term for that, but okay, let&rsquo;s use it. And it&rsquo;s nice, because maybe you may find a role that is different and you like more, for example. And I think some projects that you are doing after hours, it&rsquo;s pretty good for learning that. But you might discover that, okay, maybe there is some role that you prefer more. And I think also this kind of people are also very valuable in the team, because if you need to collaborate with other teams, so with some sysadmins, with frontend, with some cloud ops or whatever, you have more knowledge and you can, you know, have a discussion and, you know, because if you are working as a developer, you often may need something from your cloud operations team. And if you have no idea how it works, it&rsquo;s, you know, sometimes hard to request something.</p> <p><strong>Miłosz</strong> [29:59]: Yeah, so you have more skills in-house. You don&rsquo;t need to rely that much on external teams.</p> <p><strong>Robert</strong> [30:04]: And it&rsquo;s also nice because if you are working in a startup, you can just deliver more things because sometimes you may have some small front-end functionality or maybe some simple data science work and you don&rsquo;t need to outsource it to other team, wait ages for somebody that will do that. You can just do it yourself. I think it&rsquo;s great. And this is, for example, what we are doing in Trios Labs. So we are kind of lucky that we did many areas so we can basically work to people and we can do a lot of stuff because we can have some experience so we are not front-end experts we are not data engineering experts but we know the field enough that we can do it in the level that is and.</p> <p><strong>Miłosz</strong> [30:48]: Just use it to beat something end to end so we are not stuck or leverage some tools to help you do it. Because if you would need to hire someone to do it for you, that would be super expensive.</p> <p><strong>Robert</strong> [31:05]: Expensive and the overhead is also pretty big, because if you&rsquo;re doing something alone, you can just do it, ship it, and it&rsquo;s working. But if you are outsourcing it to somebody, it&rsquo;s probably 10 times more expensive in terms of time.</p> <p><strong>Miłosz</strong> [31:20]: And the communication overhead is not that it&rsquo;s an issue to talk with people, but there&rsquo;s always this scoping and discussions and agreeing on things, so this will take ages.</p> <p><strong>Robert</strong> [31:36]: It&rsquo;s not only for indie companies like ours, it&rsquo;s even for bigger companies where you have teams and requiring something from other teams is just not the most efficient way. And there is a follow-up question for this question. If it&rsquo;s worth to invest in some older technologies like Laravel or PHP. And I think it&rsquo;s maybe also an interesting way actually to do some switch because obviously working with older technologies, okay, it&rsquo;s maybe not the most interesting thing in the world. But from the other side, the upside is that less people are keen to work with that. So basically, the competition is&hellip;</p> <p><strong>Miłosz</strong> [32:22]: And there is Legasse to be worked on.</p> <p><strong>Robert</strong> [32:24]: Yeah, and you can actually learn a lot on the Legasse. Usually, I remember when I learned a lot, it was the hardest projects. The projects that are greenfield and everything is great, you will not learn that much during that.</p> <p><strong>Miłosz</strong> [32:37]: I would probably consider if the language is growing or declining rapidly. I guess if you consider Java let&rsquo;s say or maybe PHP too there&rsquo;s many many projects building those languages. They probably don&rsquo;t grow rapidly right now. I didn&rsquo;t check the indexes but I just guess. It might still make sense to go get into them because there will be lots of work in a way. But if you compare it to something that&rsquo;s declining by, I don&rsquo;t know, Peril? Maybe Ruby? Yeah, so that&rsquo;s something that you need to watch out for probably. Not to spend too much time learning something that will be obsolete.</p> <p><strong>Robert</strong> [33:28]: Yeah, yeah.</p> <p><strong>Miłosz</strong> [33:29]: But if you are just getting into tech and you learn one language well, it will be worth it probably anyway, because then you can pick up the next one much easier.</p> <p><strong>Robert</strong> [33:39]: To switch between programming languages than between titles, basically. And yeah, probably one more dimension there, so local market. So if you&rsquo;re looking for hybrid or on-site, it&rsquo;s also specific.</p> <p><strong>Miłosz</strong> [34:07]: Job market in general. Check what the market looks like, which is also not ideal, because it can change in a few years, but probably not that fast. So we should be safe at least for five years or so. For example, with Go it was a bit risky when we started working on it full-time because there weren&rsquo;t that many jobs, but also it was we learned quickly, so it wasn&rsquo;t a huge investment.</p> <p><strong>Robert</strong> [34:41]: And the competition, if some companies started to ask about Go wasn&rsquo;t that big.</p> <p><strong>Miłosz</strong> [34:47]: Oh, yeah.</p> <p><strong>Robert</strong> [34:48]: As we said, so we&rsquo;re later looking for people keen to switch to Go because there were no Go people in the market.</p> <p><strong>Miłosz</strong> [34:56]: Right, yeah, and that&rsquo;s also interesting. Sometimes, you know, in a situation like there&rsquo;s a niche language which some companies use and they are desperate to hire, and there are a few developers who are also desperate to be hired because they want to learn in this technology. But they don&rsquo;t always match each other, depending on the location and so on. So, one more thought. Maybe it&rsquo;s easier to stick to the proven but old technology than learning something fresh. You know, something that&rsquo;s not stable yet. Because it might never get traction.</p> <p><strong>Miłosz</strong> [35:41]: I think maybe Elixir is one example. I&rsquo;m not sure how the job market looks like for it, but I remember learning it around the time I learned Go and And I think betting on Go was probably a more pragmatic choice.</p> <p><strong>Robert</strong> [35:57]: Yeah, definitely.</p> <p><strong>Miłosz</strong> [35:58]: I think by now, Go is much more popular.</p> <p><strong>Robert</strong> [36:02]: Yeah, I remember that a long, long time ago, also, it started to be some Scala hype. And I guess that you can still have some Scala job.</p> <p><strong>Miłosz</strong> [36:12]: But… Probably Kotlin killed it a bit, right?</p> <p><strong>Robert</strong> [36:17]: It&rsquo;s a bit different, but it also had some specific use cases. But I think the entry point also wasn&rsquo;t the best thing. But at some point, it was trying to get some traction. People went into that, but I would say that, okay, maybe in some areas it&rsquo;s still there, but it&rsquo;s not longer hot.</p> <p><strong>Miłosz</strong> [36:38]: So you have those early adopters, companies who jump on it and hire people, but there&rsquo;s not enough of them. Over time, we just kind of die down.</p> <p><strong>Robert</strong> [36:52]: It&rsquo;s just a reminder about the thing that we are advocating all the time. Don&rsquo;t focus on learning specific technologies. Obviously, you should have some baseline, but we covered it more in an episode about efficient learning. If you focus on the things that are language agnostic and timeless, it will work for you for much longer. Because if you focus on learning some frameworks, languages, obviously you need to have some base, but it&rsquo;s not the most important thing. And this was also the key when we were looking for people to our Go teams, that we&rsquo;ve been not asking about how to implement something in Go, because we just know that it doesn&rsquo;t matter that much. What mattered for us was how they are solving problems.</p> <p><strong>Miłosz</strong> [37:42]: How they</p> <p><strong>Robert</strong> [37:43]: Can for example get some requirements and stuff like that and some general.</p> <p><strong>Miłosz</strong> [37:50]: Stuff Firmina shared in the chat that Java killed Kotlin oh yeah I don&rsquo;t know so I will trust you&rsquo;re right long late leave Java I agree and Firmina also asks if one year and a half is enough time to let it go So, I think day and a half is good enough.</p> <p><strong>Robert</strong> [38:17]: One evening I think it&rsquo;s enough.</p> <p><strong>Miłosz</strong> [38:19]: Yeah, it&rsquo;s ideal. What&rsquo;s the next question?</p> <p><strong>Robert</strong> [38:27]: The next question is about clean architecture. You would like to take that.</p> <p><strong>Miłosz</strong> [38:35]: Okay, so this is about where to define the data interfaces. Should you define it in a data layer or in the implementations? And this actually is something that where the language matters. At least if you compare Go to, let&rsquo;s say, Java. And one cool feature of Go is implicit interfaces.</p> <p><strong>Robert</strong> [39:03]: In other words, duck typing. So, if it&rsquo;s ducks, it&rsquo;s a duck.</p> <p><strong>Miłosz</strong> [39:13]: In Java, in many other languages, you have to explicitly say this class implements this interface. In Go, we don&rsquo;t need to do it. And it has this nice advantage that but you can keep the interfaces close to where you use them, so probably near the… not near the implementation of the interface, but near the thing that uses it. Usually the application layer or use cases or whatever you want to call it. Some people don&rsquo;t like that, but I think it&rsquo;s quite cool. you just need a good IDE to show you what implements an interface. That&rsquo;s a bit hard. If you are a diehard emacs fan, that might be a problem. Or maybe you need some plugins to navigate. But other than that, it&rsquo;s quite easier to keep the interface close to where it is injected. or used. And this solves this issue of a separate layer for the interfaces, like ports and adapters, where you need to keep them in one place. In Go, you don&rsquo;t need this layer at all. It&rsquo;s much simpler.</p> <p><strong>Robert</strong> [40:41]: And I think it&rsquo;s one big advantage of this duck typing and this non-explicit implementation, is that it&rsquo;s much easier to implement interface segregation principles. Basically, you no longer need to have interfaces that have 30 methods, for example, for some big repository. Because instead, you can just have multiple interfaces with the methods that you need in this use case. And you can inject this repository with multiple methods there. But you are making the contract of the interface much smaller. Because you are just defining which methods are required. And it&rsquo;s useful for refactoring, because sometimes you have a repository with that many methods, you can just cut it to multiple repositories and just inject it a bit differently. And it&rsquo;s nice. And it&rsquo;s also nice for testing, because when you&rsquo;re also writing tests, when you have some stubs or mocks, you can just stub or mock smaller interfaces.</p> <p><strong>Miłosz</strong> [41:40]: Yeah, that&rsquo;s also great. Small interfaces and close to where they are used. That&rsquo;s the idea of scenario. We covered this in more detail in the episode on Aclia Architecture. If you want to check it out, it&rsquo;s the third one,</p> <p><strong>Robert</strong> [42:01]: I think. And just a reminder, so if you have some other questions, listen on the chat, so we&rsquo;ll also cover it. Like, what&rsquo;s the joke? Okay, so let&rsquo;s maybe take a question from the chat. if MocnyŻuk asked the question. So how do you handle timeouts in distributed system in sync communication, HTTP, GRPC? So probably it can be answered from multiple levels, let&rsquo;s say. But in general&hellip; How it should be handled. So I guess the question might be, okay, if it&rsquo;s about distributed systems, I guess it may be about some chain of calls. So one service is called by something and it&rsquo;s calling other services. So in Go, for example, it&rsquo;s handled out of the box with context. So when you have the client that is calling your server and the connection is closed, so it&rsquo;s propagated basically through context and it should be propagated to HTTP Hender. It should be propagated to other requests that are going further. And if you are passing context, for example, to your database queries, it should be also cancelled and it&rsquo;s doing some kind of chain of cancellation and at the end the connection is closed and everything is great.</p> <p><strong>Miłosz</strong> [43:22]: What about more like a high-level approach? How do you define timeouts? Let&rsquo;s say you have a service that calls another service. Do you set a constant timeout for it?</p> <p><strong>Robert</strong> [43:36]: So usually i would say that at the beginning you have some endpoints from your message handler and usually it&rsquo;s good to have some default timeouts let&rsquo;s say like 30 seconds let&rsquo;s say for http request and the idea is pretty simple pretty similar because you have this context cancellation that is propagated to everything else basically yeah.</p> <p><strong>Miłosz</strong> [43:59]: I would keep it very very short or sync communication, especially for internal services. Because for external ones, external APIs, you sometimes can&rsquo;t control it. And then I would consider hiding it behind a message. Basically publishing a message, and then this message triggers this endpoint, and if it times out, it will just be destroyed eventually. If it&rsquo;s internal, I would keep it super short. Because long requests are usually a pain to handle.</p> <p><strong>Robert</strong> [44:38]: But it&rsquo;s important to not forget&hellip; So if you are talking exactly about Go here, so passing context, because sometimes I notice this problem that somewhere this context wasn&rsquo;t passed, and context background was passed, and it may be problematic when you are doing some I.O. Call, like if it&rsquo;s HTTP or some database call, it can run for much longer than you would.</p> <p><strong>Miłosz</strong> [45:05]: Like to run.</p> <p><strong>Robert</strong> [45:06]: And even if you have middleware with the timeout, it doesn&rsquo;t matter because it&rsquo;s not propagated and it will run, maybe not forever, but for pretty long.</p> <p><strong>Miłosz</strong> [45:20]: Yeah. And working with timeouts, there&rsquo;s one more difficulty. If you have a chain of services that call each other and they are slow, and then the timeout happens on the end, don&rsquo;t really know what happened in the system, the last one. Maybe it was just network failure or whatever.</p> <p><strong>Robert</strong> [45:43]: Especially you don&rsquo;t have transactions. Because if you have transactions, if you have transactions, again, we are talking about Go, but context cancellation should cancel that and it should roll back things. Yeah.</p> <p><strong>Miłosz</strong> [45:55]: So it&rsquo;s often where people will consider using a distributed transaction, maybe.</p> <p><strong>Robert</strong> [46:03]: Or at least some message in the middle, maybe event or command.</p> <p><strong>Miłosz</strong> [46:08]: Yeah, so there&rsquo;s something to watch out for. Ideally, those endpoints will be idempotent.</p> <p><strong>Robert</strong> [46:14]: You should try it.</p> <p><strong>Miłosz</strong> [46:16]: If it happens.</p> <p><strong>Robert</strong> [46:17]: Or ideally, the operations will be fast, and the things that you&rsquo;re doing synchronously, maybe it can add something to the queue, and it&rsquo;s just responding pretty quickly, and everything else is done in the background and maybe you can.</p> <p><strong>Miłosz</strong> [46:32]: Okay, I do have a follow-up. Let me read it. I have a scenario where a given call is propagated for various services via REST API. Therefore, you need to set up smaller and smaller timeouts the deeper you go in the tree of services. Oh yeah, okay, I see your point. So you have a long chain of services that call each other. And I don&rsquo;t like the approach to use increasing timeouts from top to bottom. That&rsquo;s why this question originated. How it should be handled? Right, so you have this chain. The first service needs to be aware. There will be lots of calls.</p> <p><strong>Robert</strong> [47:17]: So I assume that if you are mentioning that it&rsquo;s not like the entire chain is executed within one second, something tells me that it&rsquo;s taking a couple seconds or up to 30 seconds, for example.</p> <p><strong>Miłosz</strong> [47:30]: Yeah, maybe minutes.</p> <p><strong>Robert</strong> [47:32]: So, I don&rsquo;t know. So it&rsquo;s hard to say without seeing the system, but it&rsquo;s maybe a case for some more asynchronous communication, for example.</p> <p><strong>Miłosz</strong> [47:43]: I would consider it,</p> <p><strong>Robert</strong> [47:44]: Yeah. Yeah, because in this case, you may have a problem of cascading failures as well. Or also other solution that I know that for other people is crazy. Maybe we don&rsquo;t need as many microservices. It depends, because I don&rsquo;t know this case. maybe every team has its own service and its multiple teams and its very big product. That&rsquo;s fine in this case. But if it&rsquo;s, I don&rsquo;t know, one team has 20 services, maybe it doesn&rsquo;t need to be 20 services. Again, it&rsquo;s hard to say. I&rsquo;m more saying, more mentioning, more alternative, let&rsquo;s say, to consider that, but&hellip;</p> <p><strong>Miłosz</strong> [48:28]: But in general, if you have this spider web of service communication, I would probably try some asynchronous approach, because there are many issues there. For example, each service needs to react to some error. I&rsquo;m not sure if this is like one service calling 10 or 10 in a sequence, but any time any failure happens, maybe it&rsquo;s timeout, maybe it&rsquo;s a network issue or some domain error. It needs to handle it and it propagates it up the stack and then the next service also needs to handle this somehow.</p> <p><strong>Robert</strong> [49:06]: And if every service in the chain is storing something in the database, I hope in transaction, but from my experience, something tells me that it may be not always the case.</p> <p><strong>Miłosz</strong> [49:17]: It&rsquo;s more risky. But the transaction spans only one service anyway, so I would consider doing it with a message. Publishing a message with the first service, and then all the others reacting to it somehow.</p> <p><strong>Robert</strong> [49:32]: So, start with the question. Do you need that many microservices, PCS.</p> <p><strong>Miłosz</strong> [49:37]: Might be too late for that.</p> <p><strong>Robert</strong> [49:40]: Yeah, okay. That&rsquo;s true also.</p> <p><strong>Miłosz</strong> [49:42]: But the interesting approach has the downside that you don&rsquo;t get the error right away. I mean, you don&rsquo;t know the result of the other services, what they execute.</p> <p><strong>Robert</strong> [49:56]: Also, one more alternative, because, maybe it&rsquo;s also not that big issue, and maybe you can increase this timeout a bit more.</p> <p><strong>Miłosz</strong> [50:09]: Yeah, it&rsquo;s not stuck out the most naive.</p> <p><strong>Robert</strong> [50:12]: Yeah, I mean, it&rsquo;s a naive option, but we also know how it looks like. In perfect world, it would be good to join microservices, use asynchronous architecture for the places where we need but sometimes it&rsquo;s not important enough and we don&rsquo;t have enough time yeah.</p> <p><strong>Miłosz</strong> [50:32]: Depends how many activities are on there because if you add a new service to the chain every week, it will get much worse with times but if it&rsquo;s something you don&rsquo;t need to touch over the year whatever, we can just bomb the time out So,</p> <p><strong>Robert</strong> [50:52]: I hope it didn&rsquo;t help. But yeah, jokes aside, so, well, favorite face. So, it depends, but&hellip;</p> <p><strong>Miłosz</strong> [51:03]: Yeah, I would consider if async approach can work here. That could simplify it.</p> <p><strong>Robert</strong> [51:08]: Even simple ones. So, also, the thing that we are often advocating for is, even if you don&rsquo;t have any message broker, you have Postgres. You can have a very simple message broker on Postgres and it will be also fine for the beginning.</p> <p><strong>Miłosz</strong> [51:22]: I love how helpful we are that someone asks about sync communication specifically and our answer is to use async.</p> <p><strong>Robert</strong> [51:32]: Yeah, but this is the case of end with.</p> <p><strong>Miłosz</strong> [51:37]: Yeah, but other than that, there&rsquo;s not really much you can do. You can bump the timeouts, sell it everywhere to high value and live with it maybe it&rsquo;s not an issue or redesign the system work in a different way So, some frontline service that would send an event and then waits for the finished event. If you wait for a finished event, it&rsquo;s kind of like a timeout. You just do it over messages.</p> <p><strong>Robert</strong> [52:16]: So, it depends if you&rsquo;d like to wait for this event in context of request. So, basically, usually you do it in the way that you&rsquo;re sending some event, and the request is finished.</p> <p><strong>Miłosz</strong> [52:32]: And you can, for example, have a status in the database that each service writes to. And then you can display it on some kind of UI.</p> <p><strong>Robert</strong> [52:41]: But, again, it&rsquo;s not for free.</p> <p><strong>Miłosz</strong> [52:44]: It really depends on the use case.</p> <p><strong>Robert</strong> [52:46]: On the complexity of the system, how big is the team&hellip;</p> <p><strong>Miłosz</strong> [52:50]: If you want to share more about the exact use case you have, You can join our Discord and we can help you figure it out more in detail because it&rsquo;s hard to&hellip;</p> <p><strong>Robert</strong> [53:00]: Yeah, I think the size of the team, the scale, it&rsquo;s important here, because I think the answer depends on this context a lot, basically.</p> <p><strong>Miłosz</strong> [53:13]: Hello, full dump. Nice to see you. Thanks for joining us. And drop your questions if you have any. Okay, should we move on to the next question, is also about Clean Architecture, I think?</p> <p><strong>Robert</strong> [53:31]: Yep, that&rsquo;s true. So, my team started using Clean Architecture about a month ago, and it has been quite a learning experience. But overall, we are seeing the benefits as we wrap our heads around it. One thing we got stuck on sometimes is when to determine the boundary between application and domain layers. This is the question that we all usually have. Or question, what is domain logic? This is also a pretty interesting one. So we&rsquo;re using SQLRS in application layers. Sometimes the handlers are so simple for crowd operation that we don&rsquo;t have any dependency on the domain layer. Other may be than entities. We have other handlers that do a lot more and that&rsquo;s ended up with calling domain-specific logic. Is it common? So to recap, I think it&rsquo;s about if we need to have domain logic basically in our applications or we can simplify that because it&rsquo;s just some simple crud and it doesn&rsquo;t make sense to do some domain magic there.</p> <p><strong>Miłosz</strong> [54:41]: Yeah, I remember the long discussions about this we used to have.</p> <p><strong>Robert</strong> [54:46]: What is domain logic?</p> <p><strong>Miłosz</strong> [54:47]: Yeah, what is application logic? So, maybe we can start with that you don&rsquo;t need a domain logic in every service you have.</p> <p><strong>Robert</strong> [55:01]: Even if you have domain logic, because you can probably say domain logic about many things, like you&rsquo;re doing if in SQL query, it&rsquo;s domain logic.</p> <p><strong>Miłosz</strong> [55:10]: Yeah, so how I like to think about it is like, instead of four layers or whatever you might have, just think about it as a split between implementation details and the logic. That&rsquo;s a very basic split. You want this domain code or business code away from the SQL queries. And then, if you find it useful, you can split this logic into pure domain logic and all the other orchestration, application things that join things together. And it&rsquo;s not like a huge deal, but sometimes they work with very complex domains, where this domain layer will be huge, because the business rules are huge basically. And you need to model what your software does. And there&rsquo;s no other way than writing it down in the code. So it helps to have a separate layer that&rsquo;s not polluted by any database, calls, or whatever it&rsquo;s just pure functions and structs and lots of tests for it yeah,</p> <p><strong>Robert</strong> [56:27]: And I think for what we advocate here is trying to be mindful about that and asking yourself a question whatever I&rsquo;m doing, is it helping me or not? For example, okay, I&rsquo;m not using domain layer I have a lot of logic in my application layer okay, maybe it&rsquo;s not helpful maybe I should have domain layer or maybe the opposite side for example, I have domain layer with encapsulation and a lot of stupid mapping for CRUD so I&rsquo;m just mapping fields between layers and not doing anything for that I don&rsquo;t have tests for that and I see that it&rsquo;s not giving me any value okay, it should be not there and it&rsquo;s not easy to give a good answer if you should have a domain layer or not. You need to learn that. Oh, by doing. And I don&rsquo;t have a better answer than just writing this kind of logic and over. And yeah, you will have some puzzle.</p> <p><strong>Miłosz</strong> [57:24]: This is the hard part that you can&rsquo;t really define that. And I remember having the same issue because we have some meeting where we agree, okay, guys, this is application, domain logic, blah, blah, blah. We set this you know the system in place so everyone knows and then you have this edge case like when you for example like this person said they do just crud and then you start to wonder you know should it be there should it be there mm-hmm It&rsquo;s hard to give one answer to this, but most of the time it also doesn&rsquo;t really matter that much.</p> <p><strong>Robert</strong> [58:01]: Just try and be mindful. I know it&rsquo;s not easy, and I think it&rsquo;s also not easy with Skunko&rsquo;s fallacy, because if you already spent a lot of time to build your beautiful domain, layer, and at the end you see that it&rsquo;s just useless, it&rsquo;s a bit hard to delete that, I know.</p> <p><strong>Miłosz</strong> [58:21]: So if you could define very strict rules for your application, we wouldn&rsquo;t need probably admin developers because we would just wipe code or we would just use those builders for software.</p> <p><strong>Robert</strong> [58:40]: Or you will use spreadsheets. So I think it&rsquo;s interesting, heuristic, or kind of. Like, if you&rsquo;re building a crowd application and you&rsquo;re feeling that this is enough, maybe you don&rsquo;t need an application for that, maybe just something can do a super big spreadsheet doing that. Okay, it&rsquo;s maybe not most optimal, but sometimes it&rsquo;s more than enough for the beginning, especially if you&rsquo;re working in a startup, because you might not know if it will be useful or it will be trolled away. and, you know, the spreadsheet for the beginning. Right, fine.</p> <p><strong>Miłosz</strong> [59:16]: I think when you said that in the beginning, it was important to you. Just think of what you get. If you are just starting out, do you need this full design of all the layers and splitting? Probably not, because you don&rsquo;t really know what will be there. But if you have application layer and so handler, and you find 500 lines of code there with 50 ifs and lots of conditions, lots of arguments and so on. Then it kind of feels natural to just extract this thing somewhere else. It can be another function, another struct, or if you have many places like this, just do a separate layer of it. And that&rsquo;s basically the idea.</p> <p><strong>Robert</strong> [1:00:03]: The downside is that often we don&rsquo;t have enough time to do that, so the pain is starting to be bigger and bigger and bigger. But the more complex it is, the harder it&rsquo;s refactor that. But from the other side, I think it&rsquo;s also nice, because if something is bad enough, it&rsquo;s easier to propose changes.</p> <p><strong>Miłosz</strong> [1:00:22]: You feel the pain, so you know it will help to change it.</p> <p><strong>Robert</strong> [1:00:25]: For example, Mocnej Zhuk asked about these timeouts. I&rsquo;m guessing, but it may be two scenarios here. That those timeouts aren&rsquo;t painful and it may be worth investing in migrating, for example, to event-driven architecture, but maybe it&rsquo;s not painful enough and just increasing timeout, it will be good enough and, okay, we have some bigger problems. And the same is with refactoring. So, for example, if your model in the application layer is complex enough and it&rsquo;s slowing down your implementation a lot, you might have time to actually refactor that. And obviously, it would be great to be right at the beginning and start with domain logic and have tests from the day zero, but also let&rsquo;s be realistic that you may be wrong with your assumptions, that you may assume that, okay, it will be crud, but at the end, the complexity will explode. It happens. It happens even for us when you are working up. But the reason for that is not that it&rsquo;s sometimes some lack of skill, but it&rsquo;s often lack of understanding. That if you are starting to work on something you may not have full understanding of the problem space and you will discover it during implementation and there&rsquo;s just no way that you guess that okay maybe you will have luck and you will.</p> <p><strong>Robert</strong> [1:01:51]: Implementing in a domain logic. But you may have a bit less luck and you assume, okay, it&rsquo;s simple, complex, it will explode. And even if you do your best to guess it should have domain layer or not.</p> <p><strong>Miłosz</strong> [1:02:07]: You will be not always right. Next month, your boss can come in and say, actually, you know, we decided not to do this.</p> <p><strong>Robert</strong> [1:02:15]: Or we signed a customer that needs this, this and this.</p> <p><strong>Miłosz</strong> [1:02:18]: Yeah, can I just revert what you did last month? Because we need to change the domain model. And it&rsquo;s actually fine, you know, when this happens. So your domain model is never ready. You need to adjust it over time.</p> <p><strong>Robert</strong> [1:02:34]: So yeah, TLDR is totally fine to not have domain layer, just put everything to the application layer.</p> <p><strong>Miłosz</strong> [1:02:41]: I use some of it and some DOM.</p> <p><strong>Robert</strong> [1:02:43]: Exactly, exactly. And what&rsquo;s even better, and it&rsquo;s not like if it&rsquo;s good or not, but it&rsquo;s, I would say, necessary for applications to have modules that have domain layer and have modules that doesn&rsquo;t have domain layer. And it depends a lot on the application, but usually this is a good idea, because trying to keep consistency for everything, it&rsquo;s usually a bad idea. Because usually you have these modules that, even if you have very complex applications, not everything is critical.</p> <p><strong>Miłosz</strong> [1:03:13]: Although it will be much simpler to have one standard for everything.</p> <p><strong>Robert</strong> [1:03:18]: Yeah, but it will be just very expensive compared to how much you will gain from that.</p> <p><strong>Miłosz</strong> [1:03:28]: So I really like this idea of fixing something once you feel the pain. If something is not painful maybe it&rsquo;s fine even if it&rsquo;s not the best maybe it could be refactored maybe it could be split to modules or whatever but if it&rsquo;s not painful you probably have more stuff in the backlog to take care of and</p> <p><strong>Robert</strong> [1:03:54]: You will never finish.</p> <p><strong>Miłosz</strong> [1:03:55]: Everything yeah, you will just keep growing it that&rsquo;s normal, that happens in every company every project Oh, I think we can answer this one.</p> <p><strong>Robert</strong> [1:04:09]: But we have another one about clean architecture. Oh, okay. So, what about clean architecture and small teams with complex projects? So it&rsquo;s a question about the episode that we had. So the most popular thing that we have. So 10k views on just YouTube. So yeah, this is probably why it generated&hellip;</p> <p><strong>Miłosz</strong> [1:04:32]: So there we mentioned that architecture shines for bigger teams and complex projects. So if you have a smaller team or a simpler project, better to avoid it. The question is, what about small team and complex projects? Yes.</p> <p><strong>Robert</strong> [1:04:51]: Yeah.</p> <p><strong>Miłosz</strong> [1:04:54]: Yeah.</p> <p><strong>Robert</strong> [1:04:55]: So it makes sense. So we are a two-people team, and even if we are a quite small team, we are using some lightweight implementation of Clean Clean Architecture, because if you were already doing that, it doesn&rsquo;t add that much overhead, and you are doing it properly, and it can basically help in many, cases, even if you&rsquo;re a small team. Again, the cost is not very big if you&rsquo;re already proficient with doing that, and you can just have benefits out of that. It&rsquo;s also some natural way of doing things, but again, it&rsquo;s about doing it in a lightweight way.</p> <p><strong>Miłosz</strong> [1:05:40]: I even do it for my own projects, very small ones, because it comes kind of natural. But yeah, the key point here is to do it in a very lightweight way. For example, not inject everything from an interface like we often see in Dan. That can be crazy.</p> <p><strong>Robert</strong> [1:06:02]: Also, the thing that we mentioned in the previous question, you don&rsquo;t need to have domain layer with the clean architecture. It&rsquo;s not a must.</p> <p><strong>Miłosz</strong> [1:06:10]: You don&rsquo;t even need application layer if you don&rsquo;t know.</p> <p><strong>Robert</strong> [1:06:13]: And probably you shouldn&rsquo;t have more than four layers, because we&rsquo;ve seen the comments on Redis that do.</p> <p><strong>Miłosz</strong> [1:06:19]: You remember how it was? Yeah, it was like Clean architecture is over-engineering. You shouldn&rsquo;t have more than four layers in your project. We&rsquo;re just free. I don&rsquo;t know what some people do, but it sounds like over-engineering then, for sure. If you manage to keep it simple, you can do it for small projects, for sure. For small teams, complex projects. Sure. Why not? I&rsquo;ll just watch out for making it overcomplicated for no reason. Once again, this question of why it helps can be helpful. I want to understand why you do it, not just do it because someone says, this is a nice idea. So for example, do you run integration or component tests? If you do, the bendless injection is what you need basically to do it in the same way. If maybe you don&rsquo;t want component tests at all, maybe then you don&rsquo;t need the prevention injection at all. Let&rsquo;s try to understand if it makes sense or not.</p> <p><strong>Robert</strong> [1:07:32]: And it was an interesting follow-up question and all that. So if it&rsquo;s something like under-engineering, not spending any time on architecture at all.</p> <p><strong>Miłosz</strong> [1:07:47]: Yeah my favorite quote here is that there is no there&rsquo;s no such thing as no design there&rsquo;s only bad design that seems similar I just don&rsquo;t spend any time on proper design or any kind of architecture, you&rsquo;ll end up with this big ball of mud. And sometimes it&rsquo;s fine if it&rsquo;s some project you do over the weekend and we&rsquo;ll scrape or your vibe code.</p> <p><strong>Robert</strong> [1:08:19]: Or it&rsquo;s proof of concept that you will hopefully throw away after proving if it works.</p> <p><strong>Miłosz</strong> [1:08:27]: But yeah, keep in mind it will happen. So is it what you want? If you want to maintain this thing over time, maybe just spend some time, a few hours to draw some diagrams, try to sort it out in your head how it should look like.</p> <p><strong>Robert</strong> [1:08:45]: And I think it&rsquo;s one interesting thing that we covered a lot in the&hellip; One interesting thing and side effect of this node design that we covered a lot in the previous episode, how it affects code review. So if you didn&rsquo;t make any design in your team, so The code review, so the PR that you may submit, may, let&rsquo;s say, surprise some people. And surprising PRs are usually not best because it will trigger a lot of discussions, that resolving will take ages, that fixing will take ages, and probably it will take more time that you will spend on design. And I know nobody likes meetings, but I think I like having big PRs that are discussed for ages less than meetings. And I like the sentence that five days of coding can save one day of planning. And it&rsquo;s often the case. and even if it&rsquo;s not visible on the first site.</p> <p><strong>Robert</strong> [1:10:01]: It&rsquo;s often visible during review that is going for ages. And later people are blaming that code review is awful because you have a lot of the comments, you need to resolve that. Let&rsquo;s just push everything to main. But it&rsquo;s not a problem of code review. It&rsquo;s a problem of surprising people. And don&rsquo;t surprise people with your code review.</p> <p><strong>Miłosz</strong> [1:10:24]: Architecture sounds like something complex that architects do. But it&rsquo;s basically stuff like if you have 10 services connected with synchronous calls, what happens with timeouts? That&rsquo;s basically it. Try to think about it, what will happen, how to handle this and then prepare for it somehow or either fix it or just agree it&rsquo;s fine and note it, document it somehow.</p> <p><strong>Robert</strong> [1:10:55]: Or a different thing that I also remember that we faced one time is not thinking really how teams are split so let&rsquo;s imagine that you have some process, and let&rsquo;s imagine that the beginning of the process is handled by team a later team b is doing something the process later team a is doing something again and later some team c, yeah yeah yeah and it&rsquo;s pretty problematic because nobody can really own some big chunk of the process because it&rsquo;s really chopped for small parts that are between teams and it&rsquo;s, hard to do some kind of handover of big.</p> <p><strong>Robert</strong> [1:11:42]: Let&rsquo;s say, events in this process. And again, it&rsquo;s a matter of a bit of design and thinking actually how the process looks on the high level. And obviously, it can be fixed later, but it&rsquo;s also problematic because often it means that, okay, if this part of the process is handled by this team and it&rsquo;s between parts that are owned by this team A, maybe this part of the process that is in the middle should be also owned by team A. And, okay, it can be done, the service can be migrated to other teams, but it&rsquo;s expensive, because it was written by other teams. Probably it will be harder to maintain, because you have no idea how it works. Maybe this team will be also not happy about that, because they will say, I already have 50 services to maintain, we don&rsquo;t have capacity for that. and yeah it&rsquo;s like.</p> <p><strong>Miłosz</strong> [1:12:39]: A very high level architecture like an organizational there&rsquo;s this team topologist book that talks about this I think it&rsquo;s quite nice if someone wants more on this topic you can check it out</p> <p><strong>Robert</strong> [1:12:52]: And we also mentioned in some previous question that even if you are not manager if you are not architect it&rsquo;s nice to have this knowledge from other areas for example like team topologist that is, a bit higher level, but from the other side, you can also propose better things and be promoted much faster, because very often those people that are on higher, levels, they may not know that.</p> <p><strong>Miłosz</strong> [1:13:20]: Yeah, that&rsquo;s true. We can help them fix things, which can also help you in your career. Yeah, so it&rsquo;s nice to have those more orthogonal knowledge. That&rsquo;s pure development. That&rsquo;s a good example of this, too.</p> <p><strong>Robert</strong> [1:13:39]: Yeah, and this is the thing, that if you are going to more senior roles, you&rsquo;ll notice that, okay, writing software is important, but there are many things around that. And to really be beyond this senior level, you need to understand much more things and also go beyond technical things, basically.</p> <p><strong>Miłosz</strong> [1:14:01]: Yeah, and AI just confirms it. Writing code is cheap somehow I don&rsquo;t hear many great stories about you know generated codes we are not probably doing much more meaningful work thanks to it to figure out anyway what you need to do in the first place and I think it&rsquo;s</p> <p><strong>Robert</strong> [1:14:26]: The thing that people that are most senior can get more out of that basically.</p> <p><strong>Miłosz</strong> [1:14:34]: Yeah, exactly. So you get boring parts away, but you still need to figure it out on a high level and design it properly.</p> <p><strong>Robert</strong> [1:14:45]: Alright, cool. So I think we are shortly going out of questions, as long as you not put something in the chat. Or as long as you don&rsquo;t have something to add about previous questions that came into your mind.</p> <p><strong>Miłosz</strong> [1:15:04]: So not as long in the chat if you have any more questions.</p> <p><strong>Robert</strong> [1:15:08]: Last chance before our holiday season.</p> <p><strong>Miłosz</strong> [1:15:12]: Yeah. So we can share a bit more about our future plans.</p> <p><strong>Robert</strong> [1:15:17]: Yeah, so short term, resting a bit. So I think we had a super busy time now with Go Event Driven V2. So about doing the big update, also giving you support.</p> <p><strong>Robert</strong> [1:15:33]: Any horror stories?</p> <p><strong>Miłosz</strong> [1:15:39]: We need a separate episode about startup horror stories. Oh, yeah. But I also want people to share theirs because I love reading this.</p> <p><strong>Robert</strong> [1:15:50]: One of my favorite ones is about sending a couple million SMSs to one person within one minute. Remember, add some trodling.</p> <p><strong>Miłosz</strong> [1:16:04]: Thanks for the suggestion for me I think we need a separate episode about this and I hope to hear people&rsquo;s stories as well</p> <p><strong>Robert</strong> [1:16:16]: So after resting what are, our next plans I think we can officially confirm that we know what will be our next training that a lot of people were asking about. So do you want to share these news?</p> <p><strong>Miłosz</strong> [1:16:34]: Yeah, so we plan to release our next training, which will be the Domain Driven Design in Go. And there will be also a hands-on training like Go Event Driven and Go in one evening. Let&rsquo;s focus on Domain Driven Design. Or I would</p> <p><strong>Robert</strong> [1:16:51]: Even say that obviously Domain Driven Design, it&rsquo;s some way how to go there, but from the high level it&rsquo;s more about how to build application with complex domain logic. So many questions you heard about this complex domain logic is for a good reason, because applications that are successful often have these parts that are very complex and require a bit more love than the crowd applications. And those are usually the unique part applications that are making money for the company. And the interesting thing is that, companies are kind of happy to hire people that can work with that because, one thing is that they need to usually touch this code update this code to make more money but from other side it&rsquo;s also hard to find people that they can do that because it requires a bit different skill set to do that and and.</p> <p><strong>Miłosz</strong> [1:17:50]: Often people are like i am just the developer give me give me the requirements and i will transfer them into code so yeah that&rsquo;s not really helpful for most companies, because they need people who can understand the business domain, can help people figure out what the product needs. Basically, product engineering. So this training is about that.</p> <p><strong>Robert</strong> [1:18:14]: We&rsquo;re pretty lucky that we had a chance to work with some pretty weird domains that well, people try to use CRUD approaches sometimes there and it was actually quite interesting to see how this trying of simple approaches totally failed.</p> <p><strong>Miłosz</strong> [1:18:33]: Or maybe they thought they are using DDD, but DDD was their own idea which also didn&rsquo;t end up well.</p> <p><strong>Robert</strong> [1:18:41]: Yeah.</p> <p><strong>Miłosz</strong> [1:18:42]: Yeah, so we this would be also hands-on training, like the previous ones. We learned a lot from people&rsquo;s feedback. We had over thousand trainees in both trainings.</p> <p><strong>Robert</strong> [1:18:58]: Over 2000. I think two and a half thousand people.</p> <p><strong>Miłosz</strong> [1:19:04]: So we received lots of feedback. We had some things we wanted to improve. That&rsquo;s why we prepared the second edition of Go Event Driven. That should be much more fun to go through and easier in general. So we want to take those learnings to the next training as well. Make it more focused on what&rsquo;s the most important and focus on real life scenarios</p> <p><strong>Robert</strong> [1:19:34]: If you don&rsquo;t like coding, sorry, it&rsquo;s very hands-on.</p> <p><strong>Miłosz</strong> [1:19:40]: You need to code. There&rsquo;s no videos to watch.</p> <p><strong>Robert</strong> [1:19:43]: And yeah, I think that this is a pretty common thing. You need to learn by practice, when to go simple way and where to go complex way. This is actually what was helpful for us. just working with many projects in a daily job, after work, and trying to get this muscle memory to have some idea what approach to use there. And this is why we are advocates of hands-on, because this is the only way actually how you can gain this skill. And you can get in work in the projects and do some crazy over-engineering stuff at the end, or maybe it&rsquo;s a bit better to learn it in a controlled environment.</p> <p><strong>Miłosz</strong> [1:20:33]: So you don&rsquo;t need to confess to your manager why you need two months of rework and refactoring. So you can experiment on your own project or our guidance. So this will be in Go, initially, but we probably will have more languages later, but that&rsquo;s probably not the start. Just as with the other training, we will have a pre-sale, which will give the best price. So basically we want to check if there&rsquo;s enough interest before we start working on it. And of course you can ask for refund and so on. That&rsquo;s the same rules as we did in</p> <p><strong>Robert</strong> [1:21:19]: September so for now a bit uh a bit of rest a bit of thinking what we&rsquo;ll do uh in quarter.</p> <p><strong>Miłosz</strong> [1:21:27]: And also there&rsquo;s also one thing we want to complete which is go with the domain ebook so we have some cool ideas about to release the second edition or the 1.0 maybe edition is a better name</p> <p><strong>Robert</strong> [1:21:43]: Yeah because there are still some chapters that were not finished.</p> <p><strong>Miłosz</strong> [1:21:47]: That&rsquo;s long overdue for a few years, so finally you should have some time to do it.</p> <p><strong>Robert</strong> [1:21:53]: But you know, probably it&rsquo;s also good to share that if you have projects in your work that are taking more time than your thoughts, you&rsquo;re not the owner. We also have this problem, but for a good reason often. And yeah, as we said, in September we&rsquo;ll also do some updates of the NoSilverBullets form. It&rsquo;s a bit secret, so you&rsquo;ll see on September, but I think it will be quite cool. That&rsquo;s it, I think.</p> <p><strong>Miłosz</strong> [1:22:33]: I have no more questions.</p> <p><strong>Miłosz</strong> [1:22:36]: Firmina is laughing at us, so that&rsquo;s time to wrap up.</p> <p><strong>Robert</strong> [1:22:42]: Yeah so thanks for being with us and see you in two months yeah thank.</p> <p><strong>Miłosz</strong> [1:22:47]: You everyone for joining us next time</p> <p><strong>Robert</strong> [1:22:50]: See you next time ciao.</p>How to Create PRs That Get Merged The Same Dayhttps://threedots.tech/episode/prs-that-get-merged-the-same-day/Wed, 25 Jun 2025 16:00:00 +0000https://threedots.tech/episode/prs-that-get-merged-the-same-day/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Prioritize reviews over new work</strong> - treat PRs as work that&rsquo;s almost done and needs to be pushed to production quickly</li> <li><strong>Big PRs create a deadly loop</strong> - when reviews take ages, developers make even bigger PRs to avoid multiple long waits</li> <li><strong>Knowledge sharing is the hidden benefit</strong> - code reviews aren&rsquo;t just gatekeeping, they spread understanding of how things work across the team</li> <li><strong>One-day cycle time is possible</strong> - start work in the morning and merge by end of day with proper team culture and practices</li> <li><strong>Split work vertically and horizontally</strong> - break features into small slices and layers that multiple people can implement in parallel</li> </ul> <h2 id="introduction">Introduction</h2> <p>In this episode, we discuss how to make code reviews fast and effective by keeping pull requests small.</p> <p>We explore why big PRs are problematic, what causes them, and practical strategies to create PRs that can be merged within a day.</p> <p>Instead of waiting days for reviews with 200 comments, we focus on techniques that help teams achieve smooth, fast review cycles where work flows quickly from code to production.</p> <h2 id="show-notes">Show Notes</h2> <ul> <li>Feature flags - technique for merging code that&rsquo;s not ready yet</li> <li>Mob programming - multiple people coding together with one driver</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fepisode%2Fis-clean-architecture-overengineering%2F">Clean Architecture</a> - helps with splitting features layer by layer</li> <li>Linters - automated tools for catching style issues instead of nitpicking in reviews</li> <li>Draft PRs - for getting early feedback on experimental work</li> <li>Shadow mode - running new code alongside old code to compare results</li> <li>The Phoenix Project - book mentioned about bottlenecks in teams</li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>If you have a couple developers that are independently working on some features, they are actually not a team. They are a set of individuals working on something. And at the end, there is no shared responsibility to deliver some features.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>It&rsquo;s a well-known phenomenon that if you have small PR, people will find three defects, but if you have huge PR, they will just write LGTM.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>I like the phrase that five days of coding can save one day of planning. And I think it&rsquo;s often true and it&rsquo;s often a problem in multiple teams.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>I sometimes see people mention that code reviews are a sign of lack of trust. As in, &ldquo;You don&rsquo;t trust your teammates because you need to review code they write.&rdquo; I think it&rsquo;s funny because I find it the other way. So I actually trust them to review my code because I don&rsquo;t trust myself enough to know I don&rsquo;t make any mistakes.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>I think it&rsquo;s one of the most important skills for product engineering, and I think it&rsquo;s super rare as well. Because most developers, when they get a clear description of a problem from a stakeholder, will just say, okay, we can do it, because they want to be the hero.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>I know that a lot of people are like, let&rsquo;s skip planning, let&rsquo;s just write that and ship that and it will be everything fine. And I know that I was also like that when I was younger and less experienced, but later you can kind of learn that, okay, it can give you some acceleration at the beginning, let&rsquo;s say, but later it can be very problematic.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTY3-oDnNKE8%26amp%3Bt%3D0s">00:00:00 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTY3-oDnNKE8%26amp%3Bt%3D103s">00:01:43 - Why review code</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTY3-oDnNKE8%26amp%3Bt%3D432s">00:07:12 - Problems with big PRs</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTY3-oDnNKE8%26amp%3Bt%3D973s">00:16:13 - Causes of big PRs</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTY3-oDnNKE8%26amp%3Bt%3D1407s">00:23:27 - Planning importance</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTY3-oDnNKE8%26amp%3Bt%3D2117s">00:35:17 - Breaking down changes</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTY3-oDnNKE8%26amp%3Bt%3D2675s">00:44:35 - Big refactoring strategies</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTY3-oDnNKE8%26amp%3Bt%3D3163s">00:52:43 - Code review culture</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTY3-oDnNKE8%26amp%3Bt%3D3864s">01:04:24 - Accelerating review cycles</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DTY3-oDnNKE8%26amp%3Bt%3D4324s">01:12:04 - Q&amp;A session</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Robert</strong> [0:00]: Ever waited days for Code Review only to get 200 comments and endless discussions in these PRs, so maybe you wonder what&rsquo;s the reason of this kind of big PRs and why it&rsquo;s happened that such big PRs are happening.</p> <p><strong>Robert</strong> [0:18]: And what&rsquo;s often the problem, it&rsquo;s getting worse. I mean, when you are waiting ages for PRs to be merged, it&rsquo;s creating some kind of dead loop, let&rsquo;s say.</p> <p><strong>Miłosz</strong> [0:33]: It gets even worse.</p> <p><strong>Robert</strong> [0:35]: It gets even worse, because if you know that you wait ages for merging a PR, you are just making them bigger, because you know that if you prepare 10 smaller PRs, you will wait longer for those PRs. Today, in this live podcast, we&rsquo;ll discuss what are the problems that are causing that PRs are big and some strategies that we used in our teams to make those PRs merged much faster. So by much faster, I mean how to create PRs that are merged within a day. So let&rsquo;s imagine a scenario when you are starting to work on some task in the morning and it&rsquo;s merged by the end of the day. Sounds like a perfect word that doesn&rsquo;t exist, but our experience is a bit different.</p> <p><strong>Miłosz</strong> [1:24]: Can&rsquo;t be possible. This is our last live episode, so feel free to leave comments in the chat, or we&rsquo;ll pick them up during the discussion, or at the end we&rsquo;ll have a Q&amp;A section. So let&rsquo;s kick off with why we need to review code code at all.</p> <p><strong>Robert</strong> [1:45]: Yes, I think it&rsquo;s important to start with the fact that a lot of people are tracking code review as gatekeeping, so basically to ensure that changes that are not, expected are not merged. And I think it&rsquo;s bad. It&rsquo;s a bad sign if code review is kind of gatekeeping. So from my experience, if code review is gatekeeping, it usually means that you didn&rsquo;t make a good job earlier on agreeing how things should be implemented. So it&rsquo;s a matter of some good practices in the team, but it&rsquo;s also a matter of designing things. So in other words, if you spend time earlier on planning that, it should be not a surprise what&rsquo;s in the PR.</p> <p><strong>Miłosz</strong> [2:37]: So if someone opens a PR and they start wondering what exactly it is about, then maybe it&rsquo;s too late. Maybe they don&rsquo;t have enough context and they want enough to review it in a reasonable pace. I sometimes heard of people starting PRs with checking out the code and running the applications, trying to understand what&rsquo;s going on. I think we can agree with this. This should be an anti-pattern, unless something unexpected happens, like your team is busy with something else, I don&rsquo;t know, you ask someone else from outside the team to review your code. But most of the time, you should know beforehand what the PRs will be about.</p> <p><strong>Robert</strong> [3:30]: So, if it&rsquo;s the case&hellip;</p> <p><strong>Miłosz</strong> [3:32]: It&rsquo;s a perfect word.</p> <p><strong>Robert</strong> [3:34]: That can exist, but we&rsquo;ll go into that a bit later. But if gatekeeping is not the reason why you are doing code review, it should be some reason why you are actually doing code review. And from my perspective, one of the most important reasons why you are doing code review is the knowledge sharing. And I think it&rsquo;s not mentioned very often, and I think it&rsquo;s some kind of hidden advantage that maybe we don&rsquo;t think that often. But this knowledge sharing is important because when you&rsquo;re doing review, you&rsquo;re learning how something is implemented, and it&rsquo;s helping to spread this knowledge how something is implemented in the team. And it&rsquo;s pretty important because if you&rsquo;re spreading this knowledge, it&rsquo;s easier later to extend this functionality by more people in the team.</p> <p><strong>Miłosz</strong> [4:25]: Because you know where to look for changes if you need to extend them or implement your own feature.</p> <p><strong>Robert</strong> [4:31]: And while usually it&rsquo;s not the best idea that one person has some area in the code and just this person is touching this code, well, I&rsquo;ve seen it sometimes in my life. And okay, while in the short term it can be a good strategy because this person really knows how this code works, but at some point it may not scale if it will be more functional to implement there. And the problem starts to be a problem at this time.</p> <p><strong>Miłosz</strong> [4:58]: Also depends how big your team is. For smaller teams it&rsquo;s probably optimal because if you have a small PR, everyone can take a look and have a rough idea what you are doing. The bigger team is probably a bit more complex, because not everyone can see each PR in progress.</p> <p><strong>Robert</strong> [5:22]: Definitely. But still, if you have a bigger team, we&rsquo;re not talking about, for example, five people. Because I think if you have five people, it&rsquo;s a bit easier to share this knowledge and be in the stage that everybody knows what&rsquo;s done. But for example, if you have 15 people, it&rsquo;s a bit harder and obviously it doesn&rsquo;t make sense to not share for everybody. But I would say that if you have some subset there, that makes much more sense.</p> <p><strong>Miłosz</strong> [5:49]: I sometimes see people mention and code reviews are a sign of lack of trust. You don&rsquo;t trust your teammates because you need to review code they&rsquo;ve write. I think it&rsquo;s funny because I find it the other way. So I actually trust them to review my code because I don&rsquo;t try to trust myself enough to know I don&rsquo;t make any mistakes. So it&rsquo;s good to have another pair of eyes. Just take a glance and very often they will have some context you missed. If we had no comments on PRs, we would just stop doing them. But they come useful.</p> <p><strong>Robert</strong> [6:30]: In other words, you should not look on PRs like gatekeeping, but rather something positive that can help you to improve your stuff. I think it&rsquo;s also important that it shouldn&rsquo;t require from the reviewer to verify the changes, because I also heard that some teams were doing it like that, that somebody is doing or doing review and verifying if it&rsquo;s worked properly. I think it&rsquo;s probably more your responsibility to ensure that the functionality that you implemented is working.</p> <p><strong>Miłosz</strong> [7:02]: Sounds a bit like QA, but anyway, you should be doing QA on your own code. so all right um</p> <p><strong>Robert</strong> [7:12]: Okay i think let&rsquo;s maybe start with why having big prs is a problem because i think if you are working with software most of people agree that it&rsquo;s big elephant in the room that big prs are problem but i think a lot of people are not doing anything about that like yeah big prs are bad, but whatever.</p> <p><strong>Miłosz</strong> [7:34]: Or they just say out loud, okay, guys, let&rsquo;s not do it again.</p> <p><strong>Robert</strong> [7:38]: Let&rsquo;s not do big PRs.</p> <p><strong>Miłosz</strong> [7:40]: But then it always happens. For me, the first one is the most obvious one. It&rsquo;s just hard to review. It&rsquo;s a well-known phenomenon that if you have small PR, people will find free defects, but if you have huge PR, they will just write LGTM. And, yeah, I think we know this paint well. It&rsquo;s very easy to just glance at a huge change set and not go very deep into the details.</p> <p><strong>Robert</strong> [8:17]: Obviously, we&rsquo;re losing in this scenario the knowledge sharing that we mentioned at the beginning. If you have really huge PR, there is no way that you&rsquo;ll read that and that you&rsquo;ll be able to learn from that how it works.</p> <p><strong>Miłosz</strong> [8:32]: Exactly. And then all the processes like reading, understanding code, comments, and whatever else goes on in the PR just takes longer. And no one likes long discussions especially asynchranos so if you take a huge PR and then on top of this you have 50 comments that will take ages to review and if someone spends weeks or longer working on this PR and then you drop 20 comments on it they are probably not eager to to listen to what you want to say, your suggestions.</p> <p><strong>Robert</strong> [9:19]: You&rsquo;re just thinking, I would like to get it out as soon as possible.</p> <p><strong>Miłosz</strong> [9:23]: I just want to be done with it and push it through to prod. So, yeah, it&rsquo;s probably not encouraging to change this. It becomes kind of like an artificial review process but doesn&rsquo;t give you much.</p> <p><strong>Robert</strong> [9:38]: Probably in this case, it wouldn&rsquo;t change much if you would not have any cool review. The chance we can push to mine and&hellip; Yeah, exactly. Yeah, and another problem with big PRs is that, as you said, when you have the situation when, we are working on this PR for longer time, it&rsquo;s a big chance that you receive 20 comments and okay, it depends on the team, but usually it may end up with a big rework of the job that you&rsquo;ve made. So in other words, you will lose a lot of time on writing this code and you&rsquo;ll lose a lot of time on rewriting it later.</p> <p><strong>Miłosz</strong> [10:20]: And you are very late in the process because someone spent already a week or two writing it. And now if you want to rework it now, it probably will be a huge change.</p> <p><strong>Robert</strong> [10:32]: And even worse, probably in the meantime, when you were working on that in your basement, people were doing other changes and you probably will have a lot of conflicts. It&rsquo;s even more funny if other person in the team have PR like yours and this person merged it earlier. Okay, if you are not a lot of people in the team, you can always not review this person&rsquo;s PR, so you are safe that it will not introduce conflicts. But you see the problem, the conflict of interest. I mean, it&rsquo;s also problematic for you if the other person is merging some big PR, so you are not doing a review for this person because it&rsquo;s too late for your PR.</p> <p><strong>Miłosz</strong> [11:10]: Or you&rsquo;re raised to do it first, so you don&rsquo;t need to deal with conflicts.</p> <p><strong>Robert</strong> [11:14]: Or you can add a lot of comments, so this person PR will be also blocked, so you can merge your PR first.</p> <p><strong>Miłosz</strong> [11:25]: And then, when the conflicts happen, you get an approve and you think it&rsquo;s ready, and then you just resolve the conflicts. And two hours later, you are finished, but it takes time to do something more productive. So just spend time on maintaining this PR instead of writing code. And this merging can also introduce new bugs or challenges that no one reviews, so it&rsquo;s another risk.</p> <p><strong>Miłosz</strong> [12:03]: There&rsquo;s also a longer feedback loop, because communication is asynchronous in the comments, so someone gets sick or has days off, you need to wait for them, they come back, they lost some context, we need a separate discussion about all of this. And also that&rsquo;s a paradox people might think that small PRs require context switching because you have to very often review code and review what you are doing but in reality big changes are even worse because of this because you need to every time you come back to it you need to refresh this huge context in your mind instead of something very small. So that&rsquo;s another one. And also if you want to stack PRs, so you want to branch out of an existing branch because you need the changes in there, which is often useful. With small PRs, it&rsquo;s quite easy to do, not risky, but with big PRs, you block everything. The big PR gets merged.</p> <p><strong>Robert</strong> [13:20]: And later you have PR that depend on PR that depend on another PR. And if the first PR in the chain will need to be changed largely, it&rsquo;s just an horror to keep it up to date. I have PTSD if I think about such PRs.</p> <p><strong>Miłosz</strong> [13:40]: And then someone asks when this will be done and says, Yeah, we only need to wait for Robert&rsquo;s PR to be merged. We&rsquo;ve been waiting for two weeks now, so at the moment. Yeah, we&rsquo;ve seen it too many times. And other ideas? Yeah, this lack of visibility is also risky because&hellip; If you don&rsquo;t know when the feature will drop, because a PR takes us long to merge, you may be tempted to use estimates, which is also problematic, but this comes from the fact that no one knows when something will happen. You have this huge PR, which seems almost ready to merge, but you keep being added to it and discussed, and you just never know when it&rsquo;s finished. And the worst of all, you can create a long-living branch. One that takes three months or so to merge. And it&rsquo;s a completely different version of the software.</p> <p><strong>Robert</strong> [15:03]: It keeps living in parallel.</p> <p><strong>Miłosz</strong> [15:07]: If you ever work with this, you probably don&rsquo;t want to do it again.</p> <p><strong>Robert</strong> [15:12]: So, it all sounds bad. I mean, there are multiple reasons. Why we should not do big PRs, but from the other side, for some magical reason, from my experience, having big PRs is very often the problem in multiple teams. And there are a couple of things that I believe are creating big PRs. And again, I think it will be mentioned multiple times in this episode that, in my opinion, we very often have good intentions, but despite that, the problem still occurs. And the same is with big PRs. I think that&rsquo;s why it&rsquo;s important to be conscious about what can cause big PRs. I think one of the things that is coming to my mind at the beginning is that we have some kind of complete feature mindset.</p> <p><strong>Robert</strong> [16:13]: We like to create a feature and have it work on production ASAP. So it makes sense that we&rsquo;re working on something, and it&rsquo;s just nice when you write that and it&rsquo;s going directly to the production. And it sounds good, but unfortunately often features are not that simple that we can easily implement it within one PR. If we&rsquo;ll try to do that, okay, maybe someone sometimes will estimate that, yeah, it&rsquo;s just one day of work. But how often do you think that something is one day of work and it&rsquo;s ending up with a week of work because there is some hidden complexity that you didn&rsquo;t take into consideration at the beginning.</p> <p><strong>Miłosz</strong> [16:54]: And splitting it seems like additional work. So you might be tempted to avoid it. You wouldn&rsquo;t want to go. It should be easier, right?</p> <p><strong>Robert</strong> [17:05]: Yeah. And I think it&rsquo;s connected to, planning, but In many cases, when we&rsquo;ll discuss about causes why we have big PRs, it&rsquo;s connected to how we plan things, how we did some shortcuts maybe with planning, and I think often big PRs are connected to some surprises that we are creating. So what I mean by that is that&hellip;</p> <p><strong>Miłosz</strong> [17:37]: So-called missed requirements?</p> <p><strong>Robert</strong> [17:41]: Yes, yes.</p> <p><strong>Miłosz</strong> [17:51]: If we don&rsquo;t know some edge cases before we start the work, then you&rsquo;re in progress of working on a PR, you discover something new, you have to stop, maybe discuss with someone else,</p> <p><strong>Miłosz</strong> [18:05]: and then it starts to get tough to push through. Because you put some work, you started some writing some code already, it&rsquo;s half done, but something blocks you.</p> <p><strong>Robert</strong> [18:19]: Yeah, and obviously, if you include something like that in the PR, if somebody will be reviewing that, it will be kind of surprising. Even if you have some planning process at the beginning, if some person is reviewing your PR, this person might be surprised. I mean, okay, we&rsquo;re planning this, but why does PR have this, this, that, and this? And this is triggering discussions. and discussions is triggering a slowdown of PRs, basically.</p> <p><strong>Miłosz</strong> [18:50]: Yeah. Sometimes, if PRs get reviewed slowly in general in your team, you may think, like, no, it makes no sense to&hellip; What happened?</p> <p><strong>Robert</strong> [19:08]: Oh our camera died.</p> <p><strong>Miłosz</strong> [19:12]: Yeah, so let&rsquo;s maybe take a minute break.</p> <p><strong>Robert</strong> [19:20]: It&rsquo;s tough. Yeah. So, this episode is special in a couple dimensions, because the first fact is that our room that we usually are using is used, so we were not able to use that. And our camera is overheating, so&hellip;</p> <p><strong>Miłosz</strong> [20:01]: Great. Summer is there.</p> <p><strong>Robert</strong> [20:02]: Summer is there. Cool.</p> <p><strong>Miłosz</strong> [20:04]: So, as I said before&hellip;</p> <p><strong>Robert</strong> [20:06]: What do you see? It&rsquo;s live. if it&rsquo;s not pre-recorded.</p> <p><strong>Miłosz</strong> [20:10]: So if your PRs get reviewed slowly in general in your team, you might think like, okay, it makes no sense to quit additional PRs because I will wait for all of them for a long time to be reviewed. Maybe let&rsquo;s just make a bigger run and review it once.</p> <p><strong>Miłosz</strong> [20:30]: But again, the huge PR gets reviewed even slower. So it&rsquo;s not a good solution really.</p> <p><strong>Robert</strong> [20:40]: And I think it&rsquo;s also one anti-pattern that I often see. It&rsquo;s the teams that are not real teams. What I mean by that is that if you have a couple developers that are independently working on some features, they are actually not a team. They are a set of individuals working on something. And at the end, there is no shared responsibility to deliver some features. So, if you have no shared responsibility to deliver features, it&rsquo;s hard to convince somebody to read your PRs, basically. Because nobody cares.</p> <p><strong>Miłosz</strong> [21:16]: It&rsquo;s not their job.</p> <p><strong>Robert</strong> [21:18]: My job is to implement my features and save you.</p> <p><strong>Miłosz</strong> [21:22]: I don&rsquo;t want to spend time on your work, because I&rsquo;m responsible for mine and there are stakeholders waiting for my work to be done.</p> <p><strong>Robert</strong> [21:32]: Exactly, exactly. And I think it&rsquo;s a big problem, because if you are not working as a team, but a set of individuals, well, there is no pressure or nobody feels obligated to review stuff.</p> <p><strong>Miłosz</strong> [21:50]: Yeah, so in this case, review is not the team&rsquo;s responsibility. Basically, no one&rsquo;s responsibility. Just beg people to do it for you. That sounds like an issue. You can see the setup when there&rsquo;s a feature mentioned and some stakeholders know to which developer to talk to about it. Because they are seen as owners or masters of this area. This is one of the indicators, I think, that this happens.</p> <p><strong>Robert</strong> [22:33]: We already covered a couple reasons why having big PRs is a problem and how big PRs happen. I&rsquo;m not sure if you agree with me, but I think one of the most important parts here and reasons for them is lack of planning, basically. Maybe I would say deep enough planning. I like the phrase that five days of coding can save one day of planning. And I think it&rsquo;s often true and it&rsquo;s often a problem in multiple teams. I mean, we&rsquo;re sort of developers, engineers, we like to solve problems with the code. So if we hear about planning, meetings, no, no. Yeah, yeah. It&rsquo;s always like, no, no. Yeah, yeah. Talking to people, especially non-developers. We are pretty allergic to that.</p> <p><strong>Miłosz</strong> [23:27]: Especially after you hear about the future, you already have a database schema in your mind. You already know how to solve it. All you need to do is just sit down and write.</p> <p><strong>Robert</strong> [23:40]: Yeah, and I kind of understand why people are allergic to deep learning, but from other sides, I think it&rsquo;s also creating a lot of problems. And often it&rsquo;s visible in CodeReview, because I think a lot of people are blaming the CodeReview process for many things like code review is such bad, it&rsquo;s such a waste of time, it&rsquo;s taking that much time. But I think in previous episodes, we also sometimes had this, let&rsquo;s say, non-direct, visibility of some errors. So for example, some people are blaming DII for making complexity of the applications because dependency injection is so complicated, etc.</p> <p><strong>Miłosz</strong> [24:30]: So it&rsquo;s like the tip of the iceberg. Yeah. Everything is the issue, but underneath there&rsquo;s some more complex issues.</p> <p><strong>Robert</strong> [24:36]: And I think it&rsquo;s a similar case with code review. A lot of people blame code review for a lot of bad things, but I think it&rsquo;s just surfacing other problems that you have in the team. For example, if you have, I don&rsquo;t know, 200 comments in the code PR that you submitted, Probably a good question to ask. Okay, why you have 200 questions there? Why you didn&rsquo;t agree on that earlier before you started implementation? Because when it&rsquo;s already implemented, reverting that, changing that, it will be super expensive. If you spent more time a bit earlier during planning and planning properly, probably you&rsquo;ll waste much less time. And I think it&rsquo;s not about always planning everything for 100% to not have an unexpected complexity, because it will always happen. And trying to plan that accurately, it will be super expensive in terms of time. But I think it&rsquo;s, from our experience, we see that spending more time on planning, it can definitely&hellip;</p> <p><strong>Miłosz</strong> [25:47]: It&rsquo;s easy to change the whiteboard. you erase it and rewrite it. It&rsquo;s much harder to change a 10,000 long 10,000 lines long PR with 100 comments. The earlier you discuss things and agree on how to do it, then the easier you iterate this.</p> <p><strong>Robert</strong> [26:11]: I know that a lot of people are like, let&rsquo;s skip planning, let&rsquo;s just write that and ship that and it will be everything fine. And I know that I was also like that when I was younger and less experienced, but later you can kind of learn that, okay, it can give you some acceleration at the beginning, let&rsquo;s say, but later it can be very problematic. Because, again, you lose many of the advantages that you will have from this code reveal. Knowledge sharing, or if you are doing a kind of more strict process of code reveal, it will be just blocking at some stage, because this stupid approach will be at some point, probably will go with some strange direction that you didn&rsquo;t agree with somebody else, and it will be just surprising the PR, the surprise that we were mentioning a bit earlier. And again, PRs shouldn&rsquo;t be surprising, so it should be implementation of what you agreed on high level earlier.</p> <p><strong>Miłosz</strong> [27:16]: Yeah, so it&rsquo;s another paradox. You save time on planning, but in the end, you spend even more time on the reviews. Just like with the paradox of context switching, when you think smaller PRs cause context switching, but actually the big PRs cause even worse.</p> <p><strong>Robert</strong> [27:40]: And yeah, I think it&rsquo;s interesting because on the first side, my thing that, yeah, splitting to multiple PRs, it will just add a lot of overhead and it sounds not logical to have multiple PRs instead of one big, but I think what I can recommend is trying to try using smaller PRs and see how different it will be. Obviously, it requires a team culture that supports that, because if you create 10 smaller PRs and nobody will review it kind of immediately, it will not help. And you cannot do much about that, and probably you will end up with big PRs at the end anyway, because it will just start to stack. So yeah, it wouldn&rsquo;t make sense, but we&rsquo;ll go into that, what you can do in the team to encourage teammates for a quick review.</p> <p><strong>Robert</strong> [28:40]: So, yeah, we started to mention that missing enough planning obviously can help to create bigger PRs. But again, it&rsquo;s also important to not go into extreme, because I think it&rsquo;s also easy to go into really extreme and try to plan for everything, to go really deep into the planning. But it will be very expensive to do. I think it&rsquo;s probably not a possible event because without touching the code it&rsquo;s really hard sometimes to find all edge cases and complexities that you have.</p> <p><strong>Miłosz</strong> [29:16]: The balance is in deciding what you want to agree on. For example, we skipped on most implementation details like exact database schema or naming of things. That was up to the implementer most of the time. High-level architecture, like what endpoints do you want, what services this impacts, and so on. Some very high-level picture of it, that&rsquo;s super useful to have early.</p> <p><strong>Robert</strong> [29:47]: I think it&rsquo;s all about this non-surprising PR at the end. To think what you should plan earlier, to not surprise the reviewer when you are reviewing that. And if you were not working like that earlier, obviously, it will take some time to learn how to do that. But I think it&rsquo;s a skill that you are just getting with time. You&rsquo;re implementing more, you&rsquo;re learning more how, okay, this is surprising somebody in the PR. Okay, maybe we should raise it during that.</p> <p><strong>Miłosz</strong> [30:20]: Next time, let&rsquo;s agree with what endpoints they will be exposed. This is a quick discussion, right? Of course, it&rsquo;s better to have before any code is written. Like, do we want endpoint in this service, or do we want a message handler in this service? High-level, that can take 5 minutes. But after someone implements it, we can make one hour to revert and replace it. Or even worse, if the discussion is asynchronous.</p> <p><strong>Robert</strong> [30:52]: Another thing that it&rsquo;s worth doing is also including in the planning discussion some product person, let&rsquo;s say. So it depends on each company how it looks like, but often you have some, maybe not stakeholder, but maybe some product owner or product manager or whatever. And it often may be helpful. So obviously it&rsquo;s required not to have some discussions that are not super technical because this person will be just bored and this person will not understand anything.</p> <p><strong>Miłosz</strong> [31:23]: So they will do it first. First understand the product.</p> <p><strong>Robert</strong> [31:28]: But I think it&rsquo;s nice because it also can help to cut scope a lot. So I remember that And often when we are doing it like that, we included product people during the planning phase. So when we have some planning meetings and we have this product person in the meetings, it sometimes helped a lot to simplify scope. Because we started to discuss some features, but we had like, okay, it will be very complicated because this, this, and this. And the person in the same room said, okay, so if it&rsquo;s complicated, we can just keep that because it&rsquo;s not that important. And sometimes you can lose hours as a developer to try to figure out some super complex problem, but at the end, nobody will care that much.</p> <p><strong>Miłosz</strong> [32:11]: I think it&rsquo;s one of the most important skills for product engineering, and I think it&rsquo;s super rare as well. Because most developers, when they get a clear description of a problem from a stakeholder, will just say, okay, we can do it, because they want to be the hero and show, and we&rsquo;ll be like this.</p> <p><strong>Robert</strong> [32:32]: And we are problem solvers, so if we see problem, okay, we need to solve this problem, it will be so cool.</p> <p><strong>Miłosz</strong> [32:38]: Especially if it&rsquo;s well defined, if you have all the requirements and so on, you might be eager to just jump into it. You will tell them, okay, we can do it tomorrow, no problem. They will be like guys you are the best team in the company but then you get stuck on these complex cities and you need to talk to the stakeholders again and it takes a long time to read about everything So, I think it&rsquo;s a super important skill to be able to pull them early to the discussion and see where you can cut the scope. Or, even if it&rsquo;s not out of scope entirely, just find the V1 you want to deploy. And that&rsquo;s super useful to have.</p> <p><strong>Robert</strong> [33:28]: And I think we&rsquo;re often kind of missing this, that we are implementers of some</p> <p><strong>Robert</strong> [33:35]: external requirements and again, we&rsquo;re trying to be heroes or.</p> <p><strong>Miłosz</strong> [33:39]: I am just a developer tell me what to do and I will do it so that&rsquo;s a common approach and E-definance because you can just trust people know what they want to help them figure out what exactly it is they want so basically,</p> <p><strong>Robert</strong> [34:01]: One unexpected strategy to make your PR smaller, implement less things.</p> <p><strong>Miłosz</strong> [34:08]: Yeah, or start with less. That&rsquo;s maybe more precise. Try to start with a small scope.</p> <p><strong>Robert</strong> [34:17]: But sometimes it&rsquo;s not possible. I mean, sometimes we&rsquo;ll ask product master or camera, right again. So, okay, can we simplify that? No, you can. So, what&rsquo;s the strategy in this case?</p> <p><strong>Miłosz</strong> [34:34]: I guess, breaking down changes, right? So, our ideal scenario would be having PRs that are implemented within one day. So, as you said at the start, someone posts a PR in the morning, it is merged on production in the afternoon. Something like this. This cycle time would be ideal.</p> <p><strong>Miłosz</strong> [35:09]: And there are actually some strategies you can use.</p> <p><strong>Miłosz</strong> [35:17]: Probably the simplest one is to merge code that&rsquo;s not ready yet. behind the feature flag. But it&rsquo;s critical that you&rsquo;re able to manage feature flags in your project one way or another, even if it&rsquo;s just some configuration. But it allows you to merge the PR, so it&rsquo;s already on production, even if it&rsquo;s used. But you avoid these discussions and conflicts, and so on. We just work on the code that&rsquo;s already on the main branch. Even if not exposed yet. And you can have the tests running, you can enable it on some pre-production environment, you can show it to stakeholders in some very early form, some MVP form, and get some early feedback from them. So that would be my number one for breaking down changes and for small PRs.</p> <p><strong>Robert</strong> [36:30]: Another thing that comes into my mind, it sounds pretty obvious, but basically to have some discussion to some RFC, ADR, some design document, and basically discuss it with the rest of your team and have some plan how we can lead it to some smaller parts. We already mentioned it multiple times. So, I think this magical value is one day that I know for so many people, it sounds like, no, it&rsquo;s not possible to start implementation in the morning and merge something by the end of the day and not working until midnight. I mean, I know that for some people it sounds crazy, but it&rsquo;s very possible.</p> <p><strong>Miłosz</strong> [37:14]: Yeah, I remember having five minutes cycle time. If it&rsquo;s small, you can just ping someone and then take a look and in 10 seconds you get an approve.</p> <p><strong>Robert</strong> [37:26]: And the rest of the people is aligned because you spent some teamwork to discuss how it should be implemented. And it&rsquo;s not surprising for people. So somebody can take a look quickly on that. See, ah, yeah, it&rsquo;s what we discussed. Looks great. Merging.</p> <p><strong>Miłosz</strong> [37:43]: And because it&rsquo;s merged, for the next PR that&rsquo;s built on top of this, Those changes are already out of context, so you don&rsquo;t spend time discussing them, but that&rsquo;s your dream.</p> <p><strong>Robert</strong> [37:57]: Do you have maybe some tips from our experience, how you can split this work</p> <p><strong>Robert</strong> [38:01]: to make it working nicely?</p> <p><strong>Miłosz</strong> [38:05]: Yeah, so you can either split vertically or horizontally. And both kind of make sense. Probably vertically makes more sense at first, so instead of big chunks of the feature, you&rsquo;ll deploy small parts of it. So for example, you can&hellip; Let&rsquo;s say if you beat a dashboard or something like that, you can deploy just one chart or one button. And the endpoints behind it and so on. So it&rsquo;s something you can deploy, for example, on staging environment or even in production. And show it to your stakeholders and ask them, is this what you want? It can be a very simple feature, part of the entire feature, but it&rsquo;s already something you can ask them about.</p> <p><strong>Robert</strong> [39:03]: Yeah, and I think you can go even one step further because, okay, you can have some vertical slice to prepare a chart, but often though vertical slices of entire functionality, it can be still pretty complex. You can also slice this vertical slice horizontally. For example, have separate tasks for SQL repository. So it works pretty nicely with clean architecture that we&rsquo;ve been discussing in the previous episode, so I recommended to check it out. What Longstar showed is that it helps pretty nicely with splitting these features layer by layer. So it can be implemented by one person or by multiple people, and you can implement&hellip; Again, it&rsquo;s good for more complex features, because for some features it doesn&rsquo;t make sense to split that because it will just use a solar head. But if it&rsquo;s a more complex feature, let&rsquo;s say that it&rsquo;s one week work for one person. You can always split it by layer. And for example, every person in the team can take one layer. And instead of implementing something for an entire week, you can have five people that are just separate slices with a grid API layer, and it can be even implemented within a day. And it&rsquo;s cool, because you can show it to some stakeholders after this day and ask, okay, this is something but yeah? No? Okay, so let&rsquo;s iterate on that. And it&rsquo;s much, much, much faster.</p> <p><strong>Miłosz</strong> [40:24]: Yeah, I think it&rsquo;s another superpower that&rsquo;s very rare in tech teams in general. It gave you to work in parallel on a single feature. More often you will find this approach of, okay, so this person is responsible for this feature, see you again in two weeks on the Spring demo.</p> <p><strong>Robert</strong> [40:41]: And usually it&rsquo;s not two weeks because there are some challenges or block errors.</p> <p><strong>Miłosz</strong> [40:46]: So instead, if you can agree on the interfaces, then one person can implement the HTTP handler, the other one the message handler, the third one the database adapter, something like that. And it&rsquo;s quite simple, because you agree on the interfaces first, and then you implement them. It&rsquo;s really straightforward, and we know it works because we&rsquo;ve seen it live.</p> <p><strong>Robert</strong> [41:11]: And we have some comparisons. It was a process that we worked over a time, and we have comparison how it worked earlier, when we were not doing splitting, and later, when we did the splitting, and we were able to iterate that fast, and it was cool.</p> <p><strong>Miłosz</strong> [41:27]: Except both vertical and horizontal, splitting is not that easy. Maybe the horizontal is easier because it&rsquo;s a technical one, but the vertical split is related to product so you need some product sense to understand which features make sense at all and you need to discuss it with someone probably to understand better Oh no,</p> <p><strong>Robert</strong> [41:50]: Talking to people.</p> <p><strong>Miłosz</strong> [41:51]: I think it&rsquo;s worth doing I&rsquo;m just saying, I think it&rsquo;s rare because it&rsquo;s not trivial you need some effort from the engineers to do it</p> <p><strong>Robert</strong> [42:01]: But after you learn that, and the team will learn that, it&rsquo;s&hellip;</p> <p><strong>Miłosz</strong> [42:05]: That is quite natural, yeah. For sure you can learn it. The entire teams can learn it. And yeah, it will be the best if, side note, there&rsquo;s no single team lead doing this, but it&rsquo;s encouraged so everyone can own this with time.</p> <p><strong>Robert</strong> [42:25]: And I think it&rsquo;s another, we again see the same pattern, that it&rsquo;s some kind of overhead myth here. You may think that, okay, cutting it by horizontally and vertically, it&rsquo;s that much overhead for that. But it&rsquo;s not logical that in theory it will require more time. But from the other side, if you will not cut that and one person will implement that, for some magical reason, it will take longer anyway at the end, even if there is some overhead by cutting that. But if only one person is implementing that, this person may be bored, may have other interruptions, context switching. So at the end, there is also some hidden cost of one person doing all entire work for two weeks. And if you add all the complexities that we mentioned with the non-splitting PRs, but having one big PR instead, it&rsquo;s starting to be much, much, much harder and take much more time. And again, at the first sight, it may sound like it&rsquo;s requiring much more effort, but I recommend to check that because after you see that, you can see that in practice, it&rsquo;s much, much faster. the planned things.</p> <p><strong>Robert</strong> [43:51]: But you may think that, okay, it all sounds nice, but it&rsquo;s not possible to split all functionalities that are maybe okay. Those functionalities are a bit easier. You can probably split most of them, but sometimes there may be some work that is not possible to split. And the first thing that I have in mind is some kind of big refactoring. So, if you&rsquo;re doing some big refactoring in a project, sometimes it may be hard to split that. So, I like to compare it to Fatality from Mortal Kombat. So, you know, it&rsquo;s like you&rsquo;re getting backbone and, you know, it&rsquo;s getting all the bones.</p> <p><strong>Miłosz</strong> [44:35]: So, there are lots of unknowns. You just don&rsquo;t know what you will find.</p> <p><strong>Robert</strong> [44:39]: Yeah, but I also mean that sometimes touching one thing is creating some kind of domino effect. So you touch one thing, but to make it work, you need to adjust a lot of things in the project and need some kind of atomic change at the end. So if you touch that, you probably need to spend one week to just fix everything. And it&rsquo;s, I think, also a pretty interesting case how to deal with that. Because, OK, this one person can do this big refactoring for a week or two weeks. Later, give a PR and, guys, can you review that? Because, you know, it&rsquo;s important refactoring. If you are looking at, hmm, okay, two weeks of work, 2,000 files changed, 200,000 lines of code added, the same removed, okay, good luck reviewing that. So, one of the tactics here may be, okay, it just, okay, sounds good, let&rsquo;s merge that. But I think it&rsquo;s not the best, because usually you can end up with some brain changes in the code base, and everybody later will be, maybe not surprised in the PR, but surprised if somebody will be, because I&rsquo;ll take something like that.</p> <p><strong>Robert</strong> [45:48]: So, do you have some examples from our teams? What we did in such scenarios to avoid this? I think probably it happened to us multiple times to do this big PR and close eyes and okay, let&rsquo;s merge that, whatever. But I think it maybe wasn&rsquo;t a disaster, but it wasn&rsquo;t the best strategy.</p> <p><strong>Miłosz</strong> [46:11]: You can try mob programming.</p> <p><strong>Robert</strong> [46:13]: Sounds like an app.</p> <p><strong>Miłosz</strong> [46:14]: If you are brave.</p> <p><strong>Robert</strong> [46:17]: So, what is mob-programming? Because I think a lot of people may not know mob-programming. So maybe some of you know what spur-programming is, but mob-programming, I think it&rsquo;s much more exotic.</p> <p><strong>Miłosz</strong> [46:30]: It&rsquo;s spur-programming, but more than a pair. It&rsquo;s basically a group of people coding together, and one person is the driver with the keyboard, and the rest comment and observe. It&rsquo;s very similar to per-programming except there&rsquo;s an entire team, if it&rsquo;s small enough. Like people, I&rsquo;d say. And yeah, and you take turns coding and implement the feature together.</p> <p><strong>Robert</strong> [47:02]: Sounds like a big waste of time, isn&rsquo;t it?</p> <p><strong>Miłosz</strong> [47:06]: Yeah, like all meetings. I think we should all code alone,</p> <p><strong>Robert</strong> [47:12]: Not discuss it in the basement.</p> <p><strong>Miłosz</strong> [47:16]: Yeah, so for many, maybe not for many, but for some kinds of work, this can work well. Because of the knowledge sharing, primarily. So you don&rsquo;t need to waste time reading PRs, especially if they are big, because we do some refactoring or scaffolding of the project and so on. Instead you do it together. So for sure it doesn&rsquo;t make sense to do everything like this. If you implement an HTTP handler, there&rsquo;s probably no need for four people to watch how you do it. But if it&rsquo;s some critical domain code or some huge refactor, it&rsquo;s probably quicker this way than if everyone reads the PR separately and comments.</p> <p><strong>Robert</strong> [48:06]: I think it&rsquo;s probably good to consider alternatives, if you are thinking that it&rsquo;s a waste of time. Because, okay, what are alternatives? So you can probably just close eyes and merge these big refactoring PRs. It&rsquo;s some solution, but usually it doesn&rsquo;t end up well. Sometimes, okay, the person that created this big PR can arrange some meeting and show some highlights of how this feature or refactoring was done, it&rsquo;s probably a bit better, but still, it&rsquo;s no way that you can go over, entire thinking process that you did in one or two weeks of development.</p> <p><strong>Miłosz</strong> [48:50]: So you need to consider if it makes sense.</p> <p><strong>Robert</strong> [48:53]: But do you have maybe some tips how to run more programming sessions to keep it efficient? Because you have an entire team in the room, potentially, maybe on the call. I think it&rsquo;s harder. I mean, I think it&rsquo;s easier to do it in person, but&hellip;</p> <p><strong>Miłosz</strong> [49:12]: Everything is harder on the call. So watch out. But yeah, if you do timebox that should help, so make sure there&rsquo;s no one-person coding, but you switch seats.</p> <p><strong>Robert</strong> [49:24]: Yeah, I think it&rsquo;s very important because it&rsquo;s easy to end up with a situation when one person, sometimes some team lead or whatever, is coding everything and everybody is looking on their phones to update the code because it&rsquo;s&hellip;</p> <p><strong>Miłosz</strong> [49:38]: So I need to keep everyone engaged. Don&rsquo;t speak away. Watch out for CO2 levels in the room if you use a room make sure your team knows the process and everyone is aligned for example a big risk is if you let people drift into a topic and start discussing every line in the code that can end up terribly I think it works best if your team has been working together for a while. They know each other and I remember we had sessions with some new joiners to the team and that wasn&rsquo;t going that well because they were not aware of some patterns we were using. They questioned everything.</p> <p><strong>Robert</strong> [50:37]: Yeah, so I think it&rsquo;s good to have some facilitator.</p> <p><strong>Miłosz</strong> [50:41]: Exactly, you need some heavy facilitator. We can cut discussions.</p> <p><strong>Robert</strong> [50:46]: I remember that what often works for us was that, okay, if we&rsquo;re parking some discussions, sometimes we left a task for that, and this task could be done asynchronously. Because, again, running a session for an entire team is just expensive in terms of time, basically. And you need to be sure that you are using this time very efficiently.</p> <p><strong>Miłosz</strong> [51:16]: So, sometimes you can also do it with a short session just to start a project. I remember when we started new services, we just use Mood Programming, where we just bootstrap the project or something like that.</p> <p><strong>Robert</strong> [51:30]: Yeah, and it was pretty cool because we had really nice boosts at the beginning, so everybody was aligned. how it was implemented, and it was speeding up work at the beginning very, very much. Because, again, it&rsquo;s also the theme that is going over and over during this episode, so knowledge sharing. So this very deep knowledge sharing that actually accelerates the work a lot. Because you may have multiple tasks to implement, everybody knows how everything else in the project works, so everybody can pick up the task and tasks are not blocked because, for example, this task can be unimplemented by somebody. Everybody can take that, everybody can review that, and it&rsquo;s accelerating work a lot. That&rsquo;s kind of hidden functionality of that, let&rsquo;s say. But yeah, it can help a lot. And yeah, this is also the same with mob programming. So basically you can mob program the feature at the beginning of the project and and later iterate really, really fast.</p> <p><strong>Miłosz</strong> [52:38]: Maybe we can talk about code review culture, going back to the topic.</p> <p><strong>Robert</strong> [52:43]: Yeah, because I think it&rsquo;s pretty important because a lot of ideas that we have here, it requires some nice culture in the team. Because again, you can do small TRs, but if nobody will review them, it&rsquo;s pointless because you end up with waiting for ages for the reviews, so it will be not helpful. So what do you have in mind from our experience? What are the important traits of the team that have high review culture?</p> <p><strong>Miłosz</strong> [53:19]: Maybe what you mentioned before, making the team responsibility, not individual responsibility. The team is responsible for delivering the entire feature product. Anyone should be able to review code for knowledge showing. Avoid the silos where people are closed in their own parts of the code and they know everything about it. No one else is able to suggest changes there.</p> <p><strong>Robert</strong> [53:52]: And by everybody we mean that, okay, if you have 15 people team, or maybe not entire team, but you can have some&hellip; Okay, it&rsquo;s really problematic if your team is 15 people.</p> <p><strong>Miłosz</strong> [54:03]: I guess ideally we have two or three people working on a single feature at a time and they can review each underscore.</p> <p><strong>Robert</strong> [54:11]: I would even say that maybe five. Five is also good because you can have some extra capacity for that. And it&rsquo;s a number out of an hour.</p> <p><strong>Miłosz</strong> [54:22]: Yeah, but I mean, it&rsquo;s probably an anti-pattern to have one person responsible for all the reviews. So like the leader with the grey beard who knows everything and everyone just asks them to review their code. That&rsquo;s probably not what you want, because this person becomes a bottleneck.</p> <p><strong>Robert</strong> [54:44]: It&rsquo;s also not good when you have your buddy, for example, that is doing your reviews. because it&rsquo;s a bit better, because it&rsquo;s not in the hands of one person. By the way, I think many of you may not know the Phoenix Project book. I don&rsquo;t remember the name, but it was covering a similar situation of one person that was the person that known everything, but it was also a bottleneck at the end. So it&rsquo;s a very similar situation.</p> <p><strong>Miłosz</strong> [55:16]: I would usually just ask the person who has more experience in the area than I have. So, they can give you some insights on the way that or maybe</p> <p><strong>Robert</strong> [55:25]: Just have a team channel and.</p> <p><strong>Miłosz</strong> [55:28]: Oh yeah</p> <p><strong>Robert</strong> [55:30]: And pick up it and yeah.</p> <p><strong>Miłosz</strong> [55:32]: Yeah, that&rsquo;s yeah, that&rsquo;s the best thing times, I remember. What I mentioned about the 5-minute cycle time is when you just post a PR, post a link to some Slack channel and then you get an approval for comments 10 minutes later. That was my ideal working condition.</p> <p><strong>Robert</strong> [55:56]: Yeah, it&rsquo;s possible. It requires times and good culture.</p> <p><strong>Miłosz</strong> [56:03]: I think the main concern is there is context switching. But once again, switching context to a 50-line long VR is something completely different than 500-line VR.</p> <p><strong>Robert</strong> [56:16]: Or even 200 lines. I mean, it&rsquo;s also something that you can review pretty quickly.</p> <p><strong>Miłosz</strong> [56:21]: Yeah, I just mean the size in general. If it&rsquo;s in tens of lines, hundreds of lines, probably still kind of fine, but if it&rsquo;s thousands, then yeah, probably someone will open it and then they will go for a coffee break or back to their own code and we&rsquo;ll just make a mental note to come back to it later. If it&rsquo;s 50 lines or 100 lines, they can just read it in one minute and tell you what they think.</p> <p><strong>Robert</strong> [56:54]: I have actually one thing in mind when we&rsquo;re discussing that, that one of the problem with the discussion that we have now, it&rsquo;s that having this so having everything working requires multiple things. For example, if one thing doesn&rsquo;t work, it can break the good PR flows. For example, if, for example, it takes ages for people to pick up PRs, so everything else can be problematic. So it&rsquo;s important that to not pick some parts of this, but try to, improve it from multiple angles. Okay, we&rsquo;re going back to the strong code review culture. So other thing that it&rsquo;s maybe also not obvious is actually how we are doing reviews. So I have two things in mind.</p> <p><strong>Robert</strong> [57:46]: So the first is the feedback culture and feedback mindset. So you should not try this last.</p> <p><strong>Robert</strong> [57:59]: You should not take this code review feedback personally, let&rsquo;s say, because I think it&rsquo;s often that people are looking like, oh, somebody commented on my code, I&rsquo;m so stupid, and this person is also stupid because he doesn&rsquo;t like me or whatever. So, try to avoid that and try to separate that thought. This is the cult, this is you, and this is a separate thing. It&rsquo;s also always good to assume good intentions, because it&rsquo;s easy to go in a direction like, ah, somebody is commenting on that, would like to make my life harder, and why he&rsquo;s doing that. And I think it&rsquo;s very rarely true, so most often it&rsquo;s that actually somebody cares on the other side, and this person would like to create code that is maintainable and easy to work with. So I think it&rsquo;s important to start with mindset by default that the person on the other side have good intentions. So this person cares and this person would like to just have nice code. Obviously, sometimes it&rsquo;s going into extremes. Sometimes the person can have also some bad intentions, but it&rsquo;s more about the default approach so I think by default and it will be probably true in 80% or 90% cases that people have good intentions unless.</p> <p><strong>Miłosz</strong> [59:19]: They need to pick yeah</p> <p><strong>Robert</strong> [59:23]: And I think it&rsquo;s another problem so if you don&rsquo;t know what nitpicking is so you know it&rsquo;s all those comments like okay it&rsquo;s not important but maybe you should use spaces here or, change how it&rsquo;s formatted to a different&hellip;</p> <p><strong>Miłosz</strong> [59:41]: Can you order the variables alphabetically?</p> <p><strong>Robert</strong> [59:46]: Yeah, yeah. And all those comments that are&hellip;</p> <p><strong>Miłosz</strong> [59:50]: Basically, I would like this code to look like this, but you do it differently.</p> <p><strong>Robert</strong> [59:55]: So you can resolve this comment. I don&rsquo;t mind. But, yeah. So this is nitpicking, long story short. And in my opinion, If I do a code review and I think about adding a nitpick, I don&rsquo;t do that. Because if it&rsquo;s not that important that it&rsquo;s not required for merging, it doesn&rsquo;t make sense actually to add any comment, spend other person&rsquo;s time on resolving that or maybe addressing that, because it&rsquo;s just not important. In my opinion, all those nitpicks should be covered by the Linter, basically. So if you have some styling requirements, just put Linter for that and not spend human&rsquo;s time.</p> <p><strong>Miłosz</strong> [1:00:46]: Because they are often cosmetics, so our Linter is good at catching that.</p> <p><strong>Robert</strong> [1:00:50]: Exactly. And Linter have also one nice feature that it has a pretty quick feedback loop. Because if you are leaving a comment in the PR, some person will read it with some delay, some person needs to address that. It&rsquo;s just a lot of waste of time. And with Linter, you&rsquo;re just running Linter. You can see, in the worst case, a couple of seconds later. In the best case, in less than one second, that everything is fine or what should be fixed. You can format that and it&rsquo;s fine. So I think this is how it should be solved with NitPix. So if something is that important enough to check for that, fine. Let&rsquo;s put linter for that and linter will ensure. If it&rsquo;s not important, why to do that?</p> <p><strong>Miłosz</strong> [1:01:34]: Makes sense. Also, as the person submitting the PR, you can do some things to make it easier. Something I like to do is being my first reviewer. Just checking the trendset and often you will find some leftovers. Instead of someone commenting it for you, you can just do it yourself. Make their job a bit easier. and they can also include a short description, like why you did something. We sometimes comment on the onlines ourselves just like a heads up for the reviewer like, you know, I know this can look strange, but I did this because of something.</p> <p><strong>Robert</strong> [1:02:21]: Or even better, leave some comments in the code that explains why it&rsquo;s done in some way. And I think it&rsquo;s important that you mention that to describe why not how. Because how it&rsquo;s written, what&rsquo;s written there, is visible in the code. I mean, it is the source of truth. But why? It&rsquo;s not in the code. You can only see how it&rsquo;s done.</p> <p><strong>Miłosz</strong> [1:02:44]: You generate the how with AI. Summary, but it&rsquo;s not really the point. The point is to give some extra context, not what is already there, but different words.</p> <p><strong>Robert</strong> [1:02:54]: Exactly.</p> <p><strong>Miłosz</strong> [1:02:56]: Maybe one other tactic is draft PRs. If you have some very rough idea, especially if it&rsquo;s something experimental or something your team didn&rsquo;t do before, I would create a draft PR and very quickly, in a sloppy way, share with everyone and ask them to give me some first impressions.</p> <p><strong>Robert</strong> [1:03:20]: Especially if you discovered something unexpected in a PR. So remember, don&rsquo;t surprise the reviewer, and if you feel that, okay, this may surprise somebody, yeah, good place for a draw.</p> <p><strong>Miłosz</strong> [1:03:34]: This works only if you have this early feedback culture. You can get someone&rsquo;s comments in 15 minutes. I think it&rsquo;s way better to discuss it early, because if you go deeper into this and you have more tool you work, if you have some insights. And if you ask them early, it&rsquo;s even better to have some quick meeting to align and agree and then implement.</p> <p><strong>Robert</strong> [1:04:02]: It&rsquo;s also connected with this idea that you are working as a team, not as a set of individuals. So, you collectively care about this draft.</p> <p><strong>Miłosz</strong> [1:04:13]: And you have to see why we are in that. Yeah, but this needs to be in place because you will need to wait two days for our draft PR review. It doesn&rsquo;t make zero sense.</p> <p><strong>Robert</strong> [1:04:24]: Exactly. And I think it&rsquo;s actually something pretty important to touch on. So, what are the tactics to accelerate review cycles? Because I think it&rsquo;s a requirement for most of the things that we mentioned. So, if your review cycle is slow, it doesn&rsquo;t make sense to split your PRs to smaller PRs, because if you are creating 10 smaller PRs and you are waiting for them for one week, it&rsquo;s not a big difference between making one big PR&hellip; You&rsquo;re just stuck PRs needlessly. Yeah, probably in this case it&rsquo;s better to not split PRs and have big PRs that nobody will really review or create 200 comments. Probably it&rsquo;s better than having 10 PRs like that. Unfortunately, so I think this accelerating review cycles is key here and keeping this cycle time more is also critical. So one of the most important things and mindset to learn is that making reviews should be.</p> <p><strong>Robert</strong> [1:05:33]: We should prioritize making review, basically. So it&rsquo;s pretty compatible with the lean Kanban approaches.</p> <p><strong>Robert</strong> [1:05:42]: So basically the idea is that you should prioritize work to be done. So if you have something that is close to be done, PR, this is work that is almost done, let&rsquo;s say, you should prioritize that and try to get it out of your board, let&rsquo;s say, as soon as possible. And, It&rsquo;s a simple idea, but, again, very, very important.</p> <p><strong>Miłosz</strong> [1:06:06]: So, I guess, higher priority than full-in-progress, because it&rsquo;s closer to production.</p> <p><strong>Robert</strong> [1:06:11]: Exactly, exactly. So, obviously, it requires some context switching. So, if you&rsquo;re working on some feature and you see that there is some PR to be reviewed, obviously, you probably shouldn&rsquo;t, if you&rsquo;re working on something, jump straight into that and review that. But maybe there is some person that finishes the job and can jump in this PR.</p> <p><strong>Miłosz</strong> [1:06:37]: Yes, contact switching can be a problem, probably for some people more than for others, but you probably don&rsquo;t work eight hours looking at the code anyway. I mean, writing.</p> <p><strong>Robert</strong> [1:06:49]: You are not. We&rsquo;ll need to talk about it after.</p> <p><strong>Miłosz</strong> [1:06:52]: Maybe you&rsquo;re supposed to.</p> <p><strong>Robert</strong> [1:06:54]: We&rsquo;ll talk after recording about that. But jokes aside, yeah. And I think it&rsquo;s going back to the knowledge sharing. So we covered this knowledge sharing multiple times, and I know that some people are, why we need knowledge sharing? And this is actually a great example why it&rsquo;s important, because if you don&rsquo;t have only one person that is able to review your QR, but entire team can do that, you don&rsquo;t need to wait for this one person. You can probably have one person that finished his work or her work and can look into this PR, and it&rsquo;s making this acceleration working, basically. If you have only one person that can review your PR, obviously it&rsquo;s making the cycles much longer, because only this person, multiple stars in the sky need to go into a special&hellip;</p> <p><strong>Miłosz</strong> [1:07:48]: Into a line.</p> <p><strong>Robert</strong> [1:07:50]: Yeah, yeah, yeah.</p> <p><strong>Miłosz</strong> [1:07:54]: Yeah, and the end result is this cool flow of PRs going smooth from coach to production.</p> <p><strong>Robert</strong> [1:08:02]: Yeah, so basically you can work on something in the morning and by the end of the day, and you&rsquo;re not working for 18 hours, and it can be merged within this day. But again, it&rsquo;s important mindset shift because I know it&rsquo;s natural to focus on your job to be done. But again, try to focus on finishing work and work as a team, not set of individuals.</p> <p><strong>Miłosz</strong> [1:08:35]: That&rsquo;s a culture thing. Not easy to change, but helpful to have in place. There&rsquo;s also this kind of controversial topic of metrics. There are many tools that you connect to your GitHub and that you track things like average PR size, cycle time, timing review and so on. And they can be useful. I think there&rsquo;s a risk that someone uses them for measuring performance. That&rsquo;s probably not a good idea, because people will start gaming them and that&rsquo;s not what you want. Especially if you try to compare teams using them or something like that. I&rsquo;m not recommended, but you can use it inside the team for insight or maybe to find some outliers. If you have one or two huge PRs in one iteration, you can ask yourself why this happened. Maybe it was okay, maybe we could improve something.</p> <p><strong>Robert</strong> [1:09:45]: But I think it&rsquo;s also important to set expectations. So we said that, okay, in this ideal work, you are starting in the morning, merging by the end of the day. But also, to set expectations. In our teams, it also wasn&rsquo;t like that for every PR. So it was probably for 80%, sometimes 70% of PRs. But I think it&rsquo;s enough.</p> <p><strong>Miłosz</strong> [1:10:07]: So the target is not perfection, right? Yeah. Just that most work goes like this. Sometimes there will be some crazy refactor that you need to do at once. Maybe it&rsquo;s fine.</p> <p><strong>Robert</strong> [1:10:19]: Sometimes you probably should reflect that, okay, why this took longer, what we can do to improve that, and try to do some kind of continuous improvement. As we were already saying about fancy Japanese words, Aizen.</p> <p><strong>Miłosz</strong> [1:10:34]: So it can be a good starting point for retrospectives in discussion.</p> <p><strong>Robert</strong> [1:10:40]: Yeah, I remember that when we were really into optimizing that, we on the retrospectives were looking on these outliers and discussing, okay, why this PR took, for example, three days, for example, to be merged. And, okay, is this something that we can improve here? Okay, this PR was like that because, I don&rsquo;t know, it was some unexpected complexity there and, okay, it happens. Probably couldn&rsquo;t do much about that. It&rsquo;s fine. Or maybe we can see sometimes, okay, we didn&rsquo;t spend enough time on planning here. Maybe we can, for example, next time, plan more about this, because this, and just improve. And again, this is a skill. So if you work in such environment for a longer time, you&rsquo;ll also learn what things are worth to do deeper planning and work when it&rsquo;s just not productive. Oh, we have some comments. Hi guys, I came late, I have concern. How could you create small PRs if you are working on tickets? And maybe the ticket has complex tasks. How do you recommend doing small PRs in those cases?</p> <p><strong>Miłosz</strong> [1:12:04]: Complex task yeah if you can follow up what you mean by complex but two things we mentioned before so one is splitting the ticket into smaller tasks but by vertically by the product features</p> <p><strong>Robert</strong> [1:12:26]: Maybe it&rsquo;s the situation when you have tasks from somebody up and you are basically implementing the tasks that you are receiving from some product owner somewhere that is planning your work.</p> <p><strong>Miłosz</strong> [1:12:40]: Oh, yeah, right. I didn&rsquo;t consider this. Because it&rsquo;s going to be like this.</p> <p><strong>Robert</strong> [1:12:48]: I think the root cause, if I understand it properly, the root cause is probably that some person is planning your work, as long as I get it properly. But I think it&rsquo;s the root cause. I mean, it shouldn&rsquo;t be a process like somebody&rsquo;s giving you tasks that you should implement and deciding on how to state that. It should be probably the opposite. So you should discuss with this person, okay, what we have to do. So you should actually receive the problem that is to be solved, but not tasks that will be implemented, basically. And it should be some collective process to agree how to do that. Unfortunately, I know it&rsquo;s not possible in all companies, and it&rsquo;s sometimes impossible to change. And in this case, change companies.</p> <p><strong>Miłosz</strong> [1:13:36]: If you can split it, try more programming.</p> <p><strong>Robert</strong> [1:13:41]: Or maybe just split it internally, so you can have one task from somewhere else, do process yourself to split it to multiple smaller tasks, and just have smaller PR. So we already mentioned it a bit earlier, that you can merge code that is not functional yet, So it can work in the background, or maybe not. You may have some functions that are implemented but are not included in the production process, let&rsquo;s say. In the last PRs, for example, you can have multiple PRs, and in the last PR you can integrate everything. In the meantime, if something is much more complicated, you can also do the&hellip;</p> <p><strong>Miłosz</strong> [1:14:38]: It&rsquo;s kind of what you mentioned before,</p> <p><strong>Robert</strong> [1:14:41]: Right?</p> <p><strong>Miłosz</strong> [1:14:44]: Just because a task is complex doesn&rsquo;t mean you can&rsquo;t split it into small PRs. You can have the scaffolding first and then use feature flags to deplace on that code to production before you start using it. I&rsquo;m not sure if you&rsquo;ve listened the entire episode, but we mentioned this before.</p> <p><strong>Robert</strong> [1:15:06]: But yeah, the downside is that, again, it requires the entire team to kind of come into this approach. because if you have a long review cycle and you have many smaller PRs, it may be just blocked too long. You will not see the benefit of that. I also wanted to share that if you are splitting such tasks in more smaller tasks, if it&rsquo;s more complex, you can also run shadow mode before the last merge. So you can run new code along the old code and compare if this code is creating the same result without storing it to the database or something like that. So sometimes you may be doing some bigger refactoring or something like that. So maybe you have some tests for that, that you see that, okay, it&rsquo;s passing, but sometimes you would like to also compare on the production if it&rsquo;s working as previously, but without breaking anything. So you can run two functions like this alongside within one service and just compare result at the end. Run it for a week, month for a complex one, and if you see that the results are consistent and it&rsquo;s working nice, you can just swap that. But again, it&rsquo;s for much more complex tasks.</p> <p><strong>Robert</strong> [1:16:23]: Ah, okay, so I was right. The case is that tasks are assigned to me, and the tasks have goals. In some cases, I need to make changes in several repositories, and maybe I need to touch several files at once. So yeah, in this case, I&rsquo;ll say that this is probably the pattern that tasks are assigned to you. In my opinion, it should not work like this.</p> <p><strong>Miłosz</strong> [1:16:45]: So yeah, but what can you do in this situation?</p> <p><strong>Robert</strong> [1:16:49]: Yeah, yeah. So, as I said, unfortunately, in many companies, you cannot re-change that. So, the thing that you can do is you can split these tasks internally. And, again, it depends on the team. I know that sometimes it may be hard to change the entire team culture to smaller PRs. Because, okay, maybe you can split your tasks into smaller tasks and try to have smaller PRs, but if nobody will look in your TRs and it will be no teamwork, because, okay, I assume if tasks are assigned to you, it may be problematic, because you are not collectively working on that. It&rsquo;s your responsibility and it&rsquo;s harder to actually integrate with that. Sometimes, unfortunately, it&rsquo;s a bit hard to change the company culture.</p> <p><strong>Miłosz</strong> [1:17:35]: Yeah, but we can still do just this on the technical side.</p> <p><strong>Robert</strong> [1:17:40]: And I would say that, okay, sometimes it&rsquo;s maybe hard, but not impossible to change that. I mean, we&rsquo;ve seen sometimes in our life that the company culture was super hard to change, but if you push really hard, you can also do some positive changes.</p> <p><strong>Miłosz</strong> [1:17:54]: But if you can, you can do it also on the technical side of things. You can focus on the logic first and then implementation details or the other way around. The reviewers don&rsquo;t need to have the full context in their minds at the same time. They can do it in parts. It&rsquo;s much easier. So there&rsquo;s some impact you can make.</p> <p><strong>Robert</strong> [1:18:20]: We recommend checking what we&rsquo;ve mentioned a bit earlier. So we have multiple strategies and hopefully it will help. And again, try to change maybe something in your team. Maybe it will be possible. Good luck! But yeah, I think also don&rsquo;t be demotivated that it&rsquo;s so bad. Because from our experience also in the teams, when you need to do these changes and you need to push hard to do that, actually you can learn a lot. Because it&rsquo;s not like just, oh, let&rsquo;s do it like this. And everybody&rsquo;s happy. Yeah, it&rsquo;s too easy. When you are in the team that you need to re-spend a lot of time to convince your product owner, the rest of the team for that. It will be hard, but if you succeed, it will be much easier in the other teams and it&rsquo;s a useful skill.</p> <p><strong>Miłosz</strong> [1:19:11]: If you want to succeed, it&rsquo;s a change of the job.</p> <p><strong>Robert</strong> [1:19:15]: Yep, it&rsquo;s the last resort, but it often works. And if you succeed in this team, it will be also something nice to get a better job earlier. I mean, if I heard such stories like, yeah, I worked in the team when tasks were assigned to me and I convinced product owner to spend time together to split tasks. I convinced my team to split work into smaller. It was always like, okay, nice. It&rsquo;s so you know what to do and it&rsquo;s also helping to join nicer teams later. It&rsquo;s also a nice opportunity. You should try to see it in this way. All right.</p> <p><strong>Robert</strong> [1:20:02]: Other alternative that I have in mind that you can, Solve problem of big PRs is you know what. What do you mean? I see big PR, I close my eyes and yeah, legit to me, that&rsquo;s marriage death.</p> <p><strong>Miłosz</strong> [1:20:18]: That is good, yeah.</p> <p><strong>Robert</strong> [1:20:19]: I know that many teams are people who use this tactic and it can also work in some places. I mean, let&rsquo;s imagine that we&rsquo;re in a team, in some component that is fighting for survival and the long-term in this case of company is six months. And if they will fail, they will just fail and it will be closed. So obviously, you probably don&rsquo;t need to care that much about long-term maintainability because long-term may not exist, basically. But if you are in a more mature company or you are not trying to survive, and you know that maybe you are building these features and it&rsquo;s very likely that it&rsquo;s not the final, phase of this feature and you will need to change them. It&rsquo;s pretty likely if you are working in startup because very rarely the idea that you have at the beginning survives multiple iterations. So basically you need to be able to very quickly. In those cases, probably YOLO mode is not the best load.</p> <p><strong>Miłosz</strong> [1:21:33]: It&rsquo;s a bit like we discussed in the first episode, when to write low-quality code. Sometimes it makes sense, but you need to consider if you are in this place or not.</p> <p><strong>Robert</strong> [1:21:45]: I think it&rsquo;s also connected to using some basic episodes and using some good practices. Basically, to skip some good practices, you need to understand what this good practice is giving you and what&rsquo;s skipping this good practice who gets all the gear.</p> <p><strong>Miłosz</strong> [1:22:03]: If it&rsquo;s worth the short term to drop some&hellip; I mean, sorry, if it&rsquo;s long in the long term, drop some good practice in the short term.</p> <p><strong>Robert</strong> [1:22:14]: Yeah. Like code reviews. Yeah, yeah. Because, okay, maybe just one person can work on some functionality for one month, and, okay, just rush that, and because there is some tight deadline or whatever, okay, fine, but it&rsquo;s not for free. You will need to pay it back if you will need to extend this functionality, because the rest of the team will have no idea how it&rsquo;s implemented, and if only one person, implemented that, probably it&rsquo;s also not the best application in the world and it&rsquo;s very similar in the case when you have YOLO code reviews. So you&rsquo;re closing your eyes and yeah, legit to me and so again, it&rsquo;s not like you may notice that the name of this podcast is No Silver Bullet so it&rsquo;s not like you should always do code review.</p> <p><strong>Miłosz</strong> [1:23:02]: If you work on a own, you can&rsquo;t. Unless you use AI I think that&rsquo;s a good closing point consider what makes sense in this particular scenario also what you mentioned before try to be aware of the perfection trap don&rsquo;t go too far into don&rsquo;t take the metric too seriously and don&rsquo;t say something like we don&rsquo;t accept PRs above 500 lines ever it&rsquo;s like don&rsquo;t make it a hard limit or something like this</p> <p><strong>Robert</strong> [1:23:46]: But i think it&rsquo;s also important that if it&rsquo;s happening too often it&rsquo;s also good to have some pushback like sorry mate but yeah this is, and maybe you can spend some effort on splitting that because often when you have big pr it could be split and it&rsquo;s not that hard really so especially.</p> <p><strong>Miłosz</strong> [1:24:10]: If you have something like you know have a feature and then you are a good scout, so you can just refactor this a bit.</p> <p><strong>Robert</strong> [1:24:18]: Rename one function that is used in 2000 places in the project.</p> <p><strong>Miłosz</strong> [1:24:22]: So this is a perfect scenario where you can split it to a separate PR. You don&rsquo;t mix the context.</p> <p><strong>Robert</strong> [1:24:28]: It&rsquo;s probably five minutes to make a separate PR.</p> <p><strong>Miłosz</strong> [1:24:32]: It will help a lot for the reviewers because they don&rsquo;t mix the context of refactor with actual changes.</p> <p><strong>Robert</strong> [1:24:39]: I think it&rsquo;s actually what you said. It&rsquo;s very important. If you&rsquo;re doing refactoring, don&rsquo;t mix it with implementing new functionality, because it&rsquo;s very, very hard to actually distinguish that. Because if you&rsquo;re doing refactoring and keeping the functionality working as earlier, it&rsquo;s much easier to read, because you&rsquo;re just focusing on nothing. And if you&rsquo;re changing functionality and doing refactoring, it&rsquo;s much, much harder to spot actually what is part of refactoring and what is part of new functionality.</p> <p><strong>Miłosz</strong> [1:25:08]: It probably helps if you learn a bit of Git, so you are comfortable branching out of a branch, which in this case is helpful. It&rsquo;s not super complex, but I know many people are intimidated by Git. Stacking PRs is sometimes a great help. Instead of one huge PR with many mixed changes sometimes it&rsquo;s easier to create three separate PRs branching from each other which is a bit of work to do huge amount, but for the reviewers it will be much easier to review.</p> <p><strong>Robert</strong> [1:25:50]: But also try to avoid that, because if you&rsquo;re branching out of the branch, it means that you probably have problem with some basics, like you&rsquo;re waiting for too long for the review.</p> <p><strong>Miłosz</strong> [1:26:03]: Yeah, but sometimes even if you wait like, you know, four hours for review, so it&rsquo;s the same day, it makes sense to branch out so you can work on something next.</p> <p><strong>Robert</strong> [1:26:11]: It&rsquo;s not that big, yeah, yeah, yeah.</p> <p><strong>Miłosz</strong> [1:26:13]: But it&rsquo;s also&hellip; Probably not the default for everything, but I think it&rsquo;s&hellip; If you&rsquo;re not comfortable doing it, then you will be trapped. Because you will wait for a review and you will think, okay, I can&rsquo;t work.</p> <p><strong>Robert</strong> [1:26:28]: But there is also one trap. If you try to start to work on the next functionality that is branched out of that, that you&rsquo;ll receive with you, it probably may make the reverse cycle slower because you&rsquo;ll need to contact switch back to that. Sometimes it&rsquo;s maybe better to do and make a coffee, two or three, four hours. But again, as we said earlier&hellip; But again, as we said earlier, there are multiple things that you need to join to get this good, fast review cycle, and it&rsquo;s making a much better effect at the end, when you, just have multiple good practices that we mentioned, and it really flows.</p> <p><strong>Miłosz</strong> [1:27:20]: Yeah, this is a great place to be in when the world just goes straight to the production.</p> <p><strong>Robert</strong> [1:27:26]: And again, if somebody is telling you that, you know, implementing functionality and merging it within one day is just in the books on the conferences, it&rsquo;s not, really. It&rsquo;s possible to do. It requires effort. It requires high confidence. Team code review culture, but it&rsquo;s possible, and it&rsquo;s very worth, and I really hope that everybody who is listening for that will be able to work in such a team. We did that, and it was just super cool to feel this work flowing and see how fast we&rsquo;re able to implement things.</p> <p><strong>Miłosz</strong> [1:28:06]: Yeah, I think it&rsquo;s one of the top experiences you can have in a team.</p> <p><strong>Robert</strong> [1:28:10]: Now we are actually two in our teams, so it&rsquo;s a bit also easier because we can just&hellip;</p> <p><strong>Miłosz</strong> [1:28:17]: Still applies, though.</p> <p><strong>Robert</strong> [1:28:18]: Yeah, yeah, yeah. So obviously it&rsquo;s a bit easier to do when we&rsquo;re two.</p> <p><strong>Miłosz</strong> [1:28:24]: But it&rsquo;s more tempting to build big PR, so we need to watch out.</p> <p><strong>Robert</strong> [1:28:31]: Yep, we also do it from time to time. So don&rsquo;t be perfectionists. I would say 80% so try to aim for 80% of PRs to be merged within the same day. It&rsquo;s possible to do. But also, don&rsquo;t probably 100%, it will be hard. But try to learn from those PRs that are not merged within this timebox. Alright, so I think we are out of the topics so it will be time for Q&amp;A so if you have any questions please leave them on the chat and we&rsquo;ll be more than happy to ask for them. Also, one small announcement. So, if you are longer with us, and if you are in our newsletter that we highly recommend, we mentioned that we&rsquo;ll be dropping the&hellip; Live from our podcast. So in other words, we&rsquo;ll pre-record episodes because, it&rsquo;s a bit hard sometimes to organize everything.</p> <p><strong>Miłosz</strong> [1:29:30]: As we can see today, we have our overheating camera.</p> <p><strong>Robert</strong> [1:29:33]: Yeah, yeah, yeah, exactly. And as also you may notice one week ago, when we were not able to be live because of some sick leaves, etc. And also, it&rsquo;s not super easy to actually record it live because we need to think really quickly. It&rsquo;s probably too quick for us, and it&rsquo;s not that easy to keep this slow. And for our podcasting platform, we are doing edits, and it&rsquo;s easier to listen for that. But in the YouTube, the version that is staying there is the live version, basically. So there are some pauses, so it&rsquo;s a bit&hellip;</p> <p><strong>Miłosz</strong> [1:30:10]: But it was a fun experiment. We did 10 episodes.</p> <p><strong>Robert</strong> [1:30:14]: Definitely.</p> <p><strong>Miłosz</strong> [1:30:15]: I think we want to come back with live format, but probably something more interesting than this talking. Maybe some coding or something like that.</p> <p><strong>Robert</strong> [1:30:26]: If we have time. Yeah. So we cannot promise, but we&rsquo;ll also run some AMA sessions from time to time, so the next one in the next week. We should put soon to the description or maybe I will do it now. So I will put to the a link to the form for AMA questions. So I will put it to this episode&rsquo;s description now. We&rsquo;ll also cover some questions that we have in the comments on YouTube. Yes. I will just put it to the description now because I may forget that. Can I edit it? What else?</p> <p><strong>Miłosz</strong> [1:31:25]: Yeah, that&rsquo;s it. Thank you for joining the last live episode, for now at least. And thank you for the comments.</p> <p><strong>Robert</strong> [1:31:34]: Yeah, we&rsquo;ll see each other next week in the AMA episode. And later we are doing some short summer break because it was actually pretty intensive now or a bit longer. And yeah, we should be back after summer with some other updated podcast. So we&rsquo;ll have some more surprises, but we&rsquo;ll see after vacations.</p> <p><strong>Miłosz</strong> [1:32:13]: Cool.</p> <p><strong>Robert</strong> [1:32:14]: All right, cool. So I think we are done for today. So remember to leave some comments if you have under the video, leave thumbs up so the Deadly Algorithm will like us more. Subscribe to our podcast on Spotify, Apple Podcasts, and every application that you are using. Yeah.</p> <p><strong>Miłosz</strong> [1:32:42]: Thank you, everyone.</p> <p><strong>Robert</strong> [1:32:43]: Thank you. And see you next time in a week. Bye.</p>Event-Driven Architecture: The Hard Partshttps://threedots.tech/episode/event-driven-architecture/Wed, 04 Jun 2025 16:00:00 +0000https://threedots.tech/episode/event-driven-architecture/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Event-driven architecture (EDA) is powerful but tricky</strong> – it&rsquo;s great for scaling and decoupling, but has many hidden traps.</li> <li><strong>Observability is essential</strong> – debugging async systems without tracing, logs, and correlation IDs is almost impossible.</li> <li><strong>Use the outbox pattern</strong> – it’s the safest way to publish events without losing data.</li> <li><strong>Design events carefully</strong> – large, generic events can lead to tight coupling and painful refactors.</li> <li><strong>Avoid over-engineering</strong> – sometimes synchronous systems or simple monoliths are just better.</li> <li><strong>Start with sync if unsure</strong> – it&rsquo;s easier to migrate from a well-structured synchronous system to async later than the other way around.</li> </ul> <h2 id="introduction">Introduction</h2> <p>In this episode of No Silver Bullet, we dive deep into the real-world challenges of working with event-driven architecture.</p> <p>We share hard-learned lessons from building distributed systems with events, covering pitfalls like dropped messages, debugging, eventual consistency, and designing events.</p> <p>We talk about the trade-offs, share practical advice, and help you decide when EDA is actually worth the complexity.</p> <h2 id="show-notes">Show Notes</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fevent-driven%2F" target="_blank">Go Event-Driven Training</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F" target="_blank">Go in One Evening Training</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2F" target="_blank">Watermill</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fmiddlewares%2F%23duplicator" target="_blank">Duplicator Middleware</a></li> <li>Blog posts <ul> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-test-architecture%2F" target="_blank">Component tests</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2F" target="_blank">Server-Sent Events</a></li> </ul> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">Wild Workouts example project</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2Ftools%2Fpq" target="_blank">pq - poison queue CLI</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-sql" target="_blank">watermill-sql</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Ftransactional-events-forwarder" target="_blank">Outbox example</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fdebezium.io%2F" target="_blank">Debezium</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.rabbitmq.com%2F" target="_blank">RabbitMQ</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fkafka.apache.org%2F" target="_blank">Kafka</a></li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>If you have a big event that 10 teams depend on, you can&rsquo;t just say, okay guys, we will replace this event with another one or with 10 other ones.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>If you feel like you need distributed transactions between three services, maybe you should just merge them into one.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>The good news here is that message brokers are usually designed so that you don&rsquo;t lose the message, because it&rsquo;s quite critical for you if you use EDA.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>I heard horror stories about sending 10 millions of SMSs to one person within 10 minutes.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>The point here is that we don&rsquo;t accept inconsistency, we just accept that it happens a bit later. And it can be also difficult to explain to your product peers or your boss or whoever.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>Unfortunately, you cannot test every edge case that you can have in your application. I mean, you can try, but in reality, it&rsquo;s never enough time, as long as you&rsquo;re not building really critical services.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMZpPNQ6ynU%26amp%3Bt%3D0s">00:00:00 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMZpPNQ6ynU%26amp%3Bt%3D173s">00:02:53 - Events and messages</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMZpPNQ6ynU%26amp%3Bt%3D330s">00:05:30 - Debugging and observability</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMZpPNQ6ynU%26amp%3Bt%3D671s">00:11:11 - Missing events</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMZpPNQ6ynU%26amp%3Bt%3D1180s">00:19:40 - Eventual consistency</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMZpPNQ6ynU%26amp%3Bt%3D1648s">00:27:28 - Outbox pattern</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMZpPNQ6ynU%26amp%3Bt%3D1897s">00:31:37 - Designing events</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMZpPNQ6ynU%26amp%3Bt%3D2354s">00:39:14 - At-least-once delivery</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMZpPNQ6ynU%26amp%3Bt%3D2867s">00:47:47 - Dead letter queues and alerting</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMZpPNQ6ynU%26amp%3Bt%3D3579s">00:59:39 - Distributed transactions and sagas</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMZpPNQ6ynU%26amp%3Bt%3D4129s">01:08:49 - When not to use EDA</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiMZpPNQ6ynU%26amp%3Bt%3D4728s">01:18:48 - Q&amp;A</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Miłosz</strong> [0:00]: Event-driven architecture sounds great. You get better scaling, loose coupling, resilient systems.</p> <p><strong>Miłosz</strong> [0:06]: But it&rsquo;s difficult to get right and can turn into over-engineering. Today we talk about the tough parts that make you wonder if going async was actually a good idea. I am Ior.</p> <p><strong>Robert</strong> [0:18]: And I&rsquo;m Robert, and this is No Silver Bullet Live Podcast, where we discuss mindful backend engineering. We spent almost 20 years working together across different projects and teams. And we learned that but following advice like always do X or never do Y doesn&rsquo;t work and can limit your growth. In this show, we share multiple perspectives that will help you to make smart choices and grow into principal engineer level.</p> <p><strong>Miłosz</strong> [0:45]: If you have any questions or comments, you can leave them on the chat. We&rsquo;ll pick them up during the discussion or at the end we will also have a Q&amp;A</p> <p><strong>Miłosz</strong> [0:55]: session where we can discuss all of it.</p> <p><strong>Robert</strong> [0:57]: Exactly. And today&rsquo;s episode is kind of a continuation of the previous episode, so if you didn&rsquo;t have a chance to listen to that, it&rsquo;s highly recommended, but it&rsquo;s not required. So, in the previous episode, we discussed the differences between async and sync architecture. Today, we&rsquo;ll focus on one implementation of architecture that is async, so event-driven architecture. So this is some form of event-driven architecture. And you will take a look on challenges of applying event-driven architectures to your applications. Because we had a chance to work with multiple event-driven applications. So we know when it works nicely, when it doesn&rsquo;t. So it will be helpful for you to decide if event-driven architecture is for you.</p> <p><strong>Miłosz</strong> [1:45]: In the previous episode, we assumed synchronous architecture is the default approach in many projects. Then we focused on some tips when moving to async is the idea. And today we focus on the challenges, because there are many with EDA. So maybe I&rsquo;ll start with a very quick TLDR of what event-driven architecture is, which won&rsquo;t be a complete definition, but just so we are on the same page. So, EventDriven architecture is based on events, obviously, and an event is some state of change in your system. And the idea is that your system or systems publish events after something happens, something changes in the system, and then other services or systems react to it. And can publish their own events, and this is how they communicate, in contrast to calling each other by some more direct ways. And this architecture is often built on top of messages, and that&rsquo;s what we talk about today.</p> <p><strong>Robert</strong> [3:00]: It&rsquo;s probably also worth mentioning that you have a message, and message is some kind of way how you are transporting events. So basically, in most cases, event is payload inside of the event. So you can think if you are, for example, in writing Go, so message is something like htRequest and event is part of the body, let&rsquo;s say. So it&rsquo;s the analogy here.</p> <p><strong>Miłosz</strong> [3:26]: So event is a kind of a message, very specific message that says that something already happened, some facts happened and then whoever listens is free to react as they see fit exactly,</p> <p><strong>Robert</strong> [3:42]: Okay, but let&rsquo;s go to the challenges because I think every nice technique has some challenges so yeah, this is the NoCyverBluet podcast and.</p> <p><strong>Miłosz</strong> [3:56]: There are definitely many challenges when it comes to even driven architecture The first one can be debugging issues and the fear of losing events. And I actually have a quote that comes from Reddit comment we received from under the previous episode. And the commenter said, Stuff comes in, doesn&rsquo;t come out. Things appear that you cannot explain where they came from. The amount of telemetry you have to add on your own to make those things sane is absolutely ridiculous in 2025.</p> <p><strong>Robert</strong> [4:31]: Yeah, probably it sounds like a person that maybe has seen some people misusing this kind of architecture.</p> <p><strong>Miłosz</strong> [4:37]: So you clearly see someone that struggled with this kind of approach before. But I would also say this critique is kind of fair. This can definitely happen. So it&rsquo;s good to be ready for it and prepare.</p> <p><strong>Robert</strong> [4:55]: And I think it&rsquo;s a common theme if you were discussing multiple different techniques in previous episodes. It was a common theme that people were always complaining about some techniques. Also in places when this was misused. Because using some technique for some approaches may be just too sophisticated, let&rsquo;s say. And if you are not solving a real problem with any technique, you will just have more problems at the end.</p> <p><strong>Miłosz</strong> [5:27]: But even if you pick the right tool for the job, still can have this issue. Like, if you just call an HTTP endpoint, it will end up with some status code. If the other end doesn&rsquo;t support the method, you will get 404. You will right away know what&rsquo;s going on. Events, this is the hard part. If something goes wrong and you publish an event, and then the other end just doesn&rsquo;t react to it. That&rsquo;s the hard part, because you don&rsquo;t know what happened.</p> <p><strong>Robert</strong> [6:01]: It&rsquo;s kind of similar to using CI and tests. I mean, if you&rsquo;re using CI and tests, you may complain that you have a problem that you need to write tests, they&rsquo;re flaky, and you maintain them. And yeah, it&rsquo;s true. But maybe the question is, if you have right tests, maybe you should remove some of them in this case. But it&rsquo;s all about cost versus return of investment, basically.</p> <p><strong>Miłosz</strong> [6:25]: Yeah. The good news here is that message brokers are usually designed so that you don&rsquo;t lose the message, because it&rsquo;s quite critical for you if you use EDA. So I guess you could compare it to databases as well, but you don&rsquo;t see people worried about SQL database losing the data, because it was designed to keep it, right?</p> <p><strong>Robert</strong> [6:55]: And usually, if somebody is complaining that a message broker is losing messages, it&rsquo;s probably not a problem with the message broker. Let&rsquo;s say that it&rsquo;s similar to many people complaining like, oh, I think it&rsquo;s a kernel bug or a programming language bug. in 99% of cases it&rsquo;s not. Sometimes it is, but I would not start with assuming that it&rsquo;s the case.</p> <p><strong>Miłosz</strong> [7:19]: This reminds me of this chapter in the Pragmatic Programmer about select just works. It&rsquo;s exactly about this issue. So yeah, if you use the message broker the right way you shouldn&rsquo;t have this issue. Of course it&rsquo;s not that trivial sometimes to configure everything. But the good news is It is designed to help you not to lose any messages. But still, some weird stuff happens. I have quite recent example of this, where we have been running services on staging. And we got some events missing, just randomly. And it was super frustrating because we had proper telemetry and tracing and event log. And we just couldn&rsquo;t figure out why this single event doesn&rsquo;t arrive at the destination. And yeah, the intuition here is to blame the software, right? We were usingbiu RabitMQ at the time and we started wondering how can it be that it just randomly rejects some messages.</p> <p><strong>Miłosz</strong> [8:34]: And of course, this wasn&rsquo;t the reason. The reason was our frontend team were running our services locally. And they just used the staging RabitMQ address to run it. So unknowingly, they were consuming our messages and the handlers were doing changes in the local database instead of the staging database.</p> <p><strong>Robert</strong> [8:57]: So let&rsquo;s learn having access to non-development environment from local environment is always a great idea.</p> <p><strong>Miłosz</strong> [9:05]: Yeah, that&rsquo;s one lesson learned for sure. And the other is, RabbitMQ probably works. So if you see some very weird issue with some event arriving. Probably look into your code, or in this case, who uses your code. Still, it doesn&rsquo;t need to be super complex to have this observability in place. If you have a tracing setup, probably the best, because you can see the entire flow of the system. But I would say even a simple correlation ID in logs can help a lot. You can just trace which services receive the event you published.</p> <p><strong>Robert</strong> [9:53]: And I assume that if you are thinking about using event-driven architecture, you probably should have some kind of distributed architecture under a hood. And I assume that if you have, you should have already some observability for that in place. Some tracing or at least supporting correlation ID properly. If you are supporting it already for other transports like HTTP or gRPC, adding it for messages, it shouldn&rsquo;t be hard.</p> <p><strong>Miłosz</strong> [10:28]: It&rsquo;s also often skipped. Observability is not a super interesting topic. Sometimes if you build a product, it&rsquo;s often an afterthought. And then you already have trouble if you have no tooling in place and you start using events and then you need to debug something. That would be a terrible idea. So, definitely start with this.</p> <p><strong>Robert</strong> [10:57]: There is actually a question. How you build on the chat? So just a reminder. So if you have any questions, please drop them on the chat. If not now, we&rsquo;ll answer all of them at the end. But I think that this one is relevant. How you build your testing to catch dropped events like this?</p> <p><strong>Robert</strong> [11:15]: Basically, in this case, I would say that in most cases, what is helpful for us is using component tests. So there are a bit like end-to-end tests, but just on the level of your service. And there pretty useful for that. So in other words, you are calling some endpoint, you are assuming that something is processed, and at the end, it should be some output going from the system that is showing that something was processed. The bad thing is that usually when you are losing some events, it&rsquo;s some small chance that it will happen. So it&rsquo;s because of some misconfiguration or some edge case or some lost connection and it&rsquo;s a bit harder to do that. So in this case, what&rsquo;s useful is using some library that already have tests for that and is ensuring that no events are missed. Of course, you still can have some misconfiguration, but in most cases, unfortunately, it&rsquo;s like you might notice something on the production and you need to fix it afterwards. But, for example, in Watermill, so the library that we&rsquo;re developing, we have a lot of tests that are checking managed cases like disconnecting or doing some strange negative acknowledgement to ensure that no message is lost. And so far it works because, We just have a lot of tests for that.</p> <p><strong>Miłosz</strong> [12:42]: Regarding component tests, it&rsquo;s important to use the same configuration you use in production. The exact same setup. Because it may be tempting to have something much simpler in tests, so it&rsquo;s easier to just start. But this way you don&rsquo;t test this entire topology, which can be sometimes quite difficult. Especially if you have something like a single event topic that routes events to some data lake or event log, something like this. It can be useful to do it for all services, but you should do it the same way in the tests, otherwise you&rsquo;re not sure if you are testing the right thing.</p> <p><strong>Robert</strong> [13:30]: But in general, if you are using, for example, some libraries like Watermill, it should abstract it from you. And it&rsquo;s hard to lose this message, basically, as long as you will not do things like something ever happened, and you will think, okay, everything is all right, I&rsquo;m acknowledging the messaging going forward. But I would say that it&rsquo;s just about being sane where you acknowledge some message that you are processing.</p> <p><strong>Miłosz</strong> [13:51]: Yeah, but testing is also not everything. You need this tooling to debug these production issues, because this issue of another team consuming your events by mistake, there&rsquo;s no way you can do a test for it locally or in the CI.</p> <p><strong>Robert</strong> [14:08]: I think it&rsquo;s probably not only the case for an asynchronous architecture, because it can also happen for anything basically. If someone is interacting with your staging, there&rsquo;s no way that you can test that, and this is the reality.</p> <p><strong>Miłosz</strong> [14:21]: Yeah, but I think it&rsquo;s easier to see the HTTP requests, right? Because you usually have logs on both sides. And in this case, you see the event is published, but you don&rsquo;t see the other end at all. So that might be a bit more difficult to test.</p> <p><strong>Robert</strong> [14:41]: That&rsquo;s true, that&rsquo;s true. But yeah, TLDR, unfortunately, you cannot test every edge case that you can have in your application. I mean, you can try, but in reality, it&rsquo;s never had enough time, as long as you&rsquo;re not building really critical services. So, in this case, you can spend insane amount of time on ensuring that. But, yeah, it&rsquo;s&hellip;</p> <p><strong>Miłosz</strong> [15:05]: Yeah, so to reiterate use observability from the start. Add some simple correlation ID in logs, tracing if you can. That will help a lot in debugging. And also don&rsquo;t reinvent the wheel when it comes to using brokers. So for sure don&rsquo;t implement your own queue from scratch. Probably not a good idea. Similar to not developing your own database. Use proven patterns, brokers and libraries that are tested and they work on production.</p> <p><strong>Robert</strong> [15:47]: And if you were mentioning patterns, we&rsquo;ll today also mention the outbox pattern. And I think that in this situation, this is probably the most important one because this is a super common issue that people are not using the outbox pattern. And why it&rsquo;s probably one of the simplest ways to miss events. But we&rsquo;ll go into that.</p> <p><strong>Miłosz</strong> [16:06]: A good design of events can also help here. The more complex the workflow you have, the more difficult it will be to debug if anything wrong happens. So if you can keep your design simple and easy to grasp, it would be also much easier to debug any issues. So let&rsquo;s say, if you have a single event that&rsquo;s being consumed by tens of subscribers, instead of smaller events maybe that have fewer consumers, it might be more difficult to understand why some issues are happening.</p> <p><strong>Robert</strong> [16:55]: And you also mentioned that some people are kind of afraid of this losing message. And I think it&rsquo;s also important to, you know, if you are building an asynchronous architecture, it doesn&rsquo;t need to be event-driven, it can be also message-driven. You should just trust it very much. I mean, you should trust that if you are publishing some event, this event is later consumed. Because I heard multiple times that people were kind of not trusting in their infrastructure that they have for that. They feel that, okay, I&rsquo;m metting some event. I&rsquo;m not really sure if it will be later constant or whatever. So I would say that this is a red flag. I mean, it should be something that it should be as stable as your database. So when you are doing SQL query, you&rsquo;re assuming that this database basically works. And this should be also the case for your event driven or message event applications.</p> <p><strong>Miłosz</strong> [17:50]: I wouldn&rsquo;t like to work with a system that no one trusts the job done</p> <p><strong>Robert</strong> [17:57]: So, the next thing that I would say is also a pretty big challenge is maybe changing the mindset of how you are building your system. Because if you are building your system in a synchronous way, as somebody mentioned on the chat, it&rsquo;s also kind of more straightforward. So you are doing some operation, you are storing something in the database, some external service is called, and you know that it&rsquo;s done. And when you&rsquo;re building an event-driven application, for example, there is some trade.</p> <p><strong>Robert</strong> [18:32]: There&rsquo;s a use case that we discussed in the previous slide. In this use case, for example, we are reducing user and we are sending some data to some external system. But user registration is usually the critical part of the application. And if we are sending some data to external system, we cannot always trust that the system will be up, especially if you have out of traffic and we have some promotions. And let&rsquo;s imagine that we have 1,000 users registering in one minute, for example, or even more, and it doesn&rsquo;t work because some third-party system cannot handle that. It can happen, even if it&rsquo;s not super big scale. But from the other side, it may be also important to send data to this external system. So, a nice solution, maybe using Event-Driven architecture here, because you can send this data asynchronously. But the downside is that the data that will go to the system may be not always consistent. So, it may happen sometimes that there is some bug, and instead of storing this data synchronously to the system, you&rsquo;ll need to wait for a while before it will be stored there.</p> <p><strong>Miłosz</strong> [19:39]: And at first it seems like a big issue because how can I live without transactions? What to do now.</p> <p><strong>Robert</strong> [19:46]: But I would say that it&rsquo;s important to think about the trade-offs here, because what would be the trade-off here? So instead of doing it within the request, if it&rsquo;s delayed, it probably means that something is wrong.</p> <p><strong>Robert</strong> [19:57]: And if something is wrong, so this request will probably just not work. So it&rsquo;s much better to have this request working, so your customers will not experience any degradation of your service, everybody will be happy and data will be just delayed this is a trade-off and this is also something that we often, heard when we&rsquo;re discussing it with some business people that if you ask them that okay does this data need to be consistent and i&rsquo;m sure that in 90 of cases the person will say yeah yes data need to be always consistent because why it shouldn&rsquo;t be consistent what will happen if it will be delayed yeah.</p> <p><strong>Miłosz</strong> [20:35]: Yeah it&rsquo;s like asking if it should be high quality</p> <p><strong>Robert</strong> [20:38]: Of course Yes.</p> <p><strong>Miłosz</strong> [20:39]: It needs to be.</p> <p><strong>Robert</strong> [20:40]: Yeah, boss, can we make bucks?</p> <p><strong>Miłosz</strong> [20:42]: What am I paying for?</p> <p><strong>Robert</strong> [20:44]: And, yeah, obviously, like you said, with quality, we can always write highest quality code. But is this worth? I mean, writing this highest quality code, it will cost more time. Maybe we don&rsquo;t have this time. Maybe it&rsquo;s fine to cut some corners and they vary it faster. And this is the same with consistency. So sometimes it&rsquo;s better to be delayed if you compare it with something not happening at all.</p> <p><strong>Miłosz</strong> [21:11]: So this is the eventual consistency concept from EventDriven architecture. The point here is that we don&rsquo;t accept inconsistency, we just accept that it happens a bit later.</p> <p><strong>Robert</strong> [21:25]: Eventually.</p> <p><strong>Miłosz</strong> [21:26]: Eventually, yeah. So it&rsquo;s an important point and it can be also difficult to explain to your product peers or your boss or whoever. Because we normally this inconsistency we know is super small smaller than a second but sometimes it can be longer longer What&rsquo;s surprising is, very often, it&rsquo;s more acceptable than you would think. You could have inconsistent systems in reality, sometimes for hours even, and it&rsquo;s completely fine. So this is more a product decision than purely a technical decision. Something you have to discuss with your stakeholders, basically.</p> <p><strong>Robert</strong> [22:15]: Yeah and i think it&rsquo;s again always good to show trade-offs so okay it can be delayed but what if for example the system is down what we prefer maybe it&rsquo;s fine that it will be a bit delayed but it will not stop entire entire users journey you can also for example say how it&rsquo;s work on bigger social media platform for example so if you are posting a post to x facebook instagram whatever so you see that some post was posted but probably it&rsquo;s taking a couple seconds before it will be shown in different people&rsquo;s wall and it&rsquo;s just because of adding it everywhere synchronously will be just super slow because probably it needs to happen in multiple regions and doing it synchronously it will just sometimes fail and probably it&rsquo;s super rare that you are adding some posts somewhere and you see the error maybe it&rsquo;s happening under the hood so for example maybe in some region, it&rsquo;s added after five minutes, maybe after some hours even, you&rsquo;d never know. But you don&rsquo;t see it in the UI, because it&rsquo;s just eventually consistent.</p> <p><strong>Miłosz</strong> [23:21]: It&rsquo;s similar when changing your avatar or something like this. You can often see that many components on the website will be updated, others don&rsquo;t. But also not in the technical sense, you can see it in real life, in how some systems work. Even bank transfers which seem like something that&rsquo;s always consistent, right? Because you have the ledger. The traditional transfers are not immediate. So it&rsquo;s a very similar concept.</p> <p><strong>Robert</strong> [24:00]: Yeah, but it&rsquo;s important to keep in mind. So if you are discussing it with some product owner, some product person, it&rsquo;s important to show that, okay, this is the trade-off and the UX implication of that. Because if you are adding something to your system, it may be not visible immediately for users. So it&rsquo;s important to keep it in mind. Sometimes it&rsquo;s worth asking a question if it&rsquo;s really needed. If it&rsquo;s needed to have it eventually consistent. We already mentioned it in the previous episode, but we are big fans of server sent events. So this is really cool technology for handling this kind of updates. Also, I think a link in the episode materials, our blog post showing how to implement it in production-grade environments.</p> <p><strong>Miłosz</strong> [24:52]: So it&rsquo;s maybe another challenge here that you have to adjust your UI for this kind of errors. Something we also mentioned in the previous episode.</p> <p><strong>Robert</strong> [25:03]: The good thing is that there is also some midway here, because let&rsquo;s imagine that we are creating some system that is adding some data to your system, and maybe it needs to synchronize some data with external systems. But there is fortunately a mid-air ground that often people are missing, I think. I have an idea why, because to do it properly you need to know a couple of some techniques, but from the other side, it&rsquo;s not rocket science, especially if you do it already once. So, because if you are doing some changes, you can still store some changes to your database locally in the transaction and also emit an event. It has one challenge compared to that, but it&rsquo;s nice from the UX perspective, because you can write your data to your source of truth database, that is, for example, database of your service, and you can also emit an event. And it&rsquo;s nice because if operations exceeded, you can show it to the user immediately and you don&rsquo;t need to do any magic with servers and events. It will be just there.</p> <p><strong>Robert</strong> [26:05]: But there is one trouble with this approach because, as I said, it assumes that we are storing something in the database and we are emitting an event. But what if something will happen in the middle? So, for example, we stored our data in the database, and our service died before sending the event. And before somebody saying that, oh no, the chance for that is super small, trust me, even for low-scale systems, it&rsquo;s happening more often than you can think. It&rsquo;s enough. And you may also say, oh, we have graceful shutdowns. Yes, as long as, for example, your service will not go out of memory, or you have some bug that will not make it shut down gracefully.</p> <p><strong>Miłosz</strong> [26:49]: The earnest network dice, which is super common.</p> <p><strong>Robert</strong> [26:53]: And you cannot do much about that. So it&rsquo;s actually an interesting challenge to think. Okay, so in this case, we should store the data first to the database and emit our event to Message Broker, or vice versa. Maybe we should emit our event first and store data to the database.</p> <p><strong>Miłosz</strong> [27:10]: Or call the publisher inside the transaction.</p> <p><strong>Robert</strong> [27:14]: Or maybe in Thread and try to synchronize that.</p> <p><strong>Miłosz</strong> [27:18]: Yeah, so basically there is no good Almost no good way out here.</p> <p><strong>Robert</strong> [27:23]: In every situation, we&rsquo;ll end up with inconsistency, basically.</p> <p><strong>Miłosz</strong> [27:28]: Yeah, but there&rsquo;s an outbox pattern that you mentioned before, which is a nice way out of this.</p> <p><strong>Robert</strong> [27:35]: Yeah, and it&rsquo;s also pretty cool in the simplicity. So basically, the idea of an outbox pattern is that the event that we would like to emit, you should store it in the same database as you are storing your data. For example, let&rsquo;s imagine that your source of truth database is Postgres, So within one transaction, you are storing your event to Postgres, and you are also storing the data that you would like to send to your Postgres, and you are storing the event that you would like to event to this database within one transaction. And later, you are just streaming those events from this table with events to your method broker. And that&rsquo;s it, basically.</p> <p><strong>Miłosz</strong> [28:18]: And the streaming happens in the background. So it&rsquo;s essentially the main transaction.</p> <p><strong>Robert</strong> [28:22]: So it&rsquo;s still eventually consistent, but well, like everything with event driven architecture, everything is eventually consistent. So it&rsquo;s obviously a bit slower because there is one operation in the middle. So you need to stream it from your Postgres to your message broker. But well, trade-offs.</p> <p><strong>Miłosz</strong> [28:43]: The difficult part is streaming from databases is not the easiest thing to do. And there are some patterns how to do it, but still, it&rsquo;s easy to make a mistake and, for example, Watermill SQL supports it, but took us three major versions to do it right and avoid some bugs. So it&rsquo;s also another thing you probably don&rsquo;t want to write from scratch. Probably better to use something that&rsquo;s proven.</p> <p><strong>Robert</strong> [29:17]: If you are using Go, use Watermill for that, because Watermill supports Outbook&rsquo;s implementation out of the box.</p> <p><strong>Miłosz</strong> [29:23]: There&rsquo;s also a tool called Debezium, we used some time ago. It&rsquo;s also quite difficult to configure, so there&rsquo;s no easy answers here.</p> <p><strong>Robert</strong> [29:34]: You put it in a nice words. It was a nightmare.</p> <p><strong>Miłosz</strong> [29:41]: So once again, it&rsquo;s something you don&rsquo;t want to reinvent, probably, unless you have a lot of time and can write lots of tests and test it on production properly.</p> <p><strong>Robert</strong> [29:53]: And there are two alternative approaches here that I have in mind. The first one is that in some cases, actually, you can store data within transactions and later emit an event, but it&rsquo;s only in a situation when it&rsquo;s not a problem to lose some data in the event. For example, we are storing something in, let&rsquo;s imagine, some telemetry or some continuous data.</p> <p><strong>Miłosz</strong> [30:18]: Yeah, why you don&rsquo;t care about losing the message?</p> <p><strong>Robert</strong> [30:21]: Yeah, so in worst case, you may lose one data point, and it&rsquo;s fine because a couple of seconds later, you&rsquo;ll have another one. So this is fine. And maybe in this case, if you care about performance, it&rsquo;s totally fine. The second interesting approach may be also if you&rsquo;d like to avoid the streaming from PubSub to from your database to PubSub, you can just use your database as PubSub. So for example, in Watermill we have PubSub implementations for SQL, MySQL, Postgres, Firestore. So it can be also done, but it&rsquo;s also a kind of specific implementation. I would say that by default you should use Outbox and stream it to your message broker. And I think we can probably link the source code of Watermill SQL in the episode notes. If you&rsquo;re not writing in Go, you can take an inspiration. Ask AI, please rewrite this code in a way that it will not be copyrighted.</p> <p><strong>Miłosz</strong> [31:29]: If you don&rsquo;t get scared of these SQL queries.</p> <p><strong>Robert</strong> [31:33]: Yeah, but don&rsquo;t do it yourself. So there are a couple of queries there.</p> <p><strong>Miłosz</strong> [31:38]: Okay, let&rsquo;s move to the next challenge, which is designing events.</p> <p><strong>Miłosz</strong> [31:44]: This is something that looks quite easy, or seems easy. Because it&rsquo;s more soft than implementing some technical stuff. But it&rsquo;s also quite difficult to get right. And especially because there&rsquo;s no good universal answers here. For example, we often get asked if what&rsquo;s better, should I have one big event or several or smaller events. Like the difference between user updated and having something like user password changed, user email changed and so on. And there is no good answer to this, because it has many trade-offs either way you go. So small events versus big events or generic versus specific and this kind of discussion there&rsquo;s just no We&rsquo;ve got one solution here.</p> <p><strong>Robert</strong> [32:45]: But I think it&rsquo;s probably one case when it&rsquo;s a pretty bad idea. So having an event when you&rsquo;re just putting an entire entity from the database there. We&rsquo;ve seen it in one system. It was actually pretty interesting archaeology. Because when we joined the component, the event was already like 50 fields. It was crazy big, and it was super hard to mock, and it was super hard to change that. But actually, a couple years earlier, when I checked in Git history, it was a pretty small event. It would have probably four fields or something like that. But it was the same model as it was stored in the database. It was growing, growing, growing, growing, growing. Like a bowling frog, basically.</p> <p><strong>Miłosz</strong> [33:25]: So you can imagine something like user updated with 50 fields in the event with all the details of the user.</p> <p><strong>Robert</strong> [33:33]: And even if you feel that at the beginning it&rsquo;s not a big issue, maybe. I mean, also you should watch out too, you know, not overcomplicate days from day one.</p> <p><strong>Miłosz</strong> [33:41]: Exactly, that&rsquo;s the hard part, right? Because, you know, say this is an anti-pattern, but let&rsquo;s say someone has a small project they do, they write themselves. So then having even like this, maybe it&rsquo;s good enough.</p> <p><strong>Robert</strong> [33:56]: Yeah, but later, let&rsquo;s imagine that the company is still there and you have like 10 teams, 50 people, and you have no idea who&rsquo;s using that, or you would like to remove some part of these fields, almost impossible. And again, it just started with four fields and putting database model directly to event. So again, try to find some balance, but watch out for some extremes that it can&hellip;</p> <p><strong>Miłosz</strong> [34:25]: So this is what I mentioned before regarding debugging and production. So we have this one huge event that everyone consumes. Maybe it&rsquo;s more difficult to trace it and debug it, in contrast to smaller events. But this is just one heuristic. So there&rsquo;s a question on the chat, what do you prefer? Granular or more composed? I&rsquo;m not sure I can give one answer here.</p> <p><strong>Robert</strong> [34:57]: Yeah, but I think probably we can risk assessment, but that usually we have more problems with the bigger one, less granular. I think it&rsquo;s probably easier to join events later than split them, basically.</p> <p><strong>Miłosz</strong> [35:18]: Yeah, because it&rsquo;s easier to get tight coupling with bigger events, because many systems start depending on it, and then you can just&hellip; Yeah, you talk about refactor, and it&rsquo;s not always that easy. Exactly because of this reason. If you have a big event that 10 teams depend on, You can&rsquo;t just say, okay guys, we will replace this event with another one or with 10 other ones.</p> <p><strong>Robert</strong> [35:51]: And it&rsquo;s even worse if one of the teams that depends on that is data science team. If one of those teams is data science, it&rsquo;s impossible. Sorry.</p> <p><strong>Miłosz</strong> [36:02]: Yeah, refactor is easy if this is an internal event your service uses. That&rsquo;s cool. But if it becomes a contract with other teams, yeah, that&rsquo;s much more difficult. So, yeah, probably if you have more teams, it makes sense to start smaller, so at least you have this small contract. Maybe similar to API design of HTTP, probably you want to have a smaller method than one huge update method. It also depends on the API, but in terms of contract, it&rsquo;s probably easier to track who is using each method rather than how people use this one golden method that does everything.</p> <p><strong>Robert</strong> [36:52]: And I think a good heuristic to see if your events are too granular are cases when you need to do some kind of aggregation. So, basically, to do something, you need to listen to two events and do some magic to, OK, I finally received two events and I can do operations. So, it may be the sign that, OK, maybe it&rsquo;s too granular. So that&rsquo;s the challenge.</p> <p><strong>Miłosz</strong> [37:19]: I would say, this lack of good universal answers here. Also, it&rsquo;s not something very technical. You can just get better at it by reading a book. Most often you want to have discussions or event-storming sessions with other people in the team or in the company, with your stakeholders, to understand how to model it. So that also can be challenging.</p> <p><strong>Robert</strong> [37:48]: Also, watch out for not discuss it for a couple weeks, because I think at some point, probably it would be good to record an episode about some kind of trust to engineering things. But it&rsquo;s another extreme that doesn&rsquo;t help to gain trust in engineering things when you&rsquo;re spending weeks or discussing something. Such stuff.</p> <p><strong>Miłosz</strong> [38:10]: That&rsquo;s also a good point. So it&rsquo;s easy to become a purist here as well and just decide we are not moving forward until we decide how exactly this event should be made it&rsquo;s probably also an extreme you want to avoid</p> <p><strong>Robert</strong> [38:29]: To summarize I think one of the biggest challenges here is decoupling so decoupling is downside here because everything is that decoupled with event-driven architecture that sometimes it&rsquo;s hard to really change something downstream.</p> <p><strong>Miłosz</strong> [38:46]: Or check how it&rsquo;s used.</p> <p><strong>Robert</strong> [38:48]: Yeah.</p> <p><strong>Miłosz</strong> [38:49]: Especially if it&rsquo;s&hellip; This is what you mentioned before about this, in contrast with HTTP, where you see the logs and, you know, who called it? If you just use events, it might be more difficult if you don&rsquo;t have the tooling in place.</p> <p><strong>Robert</strong> [39:06]: Agreed. Okay so the next very common challenge that you can have when you are building event systems especially when you didn&rsquo;t work with them is at least once deliver and this is, connected again to how most of the message brokers are built but tldr is that, most of the message brokers are offering you at least once deliver semantics so it means that you can receive every message at least once. It&rsquo;s not possible that you will not receive this message, but you can basically receive it multiple times. This is connected to the problem that we were discussing a bit earlier with Outbox. What will happen if your consumer will lose network connection in the middle of processing the message? They are processing the message, you lose the network connection, somebody cut the fiber cable between data Or DNS.</p> <p><strong>Miłosz</strong> [40:13]: It&rsquo;s always a DNS.</p> <p><strong>Robert</strong> [40:16]: And what message broker should do? He doesn&rsquo;t know if you reprocess that, so you stored the data and committed the transaction already, or maybe not yet. And the assumption here is, in most cases, that message broker is assuming that you didn&rsquo;t process that, and it&rsquo;s red-delivered.</p> <p><strong>Miłosz</strong> [40:35]: Remember, message brokers are designed so you don&rsquo;t lose the message, which is actually a good thing.</p> <p><strong>Robert</strong> [40:39]: Yeah, the downside is that you need to keep it in mind. So basically, if messages can be redelivered, you need to handle the situation when it&rsquo;s delivered. The good thing is that it&rsquo;s rocket science. So for example, if you&rsquo;re a creating user, you&rsquo;re basically checking if the user that you are creating already exists. It&rsquo;s good to have some identity that you can match and deduplicate by that. Or sometimes you can have some duplication ID, but you need to keep it in mind.</p> <p><strong>Miłosz</strong> [41:10]: Maybe just reiterate why this is an issue. The event that create the user gets duplicated, let&rsquo;s say it&rsquo;s executed twice, right? So the first message will create the user, and the second one, or the second delivery of the same message, tries to insert the user into the database and it ends up with some kind of duplicate ID error on the SQL side. And this error will cause the message to go back to the queue, and it effectively blocks your queue.</p> <p><strong>Robert</strong> [41:47]: And it&rsquo;s probably the best situation, because you can notice this error and fix that just maybe by adding to your queries like on WK do nothing and it&rsquo;s fine.</p> <p><strong>Miłosz</strong> [41:58]: Yeah, sometimes it can have worse consequences. If, for example, this email will be delivered over and over again, and it&rsquo;s usually within milliseconds. So if the handler does something like sends a message to the user via notification or text message or something like this, and for some reason it&rsquo;s not stopped. You can do it thousands of times.</p> <p><strong>Robert</strong> [42:26]: Yeah, I heard horror stories about sending 10 millions of SMSs to one person within 10 minutes.</p> <p><strong>Miłosz</strong> [42:35]: Yeah. So that can be tricky. But more often you have an error. You can detect and fix.</p> <p><strong>Robert</strong> [42:44]: I think there is also one case when it&rsquo;s worse and it&rsquo;s hard to detect. So, for example, when you have some counters or something. So, when you have a counter and a message is re-delivered, it&rsquo;s a pretty subtle error. So, it&rsquo;s hard to detect because, for example, as we had the question on the chat earlier, how to detect some things and when. When, for example, this counter is not implemented properly, it may be hard to detect that because probably, maybe at the end, someone will notice that I&rsquo;m, for example, charged much more than I should have.</p> <p><strong>Miłosz</strong> [43:20]: So this is a worse scenario where you don&rsquo;t know that an issue occurred. You&rsquo;ll discover it much later when it&rsquo;s too late.</p> <p><strong>Robert</strong> [43:29]: But for that, actually, we have a pretty good tactic on how to avoid that. So when you have systems who are prone for this kind of issues, you can use, for example, in Watermill, duplication middleware. So basically, this is the middleware that is delivering every message twice. So basically, every message that you receive in your local environment will be delivered twice. And it&rsquo;s fine when your system is not super heavy and you are able to run tests with that, because you are just 100% sure that everything is processed properly, because this is basically how it should work. You should be able to receive every message twice. Your system should work exactly the same as this message will be delivered.</p> <p><strong>Miłosz</strong> [44:10]: It&rsquo;s a bit like chaos engineering, where you introduce some issues and purpose to the system.</p> <p><strong>Robert</strong> [44:18]: The downside is that if your operations are heavier, you probably need to have more runners in your CI or more powerful laptops.</p> <p><strong>Miłosz</strong> [44:27]: Or you could just do it in the tests.</p> <p><strong>Robert</strong> [44:31]: Or just for some handlers, this is also the case. But it&rsquo;s not worth doing it for everything. I mean, it depends on the system, of course, but doing it for everything, it may be probably an overkill. For example, if you&rsquo;re working with some financial domain, like we worked, for example, we just have this middleware enabled and we&rsquo;re just handling every message twice. If you&rsquo;re crazy, you can even do it with your production.</p> <p><strong>Miłosz</strong> [44:57]: Yeah, it should be safe to do it, actually. So the solution are idempotent handlers. So the handlers that you can safely execute twice or more times with the same input, and they won&rsquo;t fail. In this case, in this example of the user being added, you just add onConflict, do nothing. Or you just somehow ignore the error. That&rsquo;s a good application. And I know many people when they hear about it the first time are try very hard to figure out another solution. Because the idea of writing handlers like this is&hellip; I don&rsquo;t know, annoying, maybe? They don&rsquo;t like to do it. But the truth is, it&rsquo;s not that complicated most of the time, right?</p> <p><strong>Robert</strong> [45:51]: Yeah, so if you&rsquo;re inserting stuff just like that, it&rsquo;s a bit harder with counters, because sometimes you need to have some table with the application ID, so you&rsquo;re storing some message ID or operation ID. they are deduplicating it by that. One scenario when it&rsquo;s harder is when you are calling some external systems. So for example, when you are charging customers, but in most cases, when you are using some same systems, they should have some idempotency key or something like that. And basically, in this case, you are just sending this idempotency key and it&rsquo;s deduplicated on the other side. I think it&rsquo;s showing nicely like, okay, you&rsquo;re calling some external system that it&rsquo;s not really even driven, but often this kind of system offers some kind of application because it&rsquo;s not really event-driven, but it&rsquo;s a very similar situation.</p> <p><strong>Miłosz</strong> [46:39]: Yeah, payment systems try to work like this most often.</p> <p><strong>Robert</strong> [46:43]: Yeah, because this is the same situation as you have with message brokers. So you have HTTP connection, it was interrupted before the HTTP status code was sent to you, and the system on the other side doesn&rsquo;t know what to do. Especially when it&rsquo;s important in cases when you, for example, are charging people. Nobody would like to be charged twice. As long as you are not the person that is charging.</p> <p><strong>Miłosz</strong> [47:08]: You can also see this idea in API design sometimes. If you have an HTTP handler that returns 200 or 201. So you can differentiate either by the response if the entity has been created or just everything is fine, nothing to do. It can also be helpful to handle this.</p> <p><strong>Robert</strong> [47:33]: That&rsquo;s true.</p> <p><strong>Miłosz</strong> [47:38]: Okay. A bit related challenge here is messages that can&rsquo;t be processed, so similar to this error. Because of the replicated message, you can have some message that has been published by mistake or can&rsquo;t be unmartialed because of some issue. Basically it&rsquo;s broken and whatever you do, it won&rsquo;t be processed. And most often it will block your queue or this part of it.</p> <p><strong>Robert</strong> [48:15]: And I think it&rsquo;s showing nicely the trade-off that we are making here with asynchronous or even Jumann architecture. So in normal HTTP endpoints, it would be not an issue. So it will just return 500, and it&rsquo;s not a problem. But you are losing the resilience. But in this case, well, resilience is important, but you need to do something about this message.</p> <p><strong>Miłosz</strong> [48:37]: Yeah. And most often you want to move it out of the queue, so acknowledge it. But you probably also don&rsquo;t want to lose it entirely, because you might have some important details or you don&rsquo;t want to lose this information. So then you can move it to this separate queue, sometimes called the data queue or poison queue, or it just sits and waits for investigation. It sounds simple, but it&rsquo;s also not trivial sometimes. I mean publishing to a separate queue is very easy, but you need to have some tooling to review those messages and move them back to the main queue if needed. And I think there&rsquo;s no universal tooling here, depends on the PubSub. Some PubSUps have some admin interface, like RabbitMQ lets you pick messages. That&rsquo;s one way to do it. But it can be also tricky. You can also provide your own tooling for it. We recently added a CLI like this for Watermill, for the SQL queue. So it can be useful to have some even simple tooling that lets you just review what&rsquo;s there and has actions like delete or move to the main queue.</p> <p><strong>Robert</strong> [50:04]: It&rsquo;s also worth mentioning, because we&rsquo;ve been mentioning Watermill a couple of times, but if you are with us for the first time, you might not know, it&rsquo;s not our product, it&rsquo;s an open source library that we are just sharing for free and there is no enterprise additions. Everything that we mentioned is just free in open source. There was one episode, I think two episodes ago, we&rsquo;ve been sharing more details about Watermill. So worth checking definitely if you are thinking about creating your own open source library, because Watermill is pretty successful in the Go community. So to give you some number, it has more than 8000 stars on GitHub and a lot of projects using that. So again, if you&rsquo;re thinking about creating your own over-the-source project that is useful for other people, definitely worth checking.</p> <p><strong>Miłosz</strong> [50:55]: Yeah, and it can help you not reinvent the wheel. Like this CLI, for example. It&rsquo;s much easier to use something that&rsquo;s ready to use tend to figure out your own&hellip; I mean, maybe you can wipe code one, but I wouldn&rsquo;t like to wipe code anything message-related. It&rsquo;s kind of a risky area. If you lose a message, it might be permanent.</p> <p><strong>Robert</strong> [51:23]: Fortunately, we have a lot of tests, so it may be a pretty good case for that. So maybe some agent that is running our tests in the background, but it will have a lot of logs to process, so it will be expensive.</p> <p><strong>Miłosz</strong> [51:38]: Gabriel mentioned that it&rsquo;s important to regularly check deadletter queue and analyze the reason of the issues. One more challenge here to have some alerting in place. You might have the deadletter queue in place, but if you don&rsquo;t know the messages arrive there.</p> <p><strong>Robert</strong> [51:59]: I would actually say that I don&rsquo;t fully agree, because I think you shouldn&rsquo;t be checking your metrics. This metrics should let you know, because it&rsquo;s just hard to check hundreds of metrics at the end.</p> <p><strong>Miłosz</strong> [52:13]: I mean, alerting, right?</p> <p><strong>Robert</strong> [52:16]: No, I mean, you should have alerting, but it&rsquo;s making you free from checking the metrics.</p> <p><strong>Miłosz</strong> [52:26]: So, what you don&rsquo;t agree with?</p> <p><strong>Robert</strong> [52:29]: I would say that you shouldn&rsquo;t check metrics. I mean, alerting should notify you about… Oh.</p> <p><strong>Miłosz</strong> [52:35]: Yeah, yeah. Yeah, sure. Basically, you need a way to know that there&rsquo;s something in the letter Q.</p> <p><strong>Robert</strong> [52:42]: Yeah, so it&rsquo;s more like you shouldn&rsquo;t be looking at your metrics like, oh, everything looks… Right, because at some point you just may have too much metrics.</p> <p><strong>Miłosz</strong> [52:52]: Ah, okay. I see now. You meant the regular check part. Yeah, yeah, okay. I see.</p> <p><strong>Robert</strong> [52:59]: Because probably someday it may come that, okay, not look on those metrics and something may happen. So it&rsquo;s better to have something that will notify. But I assume that, yeah, Gabriel had this in mind. But I think it&rsquo;s important to distinguish because I&rsquo;ve seen some components that had metrics, but didn&rsquo;t have alerting. And it&rsquo;s better than nothing, but still, it&rsquo;s At some point, you just may miss something.</p> <p><strong>Miłosz</strong> [53:25]: Unless you have someone looking at dashboards.</p> <p><strong>Robert</strong> [53:28]: You&rsquo;re paying somebody just to, you know, his head is fixed and he&rsquo;s looking all the day on the metrics.</p> <p><strong>Miłosz</strong> [53:34]: I know one company like that.</p> <p><strong>Robert</strong> [53:37]: What&rsquo;s this company? Tell me later. All right. But I think that there is one thing that can help us with this deadletter queue and poison queues. Because, for example, I&rsquo;m personally not that big fan of poison queues, that letter queues, because you need to move these messages back, and it&rsquo;s a bit problematic sometimes. And one solution here may be proper message ordering. So basically, if you&rsquo;re doing proper segregation of the messages, So in other words, a bug for one entity will not block all other entities. You don&rsquo;t need to put this message to Poison Queue because it&rsquo;s isolated and it will just spin for maybe one person, for one customer, for some small part of the system. But if you&rsquo;re ordering all the messages in one big queue, actually one spinning event can block the entire system. And this is a pretty bad situation. In this case, you should have some kind of poison queue. But the problem with ordering is that it&rsquo;s also the science of having good bias. So you shouldn&rsquo;t have one queue for everything, because throughput will be small.</p> <p><strong>Robert</strong> [54:59]: Because one message will block everything. But from the other side, the good thing is that if you just have one big queue, you don&rsquo;t need to care about proper order of events. So you just processing each event one by one and you don&rsquo;t care about ordering. If you have everything unordered, it would be cool because it will not block anything else. But from other side, if you have events like addUser, removeUser and, this will come out of order, your read models that you have may be not right.</p> <p><strong>Miłosz</strong> [55:30]: Or in some special design to make it work.</p> <p><strong>Robert</strong> [55:33]: Yeah.</p> <p><strong>Miłosz</strong> [55:33]: Like versioning or whatever.</p> <p><strong>Robert</strong> [55:37]: Yeah. Again, we also were covering it more in the previous episode, but in my opinion, the most important here is to balance and trying to have as narrow ordering as you can. So not having super big queue, but also not have everything unordered. And it&rsquo;s hard to give one hint how to do that, but probably you need to find the smallest unit that should be ordered and just have ordering with that. I think it&rsquo;s also nicely compatible with the aggregate idea from Domain Driven Design. I think probably at some point we&rsquo;ll also do an episode about that, but basically aggregate pattern from domain-driven design is all about that. How to find the smallest boundary of transactional boundary, but it&rsquo;s kind of connected with that.</p> <p><strong>Miłosz</strong> [56:33]: One challenge here is also to realize you need ordering. Because if you&rsquo;re just starting out, you might not think about it at all. And you might just assume the order in which your publish messages is the same as you receive, which can sometimes be the case. Okay, the next topic here is transactions again, about distributed transactions.</p> <p><strong>Robert</strong> [57:03]: Our favorite ones.</p> <p><strong>Miłosz</strong> [57:05]: So, yeah, this thing is like, you know, many people when they start with distributed systems or even different architecture and they split the operations into a few services, maybe they initially use events, but then realize they actually need this to be in a transaction. And the solution is obvious. We need the transaction to span multiple services. So you just have to orchestrate those handlers to work together, and if one fails, because you have to roll back the rest of the pipeline.</p> <p><strong>Robert</strong> [57:49]: Sounds like a great smart plan. What can go wrong?</p> <p><strong>Miłosz</strong> [57:52]: Exactly. If you are a smart engineer, you should be eager to implement this right away. It&rsquo;s a sign you are a good programmer. But most of the time you should probably reconsider because the complexity will be tragic in the long run. I don&rsquo;t even know what to start with, but maybe the easiest case to consider is, let&rsquo;s say you have three services that communicate through events and you create this kind of distributed transaction or saga on top of them. And then the third one fails, so you roll back everything that happened before but then the rollback fails and what do you do now?</p> <p><strong>Robert</strong> [58:43]: I have PTSD already.</p> <p><strong>Miłosz</strong> [58:44]: So you&rsquo;re stuck with this inconsistency in the system that It can be fixed. You have to go manually to the database or whatever storage you have and fix it somehow.</p> <p><strong>Robert</strong> [58:59]: And later implement compensating transactions for everything. You spend an enormous amount of time on it.</p> <p><strong>Miłosz</strong> [59:07]: So this is something that can be useful in some very specific cases, but most of the time you probably don&rsquo;t want to deal with it.</p> <p><strong>Robert</strong> [59:15]: Very specific, probably it means super, super complicated system and many, many big teams.</p> <p><strong>Miłosz</strong> [59:22]: Probably it would be easier to just merge your three services into one and run a database transaction on top of it, rather than this thing. Or just embrace eventual consistency, like we said before, and just accept that the transaction doesn&rsquo;t need to be here. Yeah.</p> <p><strong>Robert</strong> [59:42]: And again, it&rsquo;s a different case when, for example, it&rsquo;s some very complicated business project that is orchestrating between, for example, three teams where every team is 20 people. In this case, okay, it totally makes sense, but it&rsquo;s a complicated process that probably each service in this case is very complex and you have some orchestration at a very high level. But if your team is 5 people, 10 people, and you are doing such a complicated transaction that is distributed, you can maybe ask yourself a question. Maybe it can be just one service. And I know that it&rsquo;s not sexy to have less microservices, but at least it will save a lot of time on complexity.</p> <p><strong>Robert</strong> [1:00:27]: And, for example, we have a pretty nice exercise about that in our training, Go Event Driven. So, basically, when we&rsquo;re introducing sagas, we&rsquo;re not starting with implementing sagas. We&rsquo;re actually giving exercise with services with saga implementation, and task is to remove saga, just to show how much it can simplify everything when you are removing saga, and how much complexity is added there just because somebody decided to do it as a distributed transaction. So I think it&rsquo;s important when you&rsquo;re using this kind of tool like Saga that can create a lot of complexity to actually know how you can hurt yourself with that and after that implementing that. Not the opposite way because I know it&rsquo;s very easy to start to use that but if you would like to implement it properly it takes a lot of time and it needs to pay back.</p> <p><strong>Miłosz</strong> [1:01:25]: Yeah, so watch out for too complex architecture.</p> <p><strong>Robert</strong> [1:01:33]: We&rsquo;ve been already covering a bit the topic of testing, so obviously it&rsquo;s a bit different than testing synchronous endpoints, so we already mentioned that you need to take in, consideration, eventual consistency, you need to keep in mind that messages might be delivered more than once. The good thing is that you already know that you can have, for example, duplicate middleware that is duplicating everything, but it&rsquo;s also making it a bit harder to test that. So it&rsquo;s probably more similar to problems that you have on frontend. So you are clicking something and you need to check in some intervals if something happened. In this case, it&rsquo;s similar. So you&rsquo;re calling some endpoint, you see some that, for example, accepted, and you need to poll for some data that is happening under the hood. Maybe sometimes you can listen for events, but it&rsquo;s also listening for some events and waiting after it will happen. Obviously, it&rsquo;s adding some complexity. Rather, it&rsquo;s not rocket science. I mean, if you did it once, you can&hellip;</p> <p><strong>Miłosz</strong> [1:02:39]: Yeah, experience helps. And also, if you create some helper, test framework or test library that makes it easy to publish and subscribe to events. It&rsquo;s much more fun to work with.</p> <p><strong>Robert</strong> [1:02:54]: From our experience, component tests are nicely compatible with this approach. We&rsquo;ll link how you can do component testing. We have an article about that,</p> <p><strong>Robert</strong> [1:03:04]: so we&rsquo;ll link it in the materials of this episode. Because we know that a lot of people don&rsquo;t know that something like component tests exists. DLDR, it&rsquo;s nothing between end-to-end and integration tests, and it&rsquo;s fitting this case pretty nicely.</p> <p><strong>Miłosz</strong> [1:03:19]: And lets you test the broker in the same way you run it on production, which is important? Sometimes you also don&rsquo;t have access to the same exact broker you run. That can be challenging. Like if you use cloud-based Pub-Sub, you just can&rsquo;t run it locally, right? You can use emulators, which often are good enough, but for some edge cases, they might be just not supported. And that&rsquo;s the hard part here.</p> <p><strong>Robert</strong> [1:03:52]: But maybe you don&rsquo;t need these cases from the other side.</p> <p><strong>Miłosz</strong> [1:03:55]: Yeah, if not, then it&rsquo;s fine. But in general, it&rsquo;s one more moving part into the local and CI environment, so it can be a bit more tricky. On the other hand, message brokers are usually not super heavyweight, maybe except for Kafka, which is often fun to set up in Docker.</p> <p><strong>Robert</strong> [1:04:20]: And you need a lot of memory for Heap or JVM.</p> <p><strong>Miłosz</strong> [1:04:24]: Yeah, but if you use something more lightweight, Postgres maybe, or Gravity Split Lightweight too, Redis is super lightweight. So it doesn&rsquo;t have to be painful.</p> <p><strong>Robert</strong> [1:04:39]: Also, when you are creating this kind of tests, unfortunately, it&rsquo;s easier to create a flaky test, because something is not happening immediately. And if you&rsquo;re polling for something, there is more, bigger chance of flaky tests, basically. And in this case, it&rsquo;s important to have good observability in place. So the thing that we were discussing at the beginning about tracing, logging is super important, and ensuring that correlation IDs are propagated properly. It&rsquo;s also worth logging correlation ID in tests. So it&rsquo;s easier to correlate the test failure to the logs that you have from Docker containers.</p> <p><strong>Miłosz</strong> [1:05:24]: Oh, yeah. Debugging those tests can be fun as well.</p> <p><strong>Robert</strong> [1:05:28]: But if you have logs, and there are good logs and correlation IDs are propagated properly, it&rsquo;s much, much easier. LiveTip sometimes can also work to just get all those logs, put it to some AI and ask. Sometimes it can really help, basically, when you have a super big wall of text.</p> <p><strong>Miłosz</strong> [1:05:47]: Yeah, we need to integrate it as an agent.</p> <p><strong>Robert</strong> [1:05:51]: Enter Text&hellip;</p> <p><strong>Miłosz</strong> [1:05:54]: You don&rsquo;t see the logs on the output, but just a single summary. It will cost you much.</p> <p><strong>Robert</strong> [1:06:00]: I run me off.</p> <p><strong>Miłosz</strong> [1:06:04]: Coming back to observability, maybe one more thing is we talk about alerting a bit of the data queue, but also a super important thing to monitor and have alerts on is the number of unacknowledged messages or the queue length. Because sometimes you might just quietly keep spinning on one handler and you don&rsquo;t see the impact anyhow in the system. And then later you look at the logs and, oh my god, there&rsquo;s 200,000 messages waiting for to be processed.</p> <p><strong>Robert</strong> [1:06:42]: And also Gabriel put the follow-up comment that he hates systems which are like a fridge.</p> <p><strong>Miłosz</strong> [1:06:49]: I need this on a t-shirt. For context, this is about system that you need to open to check it. So it makes a lot of sense. But I like the end result. I hate systems which are like a fridge. I hate systems which are cold inside. Cool. Should we move on to the summary?</p> <p><strong>Robert</strong> [1:07:27]: Yes, but maybe one more thing also.</p> <p><strong>Miłosz</strong> [1:07:31]: There are a</p> <p><strong>Robert</strong> [1:07:34]: Couple of challenges that we&rsquo;ve covered there. Like every technique, like we mentioned, most of the techniques require some effort and extra cost to implement them. Yeah, and it&rsquo;s not different for event-driven architecture. It requires some special techniques, and it&rsquo;s important to use the right tool for the right problem. If you use event-driven architecture for systems that doesn&rsquo;t require that, it will be just pure over-engineering. You&rsquo;ll pay all the challenges that we just mentioned for nothing. You&rsquo;ll just have one more problem in your Sometimes.</p> <p><strong>Miłosz</strong> [1:08:21]: Your system is just synchronous by nature, and that&rsquo;s fine.</p> <p><strong>Robert</strong> [1:08:25]: Yeah, so your team is not super big, like for example mentioned with sagas. You don&rsquo;t have super big scalability requirements. You are not integrated with an external system. You don&rsquo;t have super big traffic spikes. In this case, maybe doing systems synchronously, it will be just enough. And if you&rsquo;re in doubt, you can always start with Synchronous and migrate to a Synchronous one later. Like we mentioned in the previous episode, if you&rsquo;re using some kind of abstraction over interactions to your system like clean architecture, it&rsquo;s much easier because you have some layer that is allowing you to interact with your system and it doesn&rsquo;t matter if it&rsquo;s from HTTP request, when it&rsquo;s a message. It&rsquo;s making the migration much, much easier. And it&rsquo;s also a common thing to ask your questions if you are doing some decisions. How you can reverse this decision? If it&rsquo;s hard, ask yourself, okay, maybe there is some way how I can make this decision easier to be reversed. It&rsquo;s worth, of course, and it can be helpful with tough decisions.</p> <p><strong>Miłosz</strong> [1:09:36]: Yeah, but if you are not sure, you can just start with sync approach and later migrate.</p> <p><strong>Robert</strong> [1:09:46]: Many traps that we mentioned today. The good thing is that it&rsquo;s not that many traps for other sites. So I think it&rsquo;s important to know about those traps and not reinventing the wheel. Because all the challenges that you may have with event-driven or asynchronous architectures, they are solved problems. And obviously you can try to solve them yourself. But I&rsquo;m quite sure that you will not do it better than how programmers figured it out within the last 50 years.</p> <p><strong>Miłosz</strong> [1:10:21]: So it&rsquo;s the hardest if you just start out. You don&rsquo;t know what you don&rsquo;t know, so you might try some naive approaches or just doing it yourself. You read about event-driven architecture and then you decide, okay, I will use events and I will send them via HTTP or I will have a long-living an HTTP connection between services or something like this. So, sure, this could work, but maybe just use a message broker, which was designed for exactly this thing. And thousands of companies used it on production for 10 years. So it&rsquo;s very likely it will work out for you, if you will.</p> <p><strong>Robert</strong> [1:11:03]: And if you think you can do it better, I would recommend you to start learning what&rsquo;s already there. And maybe you can build something better, but I think to do that, you need to actually know the state of art that is currently there. And I think it&rsquo;s possible to still improve things, but again, I think it&rsquo;s important to have a very strong base. But it&rsquo;s only in case when you have some idea how to improve some stuff. When you&rsquo;re normally working with some product, probably don&rsquo;t have time for renovating the oil and improving stuff. What&rsquo;s already there should be more than enough.</p> <p><strong>Miłosz</strong> [1:11:41]: It&rsquo;s the cool part. That&rsquo;s the difficult thing to do. It&rsquo;s exactly like with sagas. You can convince yourself that it&rsquo;s a good idea to create a distributed transaction framework for your systems. But maybe what you need to do is improve your product a bit to create something people want to buy. Which might be not as interesting in a technical sense. It&rsquo;s probably the right thing to do for your company.</p> <p><strong>Robert</strong> [1:12:12]: But from another side, I think it&rsquo;s also kind of the trap that you might think that doing some complex stuff like SAG or whatever may be interesting. Maybe at the beginning, but later you&rsquo;ll need to maintain that. And I&rsquo;m pretty sure, don&rsquo;t ask me from where, from experience, that later you&rsquo;ll need to maintain that. And it will be not interesting.</p> <p><strong>Miłosz</strong> [1:12:32]: Yeah, you know what, you need to build a framework and then leave the company.</p> <p><strong>Robert</strong> [1:12:37]: And it will be no longer a problem.</p> <p><strong>Miłosz</strong> [1:12:39]: Problem solved.</p> <p><strong>Robert</strong> [1:12:40]: Unfortunately, it&rsquo;s often the case. But trust me, it&rsquo;s much more fun to work with a system that is fairly simple, but you can iterate quickly and add features quickly there. And again, from our experience, using some boring, already well-known techniques and tools, gives a lot of fun at the end, because maintenance of that is much, much easier, and you can also do a lot of stuff.</p> <p><strong>Miłosz</strong> [1:13:08]: Moving fast is fun.</p> <p><strong>Robert</strong> [1:13:11]: For sure. Yeah, seeing how people are using your product and are happy about that, it&rsquo;s also pretty cool. And I know that it&rsquo;s not super technical from one side, but trust me, try this and it&rsquo;s much.</p> <p><strong>Miłosz</strong> [1:13:22]: Much high-stay.</p> <p><strong>Robert</strong> [1:13:23]: And I think it&rsquo;s also nice later when you&rsquo;re looking for a job and you&rsquo;re going to interview, and the interviewer will see that, okay, you&rsquo;re a pragmatic person because you were not doing CV-driven development, but instead you were using the right tool for the right problem. So it&rsquo;s, I think, often a really nice skill, and when we&rsquo;re interviewing people, it was a big bonus point to see that. Okay, even if a person was just using very simple tech, but in proper context, and have big knowledge of the simple tools, it was always great to see it.</p> <p><strong>Miłosz</strong> [1:13:59]: There&rsquo;s no silver bullet.</p> <p><strong>Robert</strong> [1:14:02]: Yeah. Sure. So, probably at the end we should say, is it worth using Evangelion architecture? And, well, unfortunately, it&rsquo;s not that easy. So, as I said, there is no silver bullet, and there are many parameters for that. So, we believe that it&rsquo;s most important to give you a lot of data points so you can decide on your own when using Evangelion architecture is good for you or not, to be prepared, the price that you need to pay, and to see if it&rsquo;s fitting properly and if it will pay back, basically.</p> <p><strong>Miłosz</strong> [1:14:42]: Yeah, there are probably no shortcuts here, no easy answers, since you consider what you work with. You should have no all those challenges before you start, so you are prepared for things like we mentioned about at least once delivery or eventual consistency. It&rsquo;s just a bit different mindsets than working with Synchronous approach. So if you have the basic theory in mind, and you spend some time working with the projects in a Synchronous way, you will be ready for handling it.</p> <p><strong>Robert</strong> [1:15:24]: I think the only silver bullet here is the fact that it&rsquo;s not like it never works or it always works. I think it&rsquo;s important to know what tools you have So not be a worker that only knows how to use Hammer, because this is not the best worker. So you should just know what tools are available and be prepared to use them in a proper context. Because using Synchronous Architecture Everywhere, it will not work. Using Asynchronous Architecture Everywhere, it will also not work. It&rsquo;s just about finding the right tools for the right problem, because Opposite is not the best.</p> <p><strong>Miłosz</strong> [1:16:07]: Yeah, so if you want to learn more about Event Driven architecture in general, or in Go specifically, we have our training of Event Driven on sale for one more week. So this is a place where you learn hands-on with exercises, and we do it for the first time in a cohort-based way, so everyone learns together. You can still join us for one more week and then we will start an intensive month of learning together.</p> <p><strong>Robert</strong> [1:16:40]: Next one probably in half year or so. So, if you&rsquo;re just listening to that and you&rsquo;re listening for this now, we are super lucky. If you&rsquo;re listening for this episode later, sorry. You can sign up to the waitlist.</p> <p><strong>Miłosz</strong> [1:16:58]: Okay. Before we move to the Q&amp;A, it&rsquo;s probably time you hit that subscribe button and leave a comment or a review, if you listen to the Minna Podcast application. And as Robert mentioned, you can subscribe to our newsletter on our blog, and we will let you know about the next episodes. Actually, we don&rsquo;t have a topic for the next episode this time.</p> <p><strong>Robert</strong> [1:17:21]: It&rsquo;s a secret.</p> <p><strong>Miłosz</strong> [1:17:22]: It&rsquo;s a surprise.</p> <p><strong>Robert</strong> [1:17:24]: Yeah, but the one thing that we already know is that we&rsquo;ll be changing the form of this live podcast a bit. So basically, we&rsquo;ll do two more live episodes and later we&rsquo;ll switch to sometimes doing live episodes. Most of the time, it will be pre-recorded because of a couple of technical challenges of doing it live. Like some workers that are super loud. Fortunately, our mics are pretty good and noise-canceling, but it&rsquo;s super loud here.</p> <p><strong>Miłosz</strong> [1:17:57]: It&rsquo;s 15 minutes before the start. They started drilling, so it&rsquo;s super fun. We also want to make the sessions a bit more interactive. If you do live sessions, we will try to figure out something more interesting for you to follow.</p> <p><strong>Robert</strong> [1:18:09]: I think we&rsquo;ll be running some Q&amp;A sessions, definitely, between pre-recorded sessions. And we&rsquo;ve also seen that most of you are listening to this after recording, so we&rsquo;ll try to balance it a bit. And I think at the end also the content will be a bit better, because trust us, it&rsquo;s not easiest to go over the scenario of the episode live, because we need to just keep flowing. But again, we&rsquo;ll still have live sessions, so no worries, it will be just a slight change. But again, we&rsquo;ll have two more live episodes, so don&rsquo;t forget to join us.</p> <p><strong>Miłosz</strong> [1:18:44]: And the short summer break and then we&rsquo;re back.</p> <p><strong>Robert</strong> [1:18:50]: Correct. Alright, so time for Q&amp;A, I think.</p> <p><strong>Miłosz</strong> [1:18:58]: I think we discussed most of the comments before because they were relevant to what we discussed at the moment. There&rsquo;s a recent question from Pedro. It&rsquo;s about starting a monolith and breaking down into smaller surfaces and then adding events.</p> <p><strong>Robert</strong> [1:19:23]: AKA monolith first.</p> <p><strong>Miłosz</strong> [1:19:25]: And this is something we did once, more than once. Once specifically. So we had this project when we started with a quite small team, but then there was a perspective that it will be much bigger in the future. didn&rsquo;t happen.</p> <p><strong>Robert</strong> [1:19:46]: At least we didn&rsquo;t lose time for doing microservices.</p> <p><strong>Miłosz</strong> [1:19:49]: We wanted to be ready to scale but we also didn&rsquo;t want to create distributed transactions at the beginning. So we started with our monoleaf and quite simple setup where we have modules that are completely separated and connected with code, basically, with interfaces between each other. So you can think like, you know, imagine microservices that expose HTTP API. So it&rsquo;s the same idea, but instead you have modules that expose interfaces in code. And that&rsquo;s, as you can see, that&rsquo;s pretty much the same thing, just different transport. In this case, there is no transport, because we don&rsquo;t need it. But we try to do it in way that, if needed, we could take any module out and it would be quite simple to deploy separately.</p> <p><strong>Robert</strong> [1:20:47]: But from the other side also, if your team is not that big, so like 10-15 peoples, maybe 20, it shouldn&rsquo;t be that big problem, so you should be able to split it from anything. Okay, it depends on time frame basically.</p> <p><strong>Miłosz</strong> [1:21:04]: But we like to think of Monolith versus Microservices question as deployment, not about project structure.</p> <p><strong>Robert</strong> [1:21:15]: And also about some kind of social concept or concept of team. So basically, when you need microservices, you should probably need them because of how big is your team. So basically, you have one project and you have 50 people, for example, committing to one repository and deploying it.</p> <p><strong>Miłosz</strong> [1:21:34]: Yeah, maybe they block each other, maybe the CA becomes complicated.</p> <p><strong>Robert</strong> [1:21:39]: Somebody is introducing some flaky tests and 50 people are not able to deploy for an entire day. So yeah, this is a place where you need to split. But if your team is five, it&rsquo;s not an issue. But anyway, if you&rsquo;d like to see the implementation of the project that Miłosz mentioned, how we did it, we have blog posts about that. So we&rsquo;ll include it in the episode materials tomorrow. And also, maybe one interesting thing. We also used to do the opposite one. We had microservices, and we did the microservices. We actually connected multiple microservices into one monolith, because it was just too granular, and it was too complicated to communicate between them. And having one service with everything, so similar situation to what we had with Saga, it would just make everything much easier.</p> <p><strong>Miłosz</strong> [1:22:34]: And Gabriel mentioned that onion architecture can help when you use the DD from the one. So onion or clean or hexagonal or whatever, layered architecture in general can help. For bigger projects especially. Also not silver bullets. A separate episode about this. So in this setup I mentioned this is exactly this idea that following hexagonal terminology let&rsquo;s say the modules communicate via adapters and that&rsquo;s it it&rsquo;s very similar to communication with any RPC just with non-network connection and</p> <p><strong>Robert</strong> [1:23:18]: Also worth mentioning you don&rsquo;t need to use DDD for on your architecture, you don&rsquo;t need to always use DomainLayer. And it&rsquo;s also totally fine. Because also often people join this and sometimes it&rsquo;s creating some monster and people say, oh, architecture is overcomplicating. But often it&rsquo;s because of that, because you don&rsquo;t need to always introduce DomainLayer.</p> <p><strong>Miłosz</strong> [1:23:42]: Yeah, and each module can have a separate structure. One can use CleanArch, one can use DD and some can be one file.</p> <p><strong>Robert</strong> [1:23:51]: Or about that in the episode about clean architecture.</p> <p><strong>Miłosz</strong> [1:23:56]: Yeah, so Gabriel mentioned you can move faster with M&amp;Oleaf. If there&rsquo;s a small team,</p> <p><strong>Robert</strong> [1:24:02]: Then definitely.</p> <p><strong>Miłosz</strong> [1:24:03]: If there&rsquo;s a small team, a single developer especially, if you&rsquo;re a single developer and you have microservices, something is wrong.</p> <p><strong>Robert</strong> [1:24:12]: It&rsquo;s not your fun project after hours.</p> <p><strong>Miłosz</strong> [1:24:16]: Yeah. We love, but we also worked with teams where there were more microservices than people on the team. And it&rsquo;s not fun at all.</p> <p><strong>Robert</strong> [1:24:29]: Probably it was fun at the beginning, but later it was the opposite.</p> <p><strong>Miłosz</strong> [1:24:35]: At the end, DDD is the best way to go, says Pedro. It depends, as always.</p> <p><strong>Robert</strong> [1:24:43]: I&rsquo;d say that probably depends which part of Domain-Driven Design, because many people are actually kind of not splitting Domain-Driven Design in two important parts. Domain-Driven Design are strategic and tactical patterns. Most people are talking about the tactical one that are in the code, But there is also a very important part of Domain Driven Design named strategic patterns. And actually, it&rsquo;s totally not connected to the code. So it&rsquo;s more about gathering requirements, talking with business, ensuring that the problem that you are solving is the right problem. And probably you should do it always, even if you are not naming it Domain Driven Design. Domain Driven Design is just giving you some tools for that. So if you are talking about strategic patterns, totally, you should do that, even if you are not naming it Domain Driven Design. What tactical patterns? Maybe. Sometimes.</p> <p><strong>Miłosz</strong> [1:25:36]: It all depends on the service or the module. What does it do? There&rsquo;s a service that lets you upload your avatar. You probably don&rsquo;t need to involve your stakeholders to discuss what file formats are supported or something like that.</p> <p><strong>Robert</strong> [1:25:53]: And have a separate service for that. We&rsquo;ve seen it once.</p> <p><strong>Miłosz</strong> [1:25:57]: So, yeah. Once again, balance is important. And if you have this modular monolith with separate modules that are independent, it helps, because you can have one module that is super technical and you don&rsquo;t care about the design here, and another that&rsquo;s pure business rules and you spend a lot of time on design. So, modularization is the way to go. Probably not in the form of microservices, but some kind of separation is great to have.</p> <p><strong>Robert</strong> [1:26:39]: It&rsquo;s also, in general, starting with microservices, there are cases, but probably there are cases when you are starting a pretty big development team, or for some reason you know that your development team will be big. And it&rsquo;s not like yeah, yeah, totally, our, theme will be big and product will be super scalable every startup thinks this and probably five percent is really ending up there so and.</p> <p><strong>Miłosz</strong> [1:27:07]: It&rsquo;s also not that easy to design a system that would be</p> <p><strong>Robert</strong> [1:27:11]: And be profitable 90 of them are spending time on setting up kubernetes and burning all the money and they are failing later five other five percent is failing because product is bad, and other 5% is lucky. And didn&rsquo;t spend most of the time on setting up Kubernetes and Istio.</p> <p><strong>Miłosz</strong> [1:27:33]: You mentioned this in the first episode as well. It&rsquo;s very hard to design a system that will be ready for all the future challenges. It&rsquo;s better to evolve over time. Because you just can&rsquo;t foresee how things will change.</p> <p><strong>Robert</strong> [1:27:48]: Easier to say than do, but again, check first episode. We have a couple nice ideas how to do there.</p> <p><strong>Miłosz</strong> [1:27:56]: I think there are no more questions. Pedro says we are doing a great job. Thank you very much.</p> <p><strong>Robert</strong> [1:28:03]: We are doing our best.</p> <p><strong>Miłosz</strong> [1:28:04]: Thanks for joining us. Thank you for all the questions. We will see you in the next episode about a secret topic.</p> <p><strong>Robert</strong> [1:28:14]: Exactly. In two weeks. So we&rsquo;ll let you know in our newsletter. Just a reminder so go event driven you&rsquo;ll be just able to join for another week next time or I&rsquo;ll be healthier or so.</p> <p><strong>Miłosz</strong> [1:28:28]: If you are listening later you can join the waitlist</p> <p><strong>Robert</strong> [1:28:33]: Or go in an evening if you don&rsquo;t go don&rsquo;t know go yet because maybe a lot of you doesn&rsquo;t know go and yeah we&rsquo;ll.</p> <p><strong>Miłosz</strong> [1:28:42]: Leave links in the description yep oh right Thank you everyone for joining us for the questions. See you in two weeks.</p> <p><strong>Robert</strong> [1:28:52]: Thank you, Milosz. Thank you, everyone.</p> <p><strong>Miłosz</strong> [1:28:54]: Bye.</p>Synchronous vs Asynchronous Architecturehttps://threedots.tech/episode/sync-vs-async/Wed, 28 May 2025 16:00:00 +0000https://threedots.tech/episode/sync-vs-async/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Start with synchronous architecture by default</strong> - it&rsquo;s simpler to understand, debug, and maintain for most use cases</li> <li><strong>Async architecture improves scalability and resilience</strong> - message queues and events help handle traffic spikes and failures</li> <li><strong>Design matters more than the technology choice</strong> - tight coupling creates the same problems in both sync and async approaches</li> <li><strong>Consider team experience</strong> - async architecture require more experienced teams and better tooling to handle new challenges</li> <li><strong>Adjust as your system grows</strong> - external APIs, heavy operations, or the need to handle failures gracefully are good use cases</li> <li><strong>Hybrid approach</strong> - use both sync and async where they fit best, rather than forcing one over the other</li> </ul> <h2 id="introduction">Introduction</h2> <p>In this episode, we discuss when to choose synchronous versus asynchronous architecture for backend systems.</p> <p>We talk about the trade-offs between simple, predictable sync communication and the complexity but resilience of async approaches using message queues and event-driven architecture.</p> <p>Instead of picking one approach over another, we focus on understanding when each makes sense and how to avoid common pitfalls like distributed monoliths and over-engineering.</p> <h2 id="show-notes">Show Notes</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fevent-driven%2F" target="_blank">Go Event-Driven training</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill" target="_blank">Watermill</a> - our open-source Go library for working with message streams</li> <li><a href="proxy.php?url=https%3A%2F%2Fleanpub.com%2Fintroducing_eventstorming" target="_blank">Event Storming</a> - a design technique with a great unfinished ebook by Alberto Brandolini</li> <li>CQRS (Command Query Responsibility Segregation) - a pattern that works well with both sync and async approaches <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DGdLu7FQBrdk" target="_blank">Killing the legacy and other CQRS stories</a> - Miłosz&rsquo;s talk on this topic</li> </ul> </li> <li>Our <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbasic-cqrs-in-go%2F" target="_blank">CQRS article</a> and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2F" target="_blank">Server-Sent Events post</a></li> <li>Message brokers mentioned: RabbitMQ, Kafka, NATS, Google Cloud Pub/Sub</li> <li>Event schemas: Protobuf, Avro, CloudEvents</li> <li>Event Sourcing - a pattern mentioned in context of recreating state from events</li> <li>Clean Architecture/Hexagonal Architecture - architectural patterns mentioned for making sync/async migration easier</li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>What&rsquo;s the key difference here is the fact that when you&rsquo;re publishing some message, you basically don&rsquo;t care how it will be processed, by whom it will be processed. And it&rsquo;s nice because it&rsquo;s decoupling you from the consumers.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>The bigger the system, the worse it becomes. For example, if many services depend on this one, which is a bottleneck, it can kill your entire platform.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>Decoupling is not always good. It&rsquo;s similar like with Don&rsquo;t Repeat Yourself. Often, don&rsquo;t repeat yourself may be a good idea, but not always. If you go into extreme, it may be problematic.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>The worst case here is when you ignore the issue. You assume the database commit never fails. And it might be true most of the time, but usually if something can happen, it will happen on production sooner or later.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>Event is information about the fact that happened. So it&rsquo;s not something that will happen, may happen. No, it&rsquo;s information about something that happened and it cannot fail.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>It&rsquo;s just counterintuitive, because you think that splitting things makes things decoupled. But it&rsquo;s not really the case. It depends on how you communicate, really.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSRjTQCDpXVo%26amp%3Bt%3D0s">00:00:00 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSRjTQCDpXVo%26amp%3Bt%3D60s">00:01:00 - Synchronous architecture</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSRjTQCDpXVo%26amp%3Bt%3D720s">00:12:00 - Asynchronous architecture</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSRjTQCDpXVo%26amp%3Bt%3D1295s">00:21:35 - Error handling and resilience</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSRjTQCDpXVo%26amp%3Bt%3D1683s">00:28:03 - Challenges with async</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSRjTQCDpXVo%26amp%3Bt%3D2273s">00:37:53 - Common anti-patterns</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSRjTQCDpXVo%26amp%3Bt%3D2849s">00:47:29 - Distributed monoliths</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSRjTQCDpXVo%26amp%3Bt%3D3444s">00:57:24 - Message ordering</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSRjTQCDpXVo%26amp%3Bt%3D3954s">01:05:54 - How to choose</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSRjTQCDpXVo%26amp%3Bt%3D4625s">01:17:05 - Getting started and migration strategies</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSRjTQCDpXVo%26amp%3Bt%3D4905s">01:21:45 - Q&amp;A session</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Miłosz</strong> [0:00]: Should we make this async? It&rsquo;s one of the questions that seems simple,</p> <p><strong>Miłosz</strong> [0:04]: but can make a huge difference in the long run. If you get it wrong, you&rsquo;ll spend months fixing the mess. How to decide? You should not just guess. I am Iwos.</p> <p><strong>Robert</strong> [0:13]: And I&rsquo;m Robert. And this is No Silver Bullet Live podcast, where we discuss mindful backend engineering. We spent almost 20 years working together across different projects and teams. Learn that following advice like always do X or never do Y doesn&rsquo;t work and can limit your growth. In this show, we share multiple perspectives that will help you to make smart choices and grow you into principal engineer level.</p> <p><strong>Miłosz</strong> [0:38]: And if you have any follow-up questions, you can leave them in the chat. We can pick them up during the discussion if they are relevant. Otherwise, we will have a Q&amp;A session at the end. So we will go through all the questions.</p> <p><strong>Robert</strong> [0:54]: And to be on the same page, today we&rsquo;ll discuss async in context of architecture, not async in context of your code. So we&rsquo;ll not discuss threads, we&rsquo;ll not discuss goroutines, we&rsquo;re not discussing code level, async, we&rsquo;ll discuss the higher level architecture, so related to pubs, queues, and similar systems.</p> <p><strong>Miłosz</strong> [1:21]: Yeah, maybe the context would be we have two services, microservices or programs in general, two or more, and we want them to talk to each other over the network in some way. That&rsquo;s what we want to focus on today. And usually, it works like one of the services asks for something, makes a request, and then waits for the result. And you have to handle the error in some way, if it happens, or basically check the status code, which is the main difference between sync and async.</p> <p><strong>Miłosz</strong> [2:08]: And yeah maybe one specific case is where you fire and forget so you make the request and then you don&rsquo;t care about what happens later in the other service but I think we can agree not to talk about this case today because it&rsquo;s pretty rare but you don&rsquo;t care about what happened on the other end if you lose the status code, the response, you don&rsquo;t know if we can continue, or should you let the user know what happened? Let&rsquo;s skip this one for now.</p> <p><strong>Robert</strong> [2:55]: If you are talking about synchronous architecture, as contrast, in most cases we are talking about our gRPC, HTTP architecture, so in other words, you&rsquo;re calling from one place, you&rsquo;re calling one service, and within the request context, some operation is done. So maybe some database, something is written to the database, maybe some transaction is done, maybe some money is moved, or something like that, so basically&hellip;</p> <p><strong>Miłosz</strong> [3:27]: Yeah, transferred to another account. Something happens in the other service.</p> <p><strong>Robert</strong> [3:35]: Yeah, and basically you know that if this request returned you the some 200-like HTTP code or gRPC status code that it succeeded, you know that it succeeded. And it&rsquo;s nice because it&rsquo;s the typical architecture that we are building. So if you are learning how to code, usually you are using synchronous architecture. You are not using event-driven architecture or as a broker, because it&rsquo;s probably something more complex for somebody who&rsquo;s learning. I guess, Milos, when you were learning how to code, you started with building some synchronous APIs, probably.</p> <p><strong>Miłosz</strong> [4:16]: Yeah, I started with no APIs at the very beginning.</p> <p><strong>Robert</strong> [4:20]: When you were learning programming, there were no APIs yet.</p> <p><strong>Miłosz</strong> [4:24]: Well, I didn&rsquo;t learn microservices first, but it&rsquo;s for sure the first method of communication you can see while learning. It&rsquo;s probably the most natural. That&rsquo;s one of the upsides of using synchronous architecture, that it&rsquo;s quite predictable in the flow. It&rsquo;s kind of like the method call goes over the network. and when you read the code, you don&rsquo;t really need to know what happens underneath. I think there are some frameworks that try to do it for you, even that you can distinguish between&hellip; Like the RPC calls basically look like normal function calls. That&rsquo;s a separate topic. But the point is, that&rsquo;s one good thing about SYNC. That it&rsquo;s pretty easy to understand what&rsquo;s going on in your function.</p> <p><strong>Robert</strong> [5:25]: And it&rsquo;s natural for you, so the entry point for that is much smaller.</p> <p><strong>Miłosz</strong> [5:30]: If an error happens, or an exception is thrown, or whatever, you know it will exit the function, just like any other function call. That&rsquo;s one of the pros.</p> <p><strong>Robert</strong> [5:44]: If you were working as a software engineer for longer, As usual, you may prefer simple approaches, because simple approach doesn&rsquo;t require any additional complexity. But, well, as usual, it doesn&rsquo;t cover all the possible use cases. So, unfortunately, there are some cases when the simple approach of synchronous communication is not enough. So, for example, you are calling some external service, it can be other team, it can be other company, And for some reason this is working slowly or even doesn&rsquo;t work. It&rsquo;s not the best situation, but this is where we are often. So we need to call some other team services or some external services. Doing it synchronously sometimes may be hard. I remember when we were working with some, for example, invoicing software that was outsourced and it was sometimes working, sometimes doesn&rsquo;t work, but we were not able to change that because it was some contract and we were being obligated to use that.</p> <p><strong>Miłosz</strong> [6:47]: It might be even an external API completely out of your company. There&rsquo;s some third-party service you use, and you need to use it, but you have to deal with the API, whatever it takes.</p> <p><strong>Robert</strong> [7:02]: Yeah, and sometimes, for example, also some real-world examples, sometimes you may be integrated with some external marketing systems. And for some unknown reason that I don&rsquo;t know why, but often those systems are not that stable. And sometimes it&rsquo;s part of customer registration flow, but from some other side, you need to send some data when you&rsquo;re registering customer. And, well, you cannot risk that basically it will break customer registration</p> <p><strong>Robert</strong> [7:30]: flow because it&rsquo;s part of the critical flow. So it&rsquo;s also pretty problematic and in synchronous way, it will be hard to handle.</p> <p><strong>Miłosz</strong> [7:41]: Yeah, it&rsquo;s one of the limits. I think it&rsquo;s often a question people ask us about designing the architecture, so They recognize that they have a database transaction that does something like saves the user, let&rsquo;s say, in the database. And does a few other things. And they also need to call this, let&rsquo;s say, marketing software via API. And they are confused how to model it, because the API call, should it be inside the transaction, should it be outside the transaction, how to do it. And basically, there&rsquo;s no good way to handle this in the synchronous approach way.</p> <p><strong>Robert</strong> [8:24]: Yeah, you can choose basically what&rsquo;s less important. For example, okay, we&rsquo;ve failed to send some marketing information, integration to some marketing system. Well, or maybe we should, it&rsquo;s something critical, and maybe we should interrupt our registration function flow. It depends, unfortunately. But, well, when you are doing it in a synchronous way, you need to do those trade-offs.</p> <p><strong>Miłosz</strong> [8:48]: The worst case here is when you ignore the issue. Like, you assume, okay, but database commit never fails.</p> <p><strong>Robert</strong> [8:56]: Or the system will always work.</p> <p><strong>Miłosz</strong> [8:58]: Yeah, and it might be true most of the time, but usually if something can happen, it will happen on production sooner or later. And what I really don&rsquo;t like about ignoring this challenge is that you end up with inconsistency in the system. And fixing this is super hard. First of all, you need to figure out if you have inconsistency. And if you have no proper monitoring in place, you might never know. You can see it weeks later or months later.</p> <p><strong>Robert</strong> [9:37]: Or even worse, let&rsquo;s imagine that something on the other side, we assume that it&rsquo;s stable, it&rsquo;s always working, this external system of other company, and you have some big promotion, big marketing, and you have hundreds of people trying to register, and the system is down. And I guess that a lot of people may be pretty upset because somebody spent some horrible amount of money on marketing, preparing that. And because of this assumption, it&rsquo;s not working. And often, you know, it&rsquo;s important integration that if you disable it, you&rsquo;ll find out that, okay, it&rsquo;s hard to track what&rsquo;s happening with customers and it&rsquo;s a super big problem for everybody.</p> <p><strong>Miłosz</strong> [10:17]: Yeah, so it can easily become a bottleneck.</p> <p><strong>Robert</strong> [10:20]: Yeah, and if you are talking about bottlenecks, the other problem that often you have with synchronous architecture is problem with scalability. So often you have some operations that are heavy and they are working fine when you don&rsquo;t have super big traffic, but hopefully your product that you&rsquo;re working on at some point may have some bigger, spikes of traffic, and you may have more customers. And, well, if it&rsquo;s not designed in a proper way, and it&rsquo;s just done everything asynchronously, at some point, people will just start to see errors, because the operations that they are trying to do cannot be done, because you just don&rsquo;t have enough resources. And if you&rsquo;re doing it synchronously, you cannot do much about that, if it&rsquo;s a critical part of the functionality.</p> <p><strong>Miłosz</strong> [11:08]: And the bigger the system, the worse it becomes. For example, if many services depend on this one, which is a bottleneck, it can kill your entire platform.</p> <p><strong>Robert</strong> [11:24]: I&rsquo;ve seen that many systems are trying to do some workarounds for that, like runjobs or or even worse, database triggers. It&rsquo;s some kind of workaround for that, but from our experience, it doesn&rsquo;t scale at the end. Not only in terms of resources, but also in terms of maintenance. It&rsquo;s a mess at some point of complexity, basically.</p> <p><strong>Miłosz</strong> [11:56]: So, the other approach is asynchronous architecture, and we don&rsquo;t talk about the code level. So, stuff like callbacks, or promises, or director model, whatever you use in your code, it&rsquo;s out of scope for now. Coming back to the context of two services that talk to each other, I would say that the goal of a synchronous architecture in this context is that we want to execute this in the background, this request, whatever it is, but also we don&rsquo;t want to lose the information about the result. And even if the result is empty, maybe there is no payload. But the status message or the fact if an error happened or not is an important fact. So we want to handle it sometime in the future, but we don&rsquo;t want to wait for it to complete in the current function.</p> <p><strong>Robert</strong> [13:00]: And again, some poor man&rsquo;s approach may be putting it to the table and doing some batch jobs, but while it can work, it&rsquo;s not a perfect approach, because Because at some point, if you have more jobs to process, you may end up with just taking more time than the entire day have. So paralyzing of that may be a challenge. And it&rsquo;s often also delaying when it&rsquo;s processed, basically. Yeah.</p> <p><strong>Miłosz</strong> [13:33]: So Gabriel mentioned in the chat that it&rsquo;s easier to debug. I think this was about a synchronous approach. Yeah, that&rsquo;s a super important point. If you have a classical function that goes one after the other of the steps, it&rsquo;s quite easy to understand what&rsquo;s wrong and debug. With icing it&rsquo;s a bit more complicated when you get into that. So, what solutions do we have in terms of the architecture? I think maybe what many people may be familiar with is task queues, which is usually a simple FIFO first in first out queue of tasks that some worker processes pick up from. And it&rsquo;s a pretty easy solution and can work well for simple use cases when you just want something to be done in the background and forget about it. The downside might be that if you have a more complex setup, it&rsquo;s probably not enough to handle it well, but it can work.</p> <p><strong>Robert</strong> [14:50]: The two cases that I would have in mind when queues may be not helpful, compared to some event-driven architecture, is a case when you have more teams, so it doesn&rsquo;t help you with organizational scalability. So basically, you have one queue, and it&rsquo;s kind of similar to sharing database. So if multiple teams are sending messages to queue, and one message is spinning, it&rsquo;s blocking other teams. So it&rsquo;s not best experience. And the second thing is that if it&rsquo;s order queue, even if you have multiple queues and you have some high priority or low priority queues, it&rsquo;s also pretty limited in terms of performance.</p> <p><strong>Miłosz</strong> [15:36]: Yeah, but for a simpler architecture, it might be a pretty good solution. For sure, better than this, hoping it won&rsquo;t break in a synchronous way. But a step forward in this are messages in general, and often using a message broker in the middle, like or a PubSub. So this is a similar approach, but instead of having workers, pulling tasks and doing one thing. We connect the services using messages. And there&rsquo;s usually many ways you can build this topology, like who receives the message, and it depends also on the infrastructure you use. You might be familiar with message brokers like RobidMQ or Kafka, or the cloud-based ones, and they all have a bit different configuration. But the point is, it allows you to communicate between processes using those messages.</p> <p><strong>Robert</strong> [16:45]: Yeah, and what&rsquo;s the key difference here is the fact that when you&rsquo;re publishing some message, you basically don&rsquo;t care how it will be processed, by whom it will be processed. Basically, emit some thing, usually event, and this event can be processed, maybe not processed, it may be spinning for a while, you don&rsquo;t care. And it&rsquo;s nice because it&rsquo;s decoupling you from the consumers.</p> <p><strong>Robert</strong> [17:13]: And it&rsquo;s adding the thing that I mentioned in terms of queues that it&rsquo;s not handled by queues. So it&rsquo;s creating some kind of way that helps with scaling multiple teams. So, if you are decoupled from customers of your teams, basically tens of, hundreds of teams can listen to your events and you don&rsquo;t care if it will fail or not.</p> <p><strong>Miłosz</strong> [17:40]: So, this is if you use events, right? So, an event is kind of a message and it states a fact that happened already. This is a big, I think, more like a mindset shift rather than a technical thing. Because you can easily use messages, but if you send, for example, requests like in RPC as other messages, doesn&rsquo;t give you the same decoupling benefits.</p> <p><strong>Robert</strong> [18:16]: Yeah, I think it&rsquo;s nicely how it works out of the box with events. I mean, how it fits this model nicely. Events is information about the fact that happened. So it&rsquo;s not something that will happen, maybe happen. No, it&rsquo;s information about something happened and it cannot fail because it already happened. So you can&rsquo;t say, oh no no it didn&rsquo;t happen it happened and it&rsquo;s nice because it&rsquo;s in some way immutable so everybody can listen to that they cannot say it didn&rsquo;t happen yeah and.</p> <p><strong>Miłosz</strong> [18:48]: They can&rsquo;t not agree yeah it i think it simplifies the design often right because let&rsquo;s say you have this process after i don&rsquo;t know someone placing an order and instead of this one process figuring out tell other things that need to happen in the entire system, like send a receipt, send an email, notify the sales team, whatever.</p> <p><strong>Miłosz</strong> [19:16]: Can we manage your guess like that? You can just send a single order place event from this system, and then other systems react to this.</p> <p><strong>Robert</strong> [19:27]: We&rsquo;ll go to anti-patterns a bit later, but I think one of very often seen anti-patterns is passive aggressive events on the pattern. So usually when you are creating an event, it should be verb in past tense. So, order placed, but it shouldn&rsquo;t be event like order should be placed. Because in this case, it&rsquo;s more like a command, and commands are more fitting the queue system. So you&rsquo;re sending this queue, and one consumer is subscribing to that, and maybe order will be placed. But maybe it will be not placed, because you don&rsquo;t have enough items in an inventory, for example.</p> <p><strong>Miłosz</strong> [20:11]: It&rsquo;s a funny thing, because it&rsquo;s mostly about naming things. There&rsquo;s nothing much different about an event than a regular message. The difference is just the payload and how you call it. But if you pick the right names, the design becomes much easier to follow.</p> <p><strong>Robert</strong> [20:29]: I think it&rsquo;s interesting because many people are looking at that like, ah, naming doesn&rsquo;t matter, whatever, name it XYZ and it will be fine. But I will not agree after seeing how it can help, or it can not help if it&rsquo;s not properly designed.</p> <p><strong>Miłosz</strong> [20:46]: Yeah, it&rsquo;s about mental load, pretty much. How easy it is to understand the design. And then it&rsquo;s also easier to debug issues or extend it.</p> <p><strong>Robert</strong> [20:57]: And I remember that some people asked about that in Watermill, so the library that we&rsquo;re maintaining in Go. And what&rsquo;s the difference between command and event? because basically API is very similar, but there are some really small reasons. And yeah, basically you can send command as event, but again, it&rsquo;s just practical to follow this because it will be easier later.</p> <p><strong>Miłosz</strong> [21:25]: Maybe let&rsquo;s come back a bit to messages in general, not just events. And I think one thing that&rsquo;s pretty different in how you approach asynchronous way is error handling.</p> <p><strong>Robert</strong> [21:40]: Yeah, and I think it&rsquo;s a cool thing that you get out of the box, we can say, with event-driven or many asynchronous approaches that is basic or un-message broker. Because if you are doing operations in the background, for example, instead of emitting an event or sending a message, when you are just doing it within a process, you never know if this process will not die. Because, for example, Kubernetes will say, you should be that. You never know. It can happen. And I know that in some smaller systems that are not mission critical, it doesn&rsquo;t matter. You lose some operation, maybe somebody from operations team will be upset, but that&rsquo;s it. But if you&rsquo;re building some systems when this consistency really matters, and I would say that in most cases it is, if you are not storing things that you would like to process in Message Broker, in some cases it will just happen that your process will die and some operation will be not processed. And when you&rsquo;re sending this event or message to your message broker, it&rsquo;s up to the message broker to ensure that it&rsquo;s processed. And only if you will confirm that it was processed, the message will be deleted.</p> <p><strong>Robert</strong> [23:07]: And it&rsquo;s especially useful when you have some temporal outage. So imagine that your some external API was down for a couple minutes, your database was down for a couple minutes. In this case, when you are subscribing to the messages in MessageBroker, it&rsquo;s not an issue. So messages will accumulate over the time, and it will just auto-resolve after the time. And I think it&rsquo;s pretty cool when we work with some systems, when we&rsquo;ve seen that, okay, some outages of external systems was happening, but it was not a big deal. So we sometimes receive non-critical alert like, oh, we have some messages that are stuck, but it was just stuck for a couple of minutes and later you can see how the chart is going down. Always pretty possible to see that.</p> <p><strong>Miłosz</strong> [23:52]: It can also be a bug in your code. You can mess something up and then the direct requests fail. So in synchronous approach all services calling your service this broken service don&rsquo;t have much choice what to do like best they can do is probably return the error up the stack to the previous caller and then up to the user show on the UI right but if you use messages for this then yeah this this thing won&rsquo;t be done in the moment, but then you can deploy a fix for the code issue, and as I said, all the messages can be processed again. Auto-healing is pretty cool to have. I think it&rsquo;s also another factor that simplifies the mental model, because you don&rsquo;t need to care about error handling in each scenario, how to do it, how to make this nice for the user to report or&hellip; Get over with. Instead, you know that you can just retry, and most of the time it will work fine.</p> <p><strong>Robert</strong> [25:14]: And another similar situation is when you are receiving bigger traffic because of many reasons. So many people went to your website or you have some attacks sometimes. It&rsquo;s also kind of similar situation because normally you would not be able to process all those requests, but if you just receive a lot of messages, you can accumulate them and process them in order that you can have. And it&rsquo;s also helping you to scale it with that, because if you have some way of autoscaling your application, you can basically configure it in multiple ways. But when this autoscaler will detect that, I know you are using a lot of CPUs or there are more messages to be processed, you can just autoscale more, process those messages and downscale your application back. And it&rsquo;s kind of almost out of the box, we can say.</p> <p><strong>Miłosz</strong> [26:08]: Yeah, not always ideal. It depends if you need this thing done right away or not. But if you can wait a while, it&rsquo;s much simpler than scaling up to handle all incoming requests.</p> <p><strong>Robert</strong> [26:22]: Yeah, and again, I think many people are mentioning event-driven or in-general asynchronous architectures in terms of technical and performance scalability, but we have also the perspective of organizational scalability. So you can basically integrate more teams over events, and thanks to the decoupling, you can add more teams and they can just work more independently.</p> <p><strong>Miłosz</strong> [26:53]: If you design them well that&rsquo;s a prerequisite obviously because there are also lots of downsides of AC Cloud architecture maybe not downsides but challenges you need to be aware of right We mentioned a couple of times already that the design is important, so that&rsquo;s one thing. It&rsquo;s more complex in general and you need to understand the rules well. Ideally, the entire team would be familiar with what the stuff works. So the more experienced team, the better for this kind of architecture.</p> <p><strong>Robert</strong> [27:35]: Yeah, but as usual, so you have more complex problems to solve, it&rsquo;s better to have more experienced team to handle that.</p> <p><strong>Miłosz</strong> [27:44]: Yeah, because for some platforms you can just use only a synchronous approach and be done with it. So at some point you just need to move to at least parts of async. And there&rsquo;s lots of new issues you need to deal with, like eventual consistency or phrase conditions or debugging, for example. Local development can be a bit more complex. You need new infrastructure, the message broker, which also is something that you need to be familiar with and you need to maintain.</p> <p><strong>Robert</strong> [28:25]: Yeah, also testing is a bit more challenging because it&rsquo;s no longer working like you&rsquo;re doing HTTP requests and after request is done, you know that operation is done. No longer. You need to do some way of polling. So if you work with frontend a bit, it will be familiar for you because like in frontend, things are not that synchronous and sometimes you need to just poll for some operation that you expect that will happen and it will, eventually happen, but maybe it will not.</p> <p><strong>Miłosz</strong> [28:59]: Yes, but speaking of front-end, the UI also needs to be adjusted sometimes.</p> <p><strong>Robert</strong> [29:03]: Yeah, I think even Gabriel mentioned that it can be more difficult to handle errors in UI in async system. So you need to either use WebSocket or ServerSendEvents to push async errors, like in case of failed sync operation. Yeah, we totally agree that this is the challenging part.</p> <p><strong>Miłosz</strong> [29:25]: Exactly, yeah. This is it. So with synchronous approach, you can just return the error of the stack until the UI and then just show a big 500 error and something like try again or contact network support. With async approaches, the error is there somewhere in the system and the user doesn&rsquo;t know if something happened or not, if it was successful or not. Let&rsquo;s say this extra setup to show the status as recurrence.</p> <p><strong>Robert</strong> [30:04]: In other words, trade-offs. It really depends on basically what the system is doing under the hood. Because again, if you&rsquo;re storing stuff to the database and it&rsquo;s your database and it&rsquo;s stable, So it probably wouldn&rsquo;t be a problem. But if you&rsquo;re doing some stuff in the background, it&rsquo;s starting to be tricky. And it&rsquo;s also easy to create a poor UX design that will be just misleading for</p> <p><strong>Robert</strong> [30:34]: people that are using that.</p> <p><strong>Miłosz</strong> [30:36]: Which is also counterintuitive because it seems like something specific to backend. And here you need to consider it in the entire flow, even in the UI.</p> <p><strong>Robert</strong> [30:47]: Yeah, and again, the thing that Gabriel mentioned, we are also big advocates of servers and events. If you are interested how to implement it properly, go. So we have a pretty nice article about that, that Miros wrote on our blog with super nice example. So we&rsquo;ll link it in the podcast materials if you are interested.</p> <p><strong>Miłosz</strong> [31:13]: Any other downsides? what about observability?</p> <p><strong>Robert</strong> [31:18]: Yeah, it&rsquo;s also a bit tough topic because when you are building your REST API, you can basically monitor your HTTP load balancer.</p> <p><strong>Miłosz</strong> [31:31]: You can open the console in the browser and most of the time you will see some error when it happened.</p> <p><strong>Robert</strong> [31:39]: Yeah, or just check your load balancer and you have 500s on your load balancer. You&rsquo;ll see that something is wrong, we need to do about that. With an asynchronous approach, it&rsquo;s not that easy because probably you should monitor how long is the queue, how long, what is the age of oldest message. It&rsquo;s also not simple, because it depends on queue or topic or subscription. Sometimes having event dot is there or queue for five minutes is fine. But sometimes, if it&rsquo;s delay 30 seconds, it&rsquo;s maybe a problem. Sometimes having 10 messages spinning is fine, sometimes it may be a critical issue. So I&rsquo;m not saying that it&rsquo;s not possible to implement, but it requires more thinking and it requires more of what you said at the beginning, that you need to spend a lot of time on designing and understanding. Okay, maybe not a lot of time, but basically you need to have it in the back of your head.</p> <p><strong>Miłosz</strong> [32:49]: Have some tooling in place, like tracing at least, to see what&rsquo;s going on, to understand where the issue is.</p> <p><strong>Robert</strong> [32:57]: Yeah, and some metrics that I know that not every team has, but in this kind of architecture, you should have that. And I know that also not all message brokers are exposing the metrics about, for example, oldest message or how many of the messages are spinning. So sometimes, unfortunately, you need to write it by hand and export it by hand. So, obviously, it&rsquo;s some cost. For the other side, it&rsquo;s also not an unsolved problem. I mean, it&rsquo;s a solved problem if you find a well-known solution for that. It&rsquo;s not rocket science at the end of the day. But it&rsquo;s just important to keep it in mind, and keep in mind that every operation is also different.</p> <p><strong>Robert</strong> [33:45]: And there&rsquo;s also one challenge that I think not a lot of people are mentioning, because it may be a bit counter-intuitive, but it&rsquo;s a problem of decoupling. And why it&rsquo;s interesting? Because usually we&rsquo;re talking about decoupling in context of good things, but also decoupling may be challenging when it&rsquo;s, let&rsquo;s say, extreme decoupling. Like with events. So let&rsquo;s imagine that you&rsquo;re emitting some event, like order was placed, And you may have no idea, actually, who is listening to this event. So when you have your API, you basically see that somebody is calling this API. You probably can have some tracing and by that correlate from where it comes. But if somebody is listening to your event and the service doesn&rsquo;t have good observability, you have no way to understand who is doing that. In worst case, even you have no idea if anybody is doing that. And it&rsquo;s starting to be problematic when you need to change something. We&rsquo;ll get into that, but it may be challenging.</p> <p><strong>Robert</strong> [34:46]: Especially if you have a bigger system with more teams, it may take months to deprecate some events.</p> <p><strong>Miłosz</strong> [34:53]: Yeah, and you want to get rid of some code that produces some old events, but you don&rsquo;t know if a team is using it. But it can be in larger organizations that can be challenging.</p> <p><strong>Robert</strong> [35:05]: So yeah, in other words, decoupling is not always good. But I think it&rsquo;s also similar like with Don&rsquo;t Repeat Yourself. Often, don&rsquo;t repeat yourself may be a good idea, but not always. And it&rsquo;s a similar situation, because don&rsquo;t repeat yourself. It&rsquo;s trying to kill the coupling, but also, if you go into extreme, it may be problematic. It&rsquo;s, I think, interesting how you can apply some patterns and anti-patterns from level of code to level of architecture in some way.</p> <p><strong>Miłosz</strong> [35:41]: Yeah, because it may promote teams that are very focused on what they do. They just publish some events and then they don&rsquo;t care about whoever is doing with those events. Yeah, it&rsquo;s harder to see than with API calls. I think that there are some tools for this. I&rsquo;m not sure if we used any of them, but probably also depends on the on the approach you take, because there&rsquo;s no one standard for how to define events. I mean, there are some, but probably depends on the company and message broker and technology use.</p> <p><strong>Robert</strong> [36:24]: Yeah, but I think you reminded me about this case and it&rsquo;s pretty interesting that I remember in that in one place we had an event that we&rsquo;re integrating, we have integration between two teams, but indeed, we have some data quality issues there. And what we ended at the end was getting rid of this event and instead having a synchronous endpoint that the other team needed to call. And we were able to basically do validation.</p> <p><strong>Miłosz</strong> [36:52]: Oh, that&rsquo;s interesting.</p> <p><strong>Robert</strong> [36:53]: Yeah. And in this case, I would say that this extreme decoupling was bad because, again, there was problem of responsibility because somebody could emit an event that was totally not valid, and it was hard to enforce, because if this event was emitted, we&rsquo;ve been notifying it after a while in our systems that we are starting to receive some events. It was like, oh, we don&rsquo;t care.</p> <p><strong>Miłosz</strong> [37:23]: We did our job, we published the event. Yeah, that&rsquo;s an interesting thing. It&rsquo;s like on the boundaries of a team, when you integrate two teams, two services that belong to different teams. And the API becomes super important. And yeah, maybe sometimes synchronous is better in this case. Yep, that&rsquo;s a good take.</p> <p><strong>Miłosz</strong> [37:50]: Let&rsquo;s move to common anti-patterns.</p> <p><strong>Robert</strong> [37:54]: So you say that it&rsquo;s not perfect and it has some pitfalls and something can go wrong with asynchronous architectures.</p> <p><strong>Miłosz</strong> [38:03]: Looks like there&rsquo;s no super bullet. So I would start with two naive takes on background processing. So I&rsquo;ve heard of systems, or seen some, where people use something like a cron job to process the background tasks. It&rsquo;s similar to task queue, but more like in-house. And again, similar to task queue, it can probably work well for some cases, except you reinvent the wheel in this case so it&rsquo;s a bit worse I would say and the main issue is if you have any more complex scenario then simple please do this later then it breaks it won&rsquo;t help you much you don&rsquo;t get all those guarantees of pub subs and so on and</p> <p><strong>Robert</strong> [39:05]: It&rsquo;s also often processed at some magic hour so it&rsquo;s creating delay basically, if you are doing bad processing with your current job.</p> <p><strong>Miłosz</strong> [39:15]: Yeah.</p> <p><strong>Robert</strong> [39:17]: But I would even say that it&rsquo;s still better than database triggers because, I mean, it&rsquo;s choosing between two evils, but at least I would say that it&rsquo;s probably a bit easier to obtain.</p> <p><strong>Miłosz</strong> [39:32]: Yeah, logic in the database is a controversial topic. I wouldn&rsquo;t want to work with a system that does it. And of course there is probably this one specific case where it makes sense. I&rsquo;m sure there is, but in general it&rsquo;s tough to update and manage this.</p> <p><strong>Robert</strong> [39:52]: And test and debug.</p> <p><strong>Miłosz</strong> [39:55]: Yeah. If I had to use a cron job like this, I would just do it in the code. And for example, in Go it&rsquo;s trivial to spawn a new Go routine. So that&rsquo;s a perfect scenario. Have anything in the loop, pull the sleep and do something every, I don&rsquo;t know, minute or something.</p> <p><strong>Robert</strong> [40:14]: It can be some good starting points that you can migrate a bit later to a more proper solution.</p> <p><strong>Miłosz</strong> [40:21]: My main issue with this is that you reinvent something that has been solved. And many people put years of work into figuring out edge cases and everything. So maybe it&rsquo;s better to do something that exists.</p> <p><strong>Robert</strong> [40:38]: Especially that, for example, with Watermill in Go, you can pretty easily use Postgres or MySQL as PubSub. So basically, you don&rsquo;t need to add an extra architecture there. And you can start with something simple, and without many changes, change it to some real PubSub later. So I would say, I know that it&rsquo;s not available in all languages, but in general, it may be also some nice starting point. and it&rsquo;s just less to migrate later when it will start to be problematic.</p> <p><strong>Miłosz</strong> [41:10]: There&rsquo;s one worse kind of cron I&rsquo;ve heard about, is that you write a cron to check for inconsistencies in the system.</p> <p><strong>Robert</strong> [41:22]: Oh, no.</p> <p><strong>Miłosz</strong> [41:24]: So let&rsquo;s take this example of calling this marketing API after a user signs up. So you realize this can sometimes fail and you write a cron job that does a check between the database and the marketing API and see if anything is missing and</p> <p><strong>Robert</strong> [41:46]: Basically you need to implement everything twice.</p> <p><strong>Miłosz</strong> [41:49]: Yeah and my my worst worry here is that you kind of promote ignoring issues And whatever happens, whatever issue like this you have in the future, a challenge like that, you will just write another consistency check. And this is another code you have to maintain. And basically you just sweep the issue under the rug instead of fixing the root cause. So yeah, I think that&rsquo;s too naive to take to work in a large-scale system.</p> <p><strong>Robert</strong> [42:30]: I can remind one case when it makes sense, but I would say it&rsquo;s rather an exception. When you are working with some, let&rsquo;s say, critical systems, you are quite sure that everything is fine, but you would like to do some double-check.</p> <p><strong>Miłosz</strong> [42:51]: Checks in general, consistency checks in general, I wouldn&rsquo;t say that are a bad thing, but if you are doing them because you can design a better API, that&rsquo;s the issue. You anticipate them to happen. I would create a consistency check in places where I&rsquo;m sure they won&rsquo;t happen, but I want to be extra careful anyway because it&rsquo;s so critical.</p> <p><strong>Robert</strong> [43:19]: I think the big difference is if you are implementing some compensating operation in this checking. For example, if you are working in some financial system, it&rsquo;s typical to do some reconciliation reports in some intervals, but it&rsquo;s making reports. It&rsquo;s not doing the compensating operations. I mean, your logic should be valid and you should just do validation at the end to ensure. But again, It&rsquo;s just for critical systems that you need to be sure. For example, if your company or whatever, $100 came in, $100 should come at the end. But again, it&rsquo;s not about doing compensation. It&rsquo;s all about double-checking the logic that you have.</p> <p><strong>Miłosz</strong> [44:06]: Right. Especially if you have a few of those in the most critical areas, that should be fine. But if in every API call you need to do this check and compressing logic then it won&rsquo;t be maintainable after a while you will go crazy just trying to cover all these cases and then any API change or whatever will also break this so you have more code to maintain again I would fix the root cause if you know it can happen.</p> <p><strong>Robert</strong> [44:42]: Agreed Thank you.</p> <p><strong>Miłosz</strong> [44:44]: And one more regarding this naive approach is, you know, when you use messaging and use kind of events, like you said, those passive-aggressive events, or just something that&rsquo;s more like technical, not in the domain or business sense.</p> <p><strong>Miłosz</strong> [45:03]: It can seem like smart design, but not always the case. It doesn&rsquo;t automatically give you good results. I worked once with a system where there were two applications, like an old one and a new one, and they shared some entities. And there was a system that, for every change in the entity, published an event that it changed, the other system consumed that and updated the entity, and the other way around as well. So, you know, if a naive setup like this, you ignore many edge cases that can happen. And there are some variants on the entities, they have some rules, but if you just do a naive update like this, even if it&rsquo;s asynchronous, it doesn&rsquo;t give you anything and can be even worse because there can be some inconsistencies and you won&rsquo;t even know. Because maybe some message arrives out of order, or is overwritten by the other one.</p> <p><strong>Miłosz</strong> [46:16]: So this comes back again to the event design topic, right? This needs to be a proper design. You need to spend some time and understand what you are doing.</p> <p><strong>Robert</strong> [46:30]: And I think it&rsquo;s probably time to give a big shout to event storming. That was usually pretty useful for designing some event-driven systems. But not only, because it may be also suggesting that event storming is for designing event-driven architectures or async architectures, but it&rsquo;s not. I mean, event storming is good for basically designing any kind of system, but it&rsquo;s pretty useful that you have notation that can be mapped to your software really directly, so I recommend it to check event storming. If you&rsquo;re looking for some materials how to do that, there is an e-book that is probably unfinished for the last 5 or 10 years. But no worries.</p> <p><strong>Miłosz</strong> [47:14]: It&rsquo;s great anyway.</p> <p><strong>Robert</strong> [47:15]: I think 5 or 10 years earlier it was already very good. So Alberto, really good job.</p> <p><strong>Robert</strong> [47:29]: Another downside or trap related to asynchronous architectures is creating distributed monolids. Because, like many useful techniques, they are helping with many things, but often they are also opening gate of hell.</p> <p><strong>Miłosz</strong> [47:44]: It&rsquo;s the new normal. Instead of monoliths, we have distributed monoliths.</p> <p><strong>Robert</strong> [47:51]: Let&rsquo;s wait for distributed monorepos.</p> <p><strong>Miłosz</strong> [47:54]: We&rsquo;ve seen it too many times.</p> <p><strong>Robert</strong> [47:57]: Yeah, but the usual scenario is like, okay, microservices are great, we need to be scalable, etc. And the team is starting to build microservices, and you start to have more microservices than people, and at some point you&rsquo;ll notice that communication between them is hard because if one microservice in the middle will not work for a while because our Kubernetes cluster is not that stable yet, all our requests in the platform are failing because of cascading failures, we need to do something about that. And unfortunately, often a reversing decision of going into microservices is not on the plate, because come on.</p> <p><strong>Miłosz</strong> [48:41]: Too late for that.</p> <p><strong>Robert</strong> [48:42]: Yeah, we invested that much in many stuff that we don&rsquo;t need. So, often the solution is, well, let&rsquo;s use event-driven architecture. And, well, it&rsquo;s making all the problems that the team had earlier, it&rsquo;s making it deeper and deeper, because debugging everything is harder, understanding everything is harder, everything works slower. Or, okay, the one upside may be that, okay, at least it&rsquo;s kind of more stable, with caveat that usually there is the thing that Gabriel mentioned about UX, that, well, if something is processing in the background, it&rsquo;s a bit harder to show it in the UI. So compared to showing error directly to user like earlier, now something is happening and user have no idea what is happening. And yeah, it&rsquo;s a pretty problematic thing that we&rsquo;ve seen many times. So it&rsquo;s important that asynchronous architecture can solve, in this case, some problems. But again, don&rsquo;t be afraid to do the thing that we like to recommend, and we&rsquo;ve did multiple times and it&rsquo;s the microservicization. So basically getting your services and making less services. Again, having more microservices than people in a team, it&rsquo;s terrible.</p> <p><strong>Miłosz</strong> [50:04]: But the interesting part about this pattern or anti-pattern is that it comes from good intentions.</p> <p><strong>Miłosz</strong> [50:12]: It starts with this idea that we have a small team now but we will grow and we need to to be able to separate some code easily. And then you have those separate services and you figure out, okay, let&rsquo;s now just connect them like we did with function calls before. And then it breaks, but it comes with this good intention of separating the constants.</p> <p><strong>Robert</strong> [50:44]: Oh, I think it&rsquo;s problematic with good intentions, because I think all the bad systems that we worked with was built on the good intentions. Because, you know, it&rsquo;s rarely the case that somebody had a bad intention. Like, let&rsquo;s build the worst systems that we can. Yay, hooray, it will be so great idea. Usually, the intention is good. But I also understand that sometimes it&rsquo;s also hard to reverse this decision. But from the other side, it&rsquo;s also sometimes leading to systems that are totally unmaintainable. And, well, even I can say that rewriting is the only way to redo something.</p> <p><strong>Miłosz</strong> [51:27]: It&rsquo;s just counterintuitive, because you think that splitting things makes things decoupled. But it&rsquo;s not really the case. It depends on how you communicate, really.</p> <p><strong>Robert</strong> [51:38]: Yeah, so basically it&rsquo;s all about splitting things that are coherent. So if you are splitting things that are coherent, you are just ending up with a lot of accidental complexity. And it&rsquo;s pretty bad. I think it&rsquo;s also partially the reason from where some people see asynchronous or even given architecture as anti-pattern and over-engineering. Because if you&rsquo;ve seen many or a couple projects using some patterns and you&rsquo;ve always seen that is overcomplicated. Well, it&rsquo;s logical that next time when somebody will suggest you let&rsquo;s do it asynchronously or let&rsquo;s use evangelical architecture will be pessimistic because you&rsquo;ve seen that, okay, it was always an over-engineered problem. But again, it&rsquo;s probably, it wasn&rsquo;t a problem of evangelical architecture. It was probably a problem of applying it in the wrong project.</p> <p><strong>Robert</strong> [52:30]: It&rsquo;s important to keep it in mind. So if you&rsquo;ve seen some technique or tool in many places and it was always overcomplicated, it&rsquo;s maybe not a problem of this tool. Maybe it&rsquo;s a problem of a person using this tool, or this person maybe didn&rsquo;t understand this properly. It&rsquo;s a thing that we&rsquo;ve seen in many places, and sometimes it&rsquo;s requiring a lot of effort to convince people that no, no, it&rsquo;s not because of this tool, it&rsquo;s because of applying it in the hard place. But again, if you are not convincing somebody, it&rsquo;s not our issue at the end.</p> <p><strong>Robert</strong> [53:09]: I mean&hellip;</p> <p><strong>Miłosz</strong> [53:12]: There&rsquo;s also not really a strict definition of all those techniques. And depending on the language you use and other technology, it will be a bit different. That&rsquo;s why it&rsquo;s hard to judge. Wrapping up distributed monoliths, the coupling is the issue here and&hellip; Synchronous is not really decoupled by default, because events are also a form of coupling, in what we mentioned before, right, so&hellip; On the one hand, you have this, you public an event and you don&rsquo;t care what happens later, On the other hand, it&rsquo;s also a kind of contract between you and the consumers. Let&rsquo;s say you have this distributed monolith and you now replace the RPC calls with messages. Is it decoupled? No, not really. It&rsquo;s the same issue, just with different transport. So it&rsquo;s more about the design of communication, not really the async or sync here.</p> <p><strong>Robert</strong> [54:27]: Because, yeah, basically you can implement RPC over queue and it will be basically the same. I mean, it doesn&rsquo;t change much if you are coding something directly over HTTP, if you are expecting to have a response from your operation.</p> <p><strong>Miłosz</strong> [54:44]: Yeah, I think Gabriel mentioned this before, that there is one interesting pattern is used by NATS. The call seems to be synchronous, but it&rsquo;s asynchronous under the hood. I&rsquo;m not sure about this exact one in NATS.</p> <p><strong>Robert</strong> [55:01]: But yeah, I guess that it may be about something that Celery supports. So basically, it&rsquo;s basically doing RPC over queue.</p> <p><strong>Miłosz</strong> [55:11]: Yeah, this is a weird hybrid approach when you can do synchronous over message broker. It kind of works.</p> <p><strong>Robert</strong> [55:20]: It has some use cases, obviously, but it&rsquo;s not really asynchronous. We also have each in Watermill, so we have the Request-Reply component. But again, it&rsquo;s not really fully asynchronous. So it&rsquo;s nice because you can have some response to know if something was successful, and if you are no longer listening, it will be processed anyway. Not like with Request, because if your Request data is originating, let&rsquo;s say originating request, and it&rsquo;s calling everything later, and this request will be cancelled. So if you are handling context cancellation properly, everything else should be killed, basically. So it should be kind of rolled back in all requests. But if you are doing RPC over cube, it will be done everywhere anyway. There are some cases when it makes sense, but&hellip;</p> <p><strong>Miłosz</strong> [56:07]: Maybe one promise here is that you use one platform for all communication. So it sounds good on paper. We don&rsquo;t need RPC because you can do everything over messages. Maybe it&rsquo;s a good idea. I don&rsquo;t know.</p> <p><strong>Robert</strong> [56:22]: But it&rsquo;s tricky. If you have a request and you would like to, for example, insert something in the database, your request was cancelled. And this cancellation is not propagating. because the translation is not propagated to the queue, so it can lead to some strange inconsistencies. Because you killed the request, so expect that it should be not written, but it&rsquo;s somewhere later in going through the queue, so it will be done anyway. And it can lead to some strange state where you don&rsquo;t know what really happened.</p> <p><strong>Miłosz</strong> [56:54]: Yeah, I would summarize it as messages behave differently from requests most of the time. So, if you use one as the other, it can work, but you can have some weird edge cases down the line, because it&rsquo;s just a different method of communication. So I will be careful with this. Okay, and any more anti-patterns?</p> <p><strong>Robert</strong> [57:24]: So I have two connected anti-patterns here. So the first one is using no message ordering. So it&rsquo;s maybe not anti-pattern, but it&rsquo;s something to watch out. Because in many systems, you need to have some way of ordering your messages. So let&rsquo;s imagine that you have event for subscriber subscribed and subscriber unsubscribed. And the problem is, if you not order them properly and build some read model based on them and they will be received out of the order, you may unsubscribe a person that was not subscribed yet, if the unsubscribe event will arrive first, because it can if it&rsquo;s not ordered, and later you will have a subscribed event that should be&hellip;</p> <p><strong>Miłosz</strong> [58:13]: Yeah, that&rsquo;s one of the things you learn when you first start using messages. Those wish behaviors that seem simple at first.</p> <p><strong>Robert</strong> [58:24]: Yeah, and the simple solution may be, let&rsquo;s just order every message and just have everything ordered. And yeah, it sounds like a pretty good idea, but sometimes it may work. But it has also one big downside, at least. I would say two big downsides. So the first one is performance. So, basically, performance of processing those messages will be limited to how fast you are often to process them. And basically, they will be processed one by one. And if operations will be slower, it will just start to pile and you will be not able to process them.</p> <p><strong>Miłosz</strong> [59:01]: Or block all processing of all messages.</p> <p><strong>Robert</strong> [59:05]: And the second thing that I believe is worse is that if any of messages will spin, it will block all other messages. And it&rsquo;s pretty bad because it&rsquo;s killing all the advantages of asynchronous architecture in terms of stability.</p> <p><strong>Miłosz</strong> [59:26]: Yeah, on one hand it can help you not care about ordering, which seems like a nice idea. Yeah, but if you make it too wide, then it can be also an issue. There&rsquo;s a lot of things to consider here, so it&rsquo;s hard to give someone a solution, right? So much depends on the domain and the system design. Sometimes you can get away with no ordering, but you can use something like a version. I remember one system where we didn&rsquo;t have ordering at all, because all events updated different parts of a read module.</p> <p><strong>Robert</strong> [1:00:12]: And I think it was also the problem where we&rsquo;re using Google Cloud PubSub and it didn&rsquo;t support ordering at this point also.</p> <p><strong>Miłosz</strong> [1:00:20]: Yeah, we can kind of get away with it, but I remember we actually used times for solving some conflicts, so it&rsquo;s also not ideal. Depending on timestamps is controversial.</p> <p><strong>Robert</strong> [1:00:33]: It&rsquo;s hard to synchronize. I think in some places where it was possible, we&rsquo;ve been using versions. So basically every event was that within aggregate, let&rsquo;s say that, within the, let&rsquo;s say, transactional boundary, so where we need to keep ordering, had an incremental version. So if we received an event that was out of order, we&rsquo;ve been rejecting that or in some cases we&rsquo;ve seen that, okay, we received newer price, for example. Sorry, we received older price and we already stored the newer, so we just can ignore older. But the downside is that you need to support it in custom way for everything.</p> <p><strong>Miłosz</strong> [1:01:17]: Not super complex, but something to consider.</p> <p><strong>Robert</strong> [1:01:20]: But nice to have it out of the box. For example, Google and PubSub have ordering keys. Kafka has partitioning. So there are method workers that just have it out of the box. And you don&rsquo;t need to think about that later, basically.</p> <p><strong>Miłosz</strong> [1:01:33]: Remember, you can talk a bit more about this in the next episode.</p> <p><strong>Robert</strong> [1:01:39]: Yeah, I think ordering is probably something that can be just an entire episode about that. because this is pretty&hellip; Maybe not complex, but it has many different use cases that it&rsquo;s good to know about. Because in many cases, you should approach it differently. Often, you don&rsquo;t need to care that much. You can choose something simple and it&rsquo;s fine. But it depends a lot on scalability requirements or and, let&rsquo;s say, resilience.</p> <p><strong>Miłosz</strong> [1:02:10]: The balance is difficult. It&rsquo;s easy to do. No ordering or ordering by every message something in the middle you have to consider manufacturers</p> <p><strong>Robert</strong> [1:02:20]: Yeah but tldr i would say it&rsquo;s try to do as narrow ordering as you can so for example try to do some subset of events for one customer maybe or you need to basically analyze where this ordering matters and based on that, do really narrow ordering and it will work, basically.</p> <p><strong>Miłosz</strong> [1:02:48]: There&rsquo;s one thing that&rsquo;s similar between async and sync. I think it&rsquo;s API design. So with Synchron&rsquo;s approach, you have some kind of contract in how your API works. If it&rsquo;s a REST API, you can have some open API document with gRPC, probably protobuf schema.</p> <p><strong>Robert</strong> [1:03:14]: Yes, I see that Gabriel asked about that. What we are usually using is protobuf, because you can marshal it later to at least two formats. Usually we are using JSON as a transport format, because it&rsquo;s easier to debug. But if you care about performance and the size of message, you can use protobuf, so it will be a bit smaller and faster. And you can also convert it to multiple programming languages, and also it&rsquo;s nicely supported the versioning of fields.</p> <p><strong>Miłosz</strong> [1:03:51]: There&rsquo;s the cloud events schema, I think. We didn&rsquo;t use that yet. Protobov usually does the job. The point is you need to follow similar ideas for both sync and isync here. So your event schema or your API schema is your contract. So you have to make sure there are no breaking changes. And yeah, the API is stable. And probably do some versioning if you need to do breaking changes. We sometimes have events with version delay, like v1, just to anticipate it. Not always needed, but sometimes useful.</p> <p><strong>Robert</strong> [1:04:44]: Yeah, and it&rsquo;s also worth mentioning that many message brokers are supporting some kind of schema registry out of the box. So those message brokers can validate the schema for you. So, for example, Kafka has it. I think Google and PubSub have it. I think it&rsquo;s also worth mentioning that some companies are using Avro for that. I think we didn&rsquo;t use that, but I know that it&rsquo;s often used in some companies, but I think I guess that it&rsquo;s more in Java world. I think in Go world, more gRPC is used, at least from our&hellip; Protobuf. Protobuf, yeah. Protobuf is more used in our bubble.</p> <p><strong>Miłosz</strong> [1:05:29]: It&rsquo;s pretty nice. And as a plus, generated code is It&rsquo;s a quite popular choice and it works pretty well with Protobuf as well.</p> <p><strong>Miłosz</strong> [1:05:49]: Shall we move to the final question? What to choose?</p> <p><strong>Robert</strong> [1:05:54]: Unfortunately, it&rsquo;s as you heard. There are really many factors to consider. Yes.</p> <p><strong>Miłosz</strong> [1:06:05]: And the balance is the hardest part, as always. There is no silver bullet, so you have to figure out what balance means for you. It&rsquo;s a bit like the CAP theorem in databases, right? So you have consistency versus availability, and you have to pick one.</p> <p><strong>Robert</strong> [1:06:26]: Yeah, so in async system you basically are sacrificing consistency, but you have better availability.</p> <p><strong>Miłosz</strong> [1:06:36]: You have eventual consistency, but that&rsquo;s it.</p> <p><strong>Robert</strong> [1:06:40]: But it&rsquo;s eventual. Cool.</p> <p><strong>Miłosz</strong> [1:06:43]: So what should we choose, do you think?</p> <p><strong>Robert</strong> [1:06:48]: I think to give you some simple algorithm, maybe. If you&rsquo;re in-depth, you can always start with synchronous. I would say that in many cases, it will be a good starting point. In many cases, using async, by default, maybe, Maybe not over-engineering, but maybe not needed at the beginning. And if you have infrastructure already in place, switching something from async to sync, may be not that big difference, as long as you didn&rsquo;t do any strange thing. And it&rsquo;s especially simple if you are using clean architecture, because you should have some application layer entry point to your application. So basically calling you from RPC or event handler or something, it can be a pretty simple change. Of course, it depends a lot on the use case, but again, it&rsquo;s if you&rsquo;re in that. In some cases, you may be just sure, like you&rsquo;re calling some external system and you know that it&rsquo;s not stable, do it synchronously. You have some critical flow that you know that it needs to be working, you need to send some external data, do it asynchronously. Again i&rsquo;m saying more about cases that you are not sure.</p> <p><strong>Miłosz</strong> [1:08:10]: Sometimes it&rsquo;s a product decision as well if something can happen in the background and sometimes it&rsquo;s tricky because in the background can mean a second like later or you know 10 seconds later so it so it&rsquo;s also it takes some skill to to be able to talk to you know whoever your stakeholder is whoever is responsible for the product to understand if it&rsquo;s fine to do it in an asynchronous way. Which means most often it will show up in the UI right after, but sometimes it will not after a few minutes maybe. That&rsquo;s also counter-intuitive. Why would you talk with your product owner about sync versus async architecture, sometimes it makes sense.</p> <p><strong>Miłosz</strong> [1:09:07]: Probably it&rsquo;s good to watch out for the extremes, as always, right? So, not everything can be handled by synchronous architecture, as you mentioned. There are some hard limits, where more complex systems just won&rsquo;t be able to do it. But as you said, sometimes doing everything over messages can be also over-engineering. So if you ever consider doing everything one way, probably you need some balance. It&rsquo;s usually a clear sign.</p> <p><strong>Robert</strong> [1:09:45]: And if you maybe didn&rsquo;t work in any system that required building asynchronous architecture, it&rsquo;s fine. But anyway, I would recommend to check out how you can do that, because it&rsquo;s just know what tools you have available. So if you have a problem that will require asynchronous architecture approach, you&rsquo;ll just know, okay, I have tool for that, it fits here, and I can use that. So you will not need to learn it when you have this problem, because it might be just too late.</p> <p><strong>Miłosz</strong> [1:10:17]: Sometimes a hybrid approach may be fine. One is what we mentioned before, the RPC of our messages. A bit unusual, maybe if it fits some use cases. Something else that comes to my mind is CQRS here. So, TLDR is a pattern where you split your controller functions, let&rsquo;s say, into commands that change the system and queries that fetch the data. Super short definition. Not to dive too deep into it, but the interesting thing about it is that it gives you this extra structure in your code. Commands and queries are separate. And then it can make mixing them and how you call them a bit more intuitive. Because queries are usually synchronous. So someone is waiting for the data, that makes sense. and commands can be either. So it can give you this mental framework to decide how to mix sync and async in one project.</p> <p><strong>Robert</strong> [1:11:33]: I think it&rsquo;s interesting, the CQRS example, that it&rsquo;s a good example of technique that people really misunderstood because for many people if they hear CQRS it&rsquo;s immediately asynchronous but no. So it&rsquo;s totally misunderstanding of this pattern. Again, we&rsquo;ll add to materials our article about SQLRS, because we&rsquo;ve been covering that. Milos also did a really nice presentation about that, so I think we can also put it to the materials. It&rsquo;s important to mention it all in our SQLRS. It&rsquo;s not about doing things asynchronously. It&rsquo;s some implementation. it&rsquo;s worked nicely, but you can do skewers with just doing everything synchronously. And it&rsquo;s nice because if you are using skewers in a proper array, so when commands are not really returning stuff, you can convert it to asynchronous architecture really easily, because it should not really depend that much on having the feedback from the command.</p> <p><strong>Miłosz</strong> [1:12:40]: It helps if you don&rsquo;t have these mixed functions that change the state and also return data. It can be more tricky to decide what to do with them.</p> <p><strong>Robert</strong> [1:12:49]: And it also doesn&rsquo;t require some super big overhead because this is also something that we heard from many people, like, oh, security requires so much effort. But no, it&rsquo;s just a matter of, instead of having some super big application services with everything, just have multiple little services with commands instead of arguments and commands that doesn&rsquo;t return stuff. And that&rsquo;s it. And you have the basic secure implementation in your product. You can put it into your CV.</p> <p><strong>Miłosz</strong> [1:13:24]: Um&hellip;</p> <p><strong>Robert</strong> [1:13:27]: So yeah, I already mentioned also the migration. So SecureOS helps with that. But you can also get some hybrid approach, because often at the beginning of the project some endpoint can be synchronous, but with time, more and more stuff is added to that.</p> <p><strong>Robert</strong> [1:13:45]: We have actually a good example in our trainings platform. So we have webhooks from our payment provider about information that somebody bought our training. And at the beginning it was pretty simple, just adding, ah, somebody has training. But now it has a lot of stuff and we need to actually convert it to asynchronous to using events. But now it&rsquo;s probably doing five things. And sometimes some of those things are failing because it&rsquo;s just doing a lot of that. But, again, at the beginning, it didn&rsquo;t make sense to do it asynchronously because it was pretty lightweight, but with time, you can convert that. And you can even start with still having a synchronous endpoint and with emitting some internal event or command and just cutting it part by part. If it&rsquo;s some internal logic, you can, at the end, maybe just remove this endpoint and say the team that is calling this endpoint, you can now just emit an event or the opposite. it. So, for example, if you have a problem with data quality and other team is not taking responsibility of that, introduce endpoint instead of event and say, if you will not respond that this is valid, sorry, you need to provide this valid event and just agree what data should be in this endpoint to make everybody happy.</p> <p><strong>Miłosz</strong> [1:15:04]: And speaking of code structure, maybe one more thing that can help is layered architecture or cleanarchitecture or hexagonal or whatever you want to call it. If you place the logic in some isolated logic, place in your code, and then have two entry points that are either messages or RPC or HTTP. It&rsquo;s super easy to switch between how you call the handler, because you have just two entry points that do nothing else, just just map the request to your internal structure. And it makes it super easy to migrate as well.</p> <p><strong>Robert</strong> [1:15:53]: And it&rsquo;s happening, I would say, because it&rsquo;s often argument in clean architecture, like, okay, in clean architecture you&rsquo;re separating database, you can switch it, or whatever. It doesn&rsquo;t happen often. But changing things from synchronous and asynchronous actually can happen quite often.</p> <p><strong>Miłosz</strong> [1:16:11]: Or just both at the same time. You can have some logic that can be executed synchronously or asynchronously by message. And if you use the layered approach, it&rsquo;s super easy to do.</p> <p><strong>Robert</strong> [1:16:22]: Yeah, and it happens pretty often because database, again, you are not changing it very often, but changing things from synchronous to asynchronous or vice versa, the first I think it&rsquo;s happening more often, but it&rsquo;s happening often. Because, again, just some operations are starting to be bigger and bigger. You start to have more and more teams. Your product is growing. This is, hopefully, what&rsquo;s happening. And in this case, it&rsquo;s just making your job easier and saving all of your time. So it&rsquo;s, I think, a great investment that is not mentioned that often.</p> <p><strong>Miłosz</strong> [1:16:56]: Yeah. Okay, maybe as a finishing point, we can talk about reducing this accidental complexity that comes with starting with messaging. First, once you start, but there&rsquo;s a lot of topics to grasp. We talked about some today, but it&rsquo;s not really an episode about event-driven architecture, only the comparison of Async and Sync. So one idea might be to start with some PubSub you already have, which is probably SQL database. I probably have some Postgres or MySQL running around, and you can actually have a pretty simple PubSub running on it. That should be good enough for most use cases. Or you can use a cloud-based PubSub. It means you don&rsquo;t need to maintain the architecture or infrastructure, which can also be a complex task sometimes with high availability in place and all this.</p> <p><strong>Robert</strong> [1:18:10]: I think Postgres is a great starting point and it can handle much more than you think. I actually have some idea about one article that maybe we can show how far away we can push Postgres-based paths up, because I&rsquo;m pretty sure that we can push it pretty far away, but let&rsquo;s see.</p> <p><strong>Miłosz</strong> [1:18:29]: Yeah, unless you want some complex topology. For starting out, it&rsquo;s a great way to start. Another way, maybe using a high-level library that abstracts away some of the concepts for you. So for example, Robert mentioned earlier Watermill, which we maintain in Go. I&rsquo;m not sure about other languages. I think it&rsquo;s something there&rsquo;s something similar in Python. Probably in every other language there should be something like that. Because you probably don&rsquo;t want to deal with all the protocol, the infrastructure you use, all the low-level details, so you can focus on the high-level publishing and subscribing to messages.</p> <p><strong>Robert</strong> [1:19:21]: It&rsquo;s pretty easy to mess something with that. So lose message or make some sub-toolbox. We&rsquo;re creating WaterMeal already for a couple years, and you&rsquo;ll be surprised how many little quirks you can have in every PubSub. It&rsquo;s very hard to debug at the end, because you may just be losing one message for one million messages, and it&rsquo;s very problematic in systems. It&rsquo;s good if somebody already did it for you. If, for example, you don&rsquo;t use Go, We&rsquo;ll recommend to use Go, but if you can use Go, you can always even look on our SQL implementation and see and inspire by our queries.</p> <p><strong>Miłosz</strong> [1:20:06]: Just use Go.</p> <p><strong>Robert</strong> [1:20:07]: It&rsquo;s a best idea.</p> <p><strong>Miłosz</strong> [1:20:11]: It&rsquo;s not that complex in the end. But there are also some quirks, so it&rsquo;s better if you don&rsquo;t reinvent the wheel. Because it took us some time and I think four major versions to arrive at where the SQL implementation is right now. So I&rsquo;m going to manage cases.</p> <p><strong>Miłosz</strong> [1:20:33]: So, if you&rsquo;d like to learn more about async architecture and event-driven architecture in particular, we have an event-driven training that&rsquo;s sales open for the next two weeks, starting from today. So you can learn those concepts in a more hands-on way, solving exercises.</p> <p><strong>Robert</strong> [1:21:00]: If you are interested in learning, Doug, you are actually pretty lucky, because we&rsquo;re running this training by Sayer, so if you are listening to it later, it may be already not open, but again, it will be open next year.</p> <p><strong>Miłosz</strong> [1:21:17]: If you listen to it later, you can join the waiting list and wait for the next sale. We run it as a cohort, so everyone learns together. And yeah, you can just Google Go event-driven or we will put a link in the notes.</p> <p><strong>Robert</strong> [1:21:32]: We have pretty good SEO, so it should be on the top of the list when you will Google Go event-driven.</p> <p><strong>Miłosz</strong> [1:21:40]: Okay. Before we move to the Q&amp;A.</p> <p><strong>Robert</strong> [1:21:45]: Yeah, definitely. Okay, so Dima mentioned that naming always matters. Yeah, so definitely it matters more often than you will think, but&hellip;</p> <p><strong>Miłosz</strong> [1:22:07]: Okay, so Gabriel shared more about that events should be facts that already happened in the system, what we talked about. Provides many advantages as states can be recreated from events. Yeah, that&rsquo;s one of the things we didn&rsquo;t mention. It&rsquo;s super useful so you can have this event log somewhere in your system and then reply those events to do whatever you want.</p> <p><strong>Robert</strong> [1:22:31]: And in other words, you have event sourcing, but event sourcing is not asynchronous. I mean, event sourcing can be asynchronous, but it can be synchronous as well. So I would say that it&rsquo;s synchronous by default. And then it&rsquo;s probably a nice topic for other episodes. So we mentioned already financial systems in a couple places, and event sourcing is actually great for this kind of system. When you need to have audit by the law or you need to have audit because you would like to understand how you ended up with some state.</p> <p><strong>Miłosz</strong> [1:23:10]: Gabriel also mentioned this dependency matrix of events based on types could be helping an organization. I think I said something like, I wonder if there is a ready software for this. I think event catalog or something like that, but we didn&rsquo;t use anything that we could use across teams and I mean place where you can just go and see which team uses what because it is to integrate with your code I guess ideally if you don&rsquo;t want to keep it up to date over time which can be a manual effort so it sounds like a challenge to do it for any language you use and if we find something like that we&rsquo;ll let you know</p> <p><strong>Robert</strong> [1:24:15]: So Gabriel mentioned that there is a nice chapter about event storming and learning domain driven design from Orly. I didn&rsquo;t read this one, I&rsquo;m not sure if you had a chance.</p> <p><strong>Miłosz</strong> [1:24:25]: I have it on my shelf. Still waiting.</p> <p><strong>Robert</strong> [1:24:29]: TBD. Maybe this summer.</p> <p><strong>Miłosz</strong> [1:24:32]: But everything about event storming, I think you can recommend right away.</p> <p><strong>Robert</strong> [1:24:38]: Yep, definitely. I think event storming was one of the biggest game changers for working with some more complex projects and collaborating with some multiple stakeholders.</p> <p><strong>Miłosz</strong> [1:24:51]: Also not super complex to learn, but you have to be willing to talk to people. Can be controversial in tech teams, sadly. Ordering is probably not needed if you have a good state machine, which guarantees the state of aggregates. It sounds to me connected to event sourcing again.</p> <p><strong>Robert</strong> [1:25:17]: Or the thing that we have with versioning. That&rsquo;s true. You can implement that. But from other side, if you also do it properly from a message broker perspective, it can simplify also a lot. Because you can just listen to event and don&rsquo;t care about that. We&rsquo;ve been able to compare migrating to that and ordering, at least in our case really help to simplify it a bit. Because I think it&rsquo;s good because if it&rsquo;s done properly, it works out of the box. So kind of out of the box, let&rsquo;s say. And it&rsquo;s harder to make mistake when you need to remember about something. But you can also implement universal state machine that can handle that. But universal things that are implemented by you often may have some chances.</p> <p><strong>Miłosz</strong> [1:26:07]: But you can see how many approaches there are to ordering. It&rsquo;s a pretty long topic. You have to consider many approaches. So thanks for mentioning this one.</p> <p><strong>Robert</strong> [1:26:21]: So Gabriel will have Monopoly on our chat today.</p> <p><strong>Miłosz</strong> [1:26:25]: Thank you for contributing. You are the third author today.</p> <p><strong>Robert</strong> [1:26:31]: I think at some point we should try to also share the link to join with us. That would be cool. Risky but cool. Gabriel also recommended Reactive Manifesto, which is a nice definition of Reactive Systems. I remember reading that probably a long, long time ago.</p> <p><strong>Miłosz</strong> [1:26:52]: Yeah, same.</p> <p><strong>Robert</strong> [1:26:53]: Recommend checking, yeah?</p> <p><strong>Miłosz</strong> [1:26:55]: I feel like an ignorantat the moment, sorry.</p> <p><strong>Robert</strong> [1:27:00]: In Scala, there is a Lagon framework, which was nice, Akka is under the hood. Maybe. We are not Scala guys.</p> <p><strong>Miłosz</strong> [1:27:12]: Yeah, so it&rsquo;s good that there is similar software, so you don&rsquo;t need to understand everything, what&rsquo;s going on, which can also be tricky because sometimes you may lose some you may not understand something correctly, the configuration of some PubSub. But to start out, it&rsquo;s super important to have this A-level concepts in place. You just publish a message, subscribe to subscribe to a message and don&rsquo;t care about what the bytes do underneath.</p> <p><strong>Robert</strong> [1:27:41]: And also Gabriel mentioned that it&rsquo;s under the hood, so it would be actually interesting to see how actor model is going up. I mean, if it&rsquo;s visible or not. Personally, I&rsquo;m not a big fan of actor model. I mean, I don&rsquo;t like the mental model of actor model. So even driven fits in my head much better because you can get to even storming and it maps one-to-one in some way and actor model for me is a bit more like magic mental mapping that you need to do and, I don&rsquo;t know what&rsquo;s your opinion on that but.</p> <p><strong>Miłosz</strong> [1:28:13]: Yeah I didn&rsquo;t use it much except for playing a bit with elixir so yeah I can&rsquo;t say much maybe you can do another episode sometimes about comparison</p> <p><strong>Robert</strong> [1:28:29]: Fortunately Gabriel said that but I love Go much more.</p> <p><strong>Miłosz</strong> [1:28:34]: Way to go!</p> <p><strong>Robert</strong> [1:28:43]: Okay. Cool, so a couple more seconds for more questions.</p> <p><strong>Miłosz</strong> [1:28:54]: While we wait, it&rsquo;s the time you should hit the subscribe button and rate our show</p> <p><strong>Robert</strong> [1:29:02]: Let&rsquo;s wait Please click this button You didn&rsquo;t click yet I see your screen Okay, thank you So, also now 5 star review, Let&rsquo;s write something nice. We&rsquo;ll wait here. We have time.</p> <p><strong>Miłosz</strong> [1:29:23]: Yeah, and instead of the YouTube subscribe, there&rsquo;s an even better subscribe button on our website, where you can join our newsletter, so then you won&rsquo;t miss the updates from us.</p> <p><strong>Robert</strong> [1:29:34]: Plus, you will also receive notifications about the episode notes. There is an entire transcript. We&rsquo;re linking to all sources that we&rsquo;re mentioning here, like WaterMule, like ebook about event storming, and about a couple more things that we mentioned and I don&rsquo;t remember now, but it will be on all-in materials for this episode. Plus, also if you release any new articles on the blog, you&rsquo;ll be notified. So I think it&rsquo;s pretty cool, because I mentioned, for example, article about SSC that we have on our blog. So we&rsquo;re trying to share a lot of quality stuff that will be useful for you. Something more, Miroz.</p> <p><strong>Miłosz</strong> [1:30:19]: I think we have no more questions. Thank you, everyone.</p> <p><strong>Robert</strong> [1:30:22]: Just a reminder. So, Go Event Driven, available for next two weeks. Later, you need to wait for half year. So, don&rsquo;t wait for the last hour, because&hellip;</p> <p><strong>Miłosz</strong> [1:30:34]: Better to join now.</p> <p><strong>Robert</strong> [1:30:35]: We are starting, and you will need to wait. Cool.</p> <p><strong>Miłosz</strong> [1:30:41]: Thank you, everyone, for joining us. Thank you, Robert, for joining me.</p> <p><strong>Robert</strong> [1:30:46]: Thank you, everyone. Thank you, Miłosz. And see you in two weeks. And we&rsquo;ll talk about&hellip;</p> <p><strong>Miłosz</strong> [1:30:52]: Event-driven architecture more in-depth than today.</p> <p><strong>Robert</strong> [1:30:55]: Sounds like a good plan. So, see you in two weeks. Thank you very much.</p> <p><strong>Miłosz</strong> [1:30:58]: Bye-bye.</p> <p><strong>Robert</strong> [1:30:59]: Bye.</p>Watermill: from a hobby project to 8k stars on GitHubhttps://threedots.tech/episode/history-of-watermill/Wed, 14 May 2025 16:00:00 +0000https://threedots.tech/episode/history-of-watermill/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Solve real problems first</strong> - successful open source projects start by addressing actual needs, not by looking for problems to fit a solution</li> <li><strong>Keep breaking changes minimal</strong> - Watermill stayed on v1 for 6 years with no breaking changes in the core library, building trust with users</li> <li><strong>Examples and documentation are crucial</strong> - provide real-world examples with automated tests, not just simple &ldquo;hello world&rdquo; demos</li> <li><strong>Promotion matters</strong> - creating a great library isn&rsquo;t enough; you need to actively share it through conferences, blog posts, and communities</li> <li><strong>Be patient with growth</strong> - Watermill took 7 years to reach 8,000 stars; overnight success in open source is rare</li> </ul> <h2 id="introduction">Introduction</h2> <p>In this episode, we share the story of how Watermill, our event-driven library for Go, grew from a side project to a popular open source library with over 8,000 GitHub stars and 100+ contributors.</p> <p>We discuss the key decisions and strategies that helped make Watermill successful, from focusing on solving real problems to maintaining backward compatibility and building a community around the project.</p> <h2 id="show-notes">Show notes</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">Watermill documentation</a> — our open source event-driven architecture library for Go</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill" target="_blank">Watermill repository</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fevent-driven%2F" target="_blank">Go Event-Driven</a> training that complements Watermill</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples" target="_blank">Real World Examples</a></li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>My idea was that it should be possible to make building event-driven applications as easy as HTTP handlers. And if you think about building HTTP APIs, actually you don&rsquo;t need to care about many things when you are building it. So you don&rsquo;t care about connection pooling, handling TLS, HTTP protocol, TCP protocol, DNS resolution, partial reads, network failures.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>It&rsquo;s not as easy as upload anything to your GitHub so some recruiter can tell you know how to code. But rather, if you spend time and maintain the library over time and people find it useful, then it can work like this.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>If you would like to create a library that people like to use, create something that people need. I know that it sounds obvious, but I&rsquo;ve seen a lot of libraries that, well, the use case may be clear for the author, but if the author is posting it somewhere, people are like, yeah, it&rsquo;s cool, but we don&rsquo;t really know how to use that and how it can be useful for us.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>If someone sees a project that has not been updated since a few months, they might be afraid to use it again. Like, is it deprecated? Is it maintained? Because I&rsquo;ve seen there is no updates. So regular updates are often a good idea.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>If you are building your library and you have pretty complex examples, it&rsquo;s a really good idea to start with some end-to-end tests for them from day zero.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>Simple examples are fine to start with, but then when you try to apply this in your own projects, you will see it&rsquo;s not that simple. I guess it&rsquo;s better to have some more complex examples and see how it works in real life.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D0s">00:00:00 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D66s">00:01:06 - What is Watermill?</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D174s">00:02:54 - Why we created Watermill</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D503s">00:08:23 - The long journey</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D633s">00:10:33 - Why create open-source projects?</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D822s">00:13:42 - Create something people need</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D1100s">00:18:20 - Early feedback</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D1200s">00:20:00 - Documentation and examples</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D1514s">00:25:14 - Libraries vs frameworks</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D1577s">00:26:17 - Public API design</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D1713s">00:28:33 - Testing strategy</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D2068s">00:34:28 - Breaking changes policy</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D2506s">00:41:46 - Maintenance challenges</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D2582s">00:43:02 - Marketing is necessary</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D3018s">00:50:18 - Commercial synergy</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D7cW2hm9uucY%26amp%3Bt%3D3320s">00:55:20 - Q&amp;A session</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Robert</strong> [0:00]: Did you ever wonder what&rsquo;s the real key to build a successful open-source project? Today, we are going to share what helped us to grow Watermill from a small sideproject to a popular open-source library with over 8,000 stars on GitHub. What have we learned along the way? I&rsquo;m Robert.</p> <p><strong>Miłosz</strong> [0:19]: And I&rsquo;m Miłosz, and this is No Silver Bullet Live podcast, where we discuss mindful backend engineering. We have spent almost 20 years working together in different projects and teams. We learned that following advice like always do X or never do Y doesn&rsquo;t work and can limit your growth. In this show, we share multiple perspectives that will help you make smart choices and grow into the principal engineer level.</p> <p><strong>Robert</strong> [0:46]: So if you have any questions, leave them on the chat, so we&rsquo;ll surely address them at the end of the live. or maybe during, if it will be also relevant to any place that we&rsquo;ll cover during the discussion.</p> <p><strong>Miłosz</strong> [1:01]: Right, so let&rsquo;s start with what is Watermill for anyone who is not aware. Watermill is a code library we maintain for building event-driven and message-oriented applications, and building it the easy way. And it has currently over 8,000 stars on GitHub, more than 100 contributors.</p> <p><strong>Robert</strong> [1:25]: Probably somebody&rsquo;s using that. So yeah, obviously GitHub stars is some kind of proxy, probably to understand how popular the library is. But, well, we&rsquo;re not buying them, so probably it means something.</p> <p><strong>Miłosz</strong> [1:37]: Yeah, and GitHub also shows, I think, above 1,000 repressories that use it. Some of them are also quite popular. So this is kind of a social proof, I guess, the best we can have that it is in fact used.</p> <p><strong>Robert</strong> [1:51]: The outside is that we don&rsquo;t really know what companies are using that. So we know some, but probably most we don&rsquo;t know because we have no way of backtracking who is using Watermill. So if you are using Watermill in your company, let us know, because it will be cool to know.</p> <p><strong>Miłosz</strong> [2:07]: Sometimes we hear from people on conferences or just randomly on the internet that But yeah, we use Otremio, we love it. But it happens after someone uses it for a few years, let&rsquo;s say. So it&rsquo;s not always the best way to know.</p> <p><strong>Robert</strong> [2:21]: So it&rsquo;s always cool to hear from you that it&rsquo;s useful. So we always appreciate that.</p> <p><strong>Miłosz</strong> [2:27]: Yeah, so Watermill supports 12 officially supported Pub/Subs like Kafka, RabbitMQ, NATS, but also Postgres or MySQL. And we have SQLite support coming soon.</p> <p><strong>Robert</strong> [2:40]: Yep, exactly.</p> <p><strong>Miłosz</strong> [2:42]: So whatever infrastructure you use, you might find it useful.</p> <p><strong>Miłosz</strong> [2:49]: Before we go further, maybe let&rsquo;s focus on what Watermill solves. Why did we create it?</p> <p><strong>Robert</strong> [2:56]: Yes, probably it will be easiest to start with actually how Watermill started. Because I think that this is interesting and it may be also useful for you to try to create some library. And, oh, it was like probably many libraries that are there. So we&rsquo;re working on some project where we had one challenge. So in this project, Event-Driven Architecture would be a perfect fit because of resilience requirements and some tasks that we could do in the background. Upside was that I was already familiar with Event-Driven Architecture, but most of the team wasn&rsquo;t at this point.</p> <p><strong>Miłosz</strong> [3:35]: Yeah, and I remember the meeting when we sat in the room and you draw the Event-Driven Architecture on the whiteboard and you were very confident it would work. And the rest of us were like, I don&rsquo;t know, it seems complicated. And we never used those pops-ups, Kafka is scary, what if we mess something up? Exactly. What would be useful is to have something easy to use, to abstract away those complicated details.</p> <p><strong>Robert</strong> [4:06]: Yeah, and I would say that the bar was set pretty high because my idea was that, it should be possible to make building event-driven applications as easy as HTTP handlers. And if you think about building HTTP handlers, HTTP APIs, actually you don&rsquo;t need to care about many things when you are building it. So you don&rsquo;t care about connection pooling, handling TLS, HTTP protocol, TCP protocol, DNS resolution, partial leads, network failures.</p> <p><strong>Miłosz</strong> [4:37]: Reconnect.</p> <p><strong>Robert</strong> [4:38]: Yeah, it&rsquo;s a lot of things that can happen in the background or it&rsquo;s happening all the time, But you are not thinking about this normally. You are defining, I would like to have handler at this URL with this method, and that&rsquo;s all. And it&rsquo;s really high level, and you don&rsquo;t need to think about all the details. And, well, Watermill is a similar idea. So you should be able to create your event-driven services, but with really high-level API. And adding support, for example, for one event should be super simple. You don&rsquo;t need to really think about topics, Kafka, or any message broker that you are using under the hood. It&rsquo;s not an only thing, because there are a couple more things that Watermill is solving. The next one is handing for you a marshalling and un-marshalling of messages, because usually you are using some transport.</p> <p><strong>Robert</strong> [5:36]: So JSON, protobuf, whatever, and just doing it over and over, it&rsquo;s just boring and a waste of time. Another thing that also is cool and Watermill can support is allowing you to swap PubSub implementation pretty quickly. So if you have seen some presentation that I was doing about Watermill over the time, I often show an example when I was using, for example, Kafka, and I wanted to switch it to Google Cloud PubSub or vice versa. And I was able to do it during live coding, and it took like one minute or less to do that. And it&rsquo;s cool about Watermill because it&rsquo;s putting high-level abstraction on this PubSub. And thanks to that, you can switch it pretty easily, like with ORM, basically, with databases. basis.</p> <p><strong>Miłosz</strong> [6:27]: Yeah, so those are the basics we started with. And then we built more features on top of them. Some of them are quite complex, but with a high-level API, so you can use them easily, like CQRS pattern with commands and queries. Sorry, not queries, but commands and events. Or Poison Queue or the transactional outbox pattern, which is super useful and very common. are commonly needed, not everyone uses them, but probably more people should. And the important part of those features are that they should be easy to use for everyone in the team, even if they don&rsquo;t know how it exactly works under the hood, which is something we would need in that project back then, because we, not all of us were experienced with even the architecture, but it would be a nice approach for the project. So if we had Watermill back then, it would be much easier to start.</p> <p><strong>Robert</strong> [7:30]: Yeah, and probably today we also don&rsquo;t want to go that much into details about some Watermill specifics and things like what&rsquo;s the difference between PubSub and Q, because I know that for many people it&rsquo;s not clear what&rsquo;s the difference, what are use cases for PubSub, what are for queue. It&rsquo;s something that we&rsquo;ll cover more in the episode that will be in two weeks. Today, I would like to more focus on what we believe that made Watermill successful. Because, again, it&rsquo;s probably not the most popular library of all times in Go, but it&rsquo;s probably the most popular library for building event-driven applications in Go. So I think it&rsquo;s a pretty big achievement. And there are a couple of things that we believe that helped us with that. And I think it&rsquo;s also worth mentioning that it wasn&rsquo;t overnight success. And as we mentioned, so it was after some project that we started after work. And we started to spend more and more time on that. And it took us time basically to make it.</p> <p><strong>Miłosz</strong> [8:37]: And the first comment was seven years ago?</p> <p><strong>Robert</strong> [8:39]: Yeah, I think it was seven years ago and seven days, something like that. So yeah, super long time ago. and it took us half year to issue version 0.1 so not 1.0 0.1 so it was some kind of alpha that we were able to raise to people so and i think i didn&rsquo;t really write down how many comments it was but it was a lot of work to at least prepare this 0.1 version i remember i&rsquo;m actually not sure but I think before version 1.0, we spent 500 days to release that. And what also helped us during that time is that we didn&rsquo;t have a daily job, because at this point we, for the first time, tried to go full-time on our own. That time it didn&rsquo;t succeed, but at least we released Watermill 1.0. But it took a lot of effort to do that.</p> <p><strong>Miłosz</strong> [9:41]: It was just going over time, steadily, instead of this huge success right away.</p> <p><strong>Robert</strong> [9:49]: Like in previous episodes, we&rsquo;re not giving you any magical solution that will make your library super popular in one month. Because probably it&rsquo;s often about a lot of luck because you can create a great library, that for some reason didn&rsquo;t take out because there are, for example, 10 other libraries that are doing the same. So there is just a lot of competition. And if you are not lucky, well&hellip;</p> <p><strong>Miłosz</strong> [10:23]: It&rsquo;s difficult with regular products and open source is more specific than that. It&rsquo;s probably worth asking why someone would create a open source project it&rsquo;s probably not to make money you can see many companies and projects try to monetize on open source, it usually doesn&rsquo;t end well all the issues with licensing and so on</p> <p><strong>Robert</strong> [10:50]: I think it&rsquo;s not fully fair when you&rsquo;re starting an open source project and making it closed source after time after more people are using that but unfortunately it&rsquo;s often happening. I guess that often it&rsquo;s some pressure from investors that invested in some company and it kind of makes sense. You can expect that if you are using VC-baked open source that someday investors will come and ask where is my money back.</p> <p><strong>Miłosz</strong> [11:21]: I can count on sponsors as such but it&rsquo;s probably not the idea to do it just for this. We found a way to connect it with our commercial projects, but we will touch on this later. But so far, I think we can summarize that we learned a lot doing it. That&rsquo;s probably the primary reason I would try again to build an open source library. Just to learn something new and publish it. And also gives you some credentials if it succeeds. Like if you&rsquo;re looking for a new job or just want to have some proof of your competence. That&rsquo;s a good way to do it.</p> <p><strong>Robert</strong> [12:06]: I think it&rsquo;s interesting sometimes to hear on the interview, on both sides actually, because sometimes when we&rsquo;ve been interviewed, some people know Watermill, but I also remember interviewing some people and they were like oh, okay, you&rsquo;re a creator of Watermill and you work here, nice.</p> <p><strong>Miłosz</strong> [12:22]: But that&rsquo;s the difference between a project you just upload to GitHub and no one uses, and some project that some company actually found useful. So it&rsquo;s not as easy as upload anything to your GitHub so some recruiter can tell you know how to code. But rather, if you spend time and maintain the library over time and people find it useful, then it can work like this.</p> <p><strong>Robert</strong> [12:48]: And I think it&rsquo;s also nice when you&rsquo;re recruiting somebody and this person can send you his GitHub or her GitHub and you can see that the person is working on some open source project after hours. And you can have some understanding on how this person is working and you can also you know feel that this person is able to, spend some time after job to learn something maybe or maybe create something cool that other people can also benefit from yeah it&rsquo;s.</p> <p><strong>Miłosz</strong> [13:19]: Also a nice way to give back to the community because we&rsquo;ve been using many open source software before and this way other people can use our ideas which also It also feels nice, you can do it for the satisfaction you get from it.</p> <p><strong>Robert</strong> [13:37]: All right but it all sounds cool but many libraries are like that and they&rsquo;re not that successful so i think it would be good to discuss some things that really makes some projects successful and, some of my biggest observation from other projects and i know it may sound obvious but, if you would like to create a library that people like to use create something that people need because I know that it sounds obvious, but from other side, I&rsquo;ve seen a lot of libraries that, well, the use case may be clear for the author, but if the author is posting it somewhere, people are like, yeah, it&rsquo;s cool, but we don&rsquo;t really know how to use that and how it can be useful for us. And, yeah, it also doesn&rsquo;t mean that it needs to be some solution for a problem.</p> <p><strong>Miłosz</strong> [14:36]: A very generic problem, right?</p> <p><strong>Robert</strong> [14:38]: Yeah, because solving generic problems is more harder because it requires more effort. So probably it&rsquo;s good to find something in the middle, like, for example, water mill. So, creating event-driven applications, it&rsquo;s not something that every company does, but a lot of companies does. So, it&rsquo;s big enough, let&rsquo;s say, market, if we can say it this way. But, okay, we can say that if you&rsquo;re building open source library, it&rsquo;s pretty close to creating a product. So, you can say that it&rsquo;s some kind of market for that. And it&rsquo;s big enough that there are many people that can use that and benefit from that. but it&rsquo;s also kind of niche so there is no competition around there so it helps a lot.</p> <p><strong>Miłosz</strong> [15:25]: Yeah, and sometimes if you start a project and you have a problem you want to solve and then you look for a solution and you find an open source library so you might feel discouraged to try and build your own I think it still makes sense to try because you may have a fresh approach. You can provide a better API or better design or less dependencies. So there&rsquo;s definitely a place for more libraries that solve the same thing.</p> <p><strong>Robert</strong> [16:01]: Yeah, definitely. So yeah, I think it&rsquo;s a good&hellip;</p> <p><strong>Miłosz</strong> [16:04]: In Go ecosystem, it was clearly visible because in the very beginning there were only a few libraries to do everything. So everyone sticked to one, let&rsquo;s say, HTTP router. Because there were no other options. But as other libraries appeared, many people moved on. Aside from routers, maybe CLI libraries are also a good example. So there&rsquo;s still a place for better APIs or a different approach that will help people to do something in a better way.</p> <p><strong>Robert</strong> [16:47]: Again, try to focus on the problem that you are solving and avoid trying to find a problem for your solution. So I&rsquo;ve seen always that people found some cool technology or some cool idea, but they didn&rsquo;t have a real use case for it. I think it&rsquo;s probably easy to do AI now. So you can try to put AI in many places, and it will be helpful in many places. But in many places, it will be more looking for problem, for solution that you have. So you should also really watch out for that. I think it&rsquo;s one interesting observation about this focus at the beginning on solving the problem. I think at the beginning, we didn&rsquo;t know what word terminal is. So we had, as we said earlier, we had a problem that we had people that didn&rsquo;t build any event-driven application earlier. And we didn&rsquo;t have an idea, okay, we need to build a library for abstraction.</p> <p><strong>Miłosz</strong> [17:51]: All use cases everywhere.</p> <p><strong>Robert</strong> [17:52]: Yeah, yeah. Fun fact is that actually Watermill at the beginning, when it was alpha, it was named GoDDD. So it was probably something that, okay, it was around these domain events that we had there. And it started with that.</p> <p><strong>Miłosz</strong> [18:07]: I&rsquo;m still not sure why.</p> <p><strong>Robert</strong> [18:11]: Probably we&rsquo;ll never know. And yeah if you&rsquo;re experimenting with time you are experimenting with time, it&rsquo;s important to not keep this for you forever so create some version that will be useful for people and try to share it ASAP for feedback.</p> <p><strong>Miłosz</strong> [18:40]: Just mark it as experimental so people are aware that the API can change so they are not annoyed. It&rsquo;s normal to have some proof concept and then keep changing the API if you find a better way. That&rsquo;s fine. Just make sure people are aware. But this early feedback can be super helpful though.</p> <p><strong>Robert</strong> [19:02]: Yeah, and one of the best early feedback that you can have, and it can be also a nice hint how to find a, problem that people care about is using it in your daily job as long as you can because i know that it depends it&rsquo;s maybe hard in some teams but like in our case so we&rsquo;ve been building it after hours but we&rsquo;ve worked in a company that it was fine to just use it in a real project that we worked in so thanks to that we had real-time feedback for that and we&rsquo;ve been able to dog foot it with it but Obviously, it&rsquo;s just a starting point. At some point, you need to share it outside so other people can give you feedback, and you can adjust to that, because you will be not always right from day one.</p> <p><strong>Miłosz</strong> [19:54]: And after a while, it&rsquo;s probably a good time to start with documentation. We have some docs in place, and the go doc we have is not enough, usually. Because the go documentation is not that bad, but it&rsquo;s still&hellip; It&rsquo;s sometimes hard to get started with just the code documentation. So, we found it very useful to have this getting started page, so someone can easily see how to install the library and some minimal example to get started, also some minimal theory. And then lots of real-life examples are also super useful, which is sometimes… I&rsquo;m super happy when I see some library and I see the examples directory, I often go straight there, instead of trying to figure out how it works. Because you can just copy some snippets and it gives you a better idea of how it works, rather than trying to understand from the code itself.</p> <p><strong>Robert</strong> [21:02]: Or clone it and run it locally and play with that. So it&rsquo;s also pretty useful. And I think it&rsquo;s also pretty close to our way how we believe that learning is the best. So actually learning by doing. So it&rsquo;s really close to it.</p> <p><strong>Miłosz</strong> [21:21]: And lots of our examples we have for Watermill are real-world examples. So they are more complex than a simple hello world. So you should be able to copy them to your solution and change them, and it should be what you need.</p> <p><strong>Robert</strong> [21:36]: Yeah, so if you&rsquo;re longer with us, you should know that we don&rsquo;t like examples like to-do apps or some fake examples, because usually it&rsquo;s just hiding a lot of complexity. Okay, maybe some Watermill examples aren&rsquo;t that simple, but they&rsquo;re as simple as they can to show how to solve these problems.</p> <p><strong>Miłosz</strong> [21:59]: Simple examples are fine to start with, but then when you try to apply this in your own projects, you will see it&rsquo;s not that simple. I guess it&rsquo;s better to have some more complex examples and see how it works in real life.</p> <p><strong>Robert</strong> [22:14]: Yeah, for example, we have the example with handing ordering in your messages. And this topic is not simple. There are a couple things that you need to do right. It also differs a bit for different PubSubs. But again, it&rsquo;s easier to give you more complex example rather than you need to figure it out yourself. Because we know that it&rsquo;s not easy because at some point we needed to figure it out as well. At the end, your life will be easier with those examples. One tip. So if you are building your library and you have pretty complex examples, it&rsquo;s a really good idea to start with some end-to-end tests for them from day zero. So every example that we have, it&rsquo;s included in the documentation, but we have also automatic tests that are checking if those examples are working properly. So it&rsquo;s cool because when we are releasing a new version of WaterMill, we&rsquo;re updating dependencies, we can just run tests on all examples and ensure that all of them are working previously.</p> <p><strong>Miłosz</strong> [23:18]: It&rsquo;s easy to break them without a version bump or something like this.</p> <p><strong>Robert</strong> [23:26]: But it&rsquo;s worth mentioning that it&rsquo;s not that easy, unfortunately, to create such tests. Sometimes it&rsquo;s taking a pretty long time to run them. But again, it&rsquo;s still better than not having an automation for that. Because doing it by hand is 10 times slower. And it&rsquo;s nothing worse than examples and documentation with snippets that doesn&rsquo;t work. Because it&rsquo;s just a super bad experience. Because if somebody is trying to use your library and your examples are not working, Well, probably the person may just leave your library and don&rsquo;t use it at the end.</p> <p><strong>Miłosz</strong> [24:03]: Yeah, but assuming they stay, you usually will have some early adopters of your library. It&rsquo;s nice to have a place for communication, for building the early community. So in our case, it was issues and then Discord. Our Discord and dedicated channel. So it gives you a place to listen for this feedback from people and your suggestions. And also sometimes they contribute, which is super nice. You have your first contributors, so it&rsquo;s not just your project, but you start to feel like you maintain it, actually. It starts to get more real. What other things help?</p> <p><strong>Robert</strong> [24:53]: The next thing that is coming to my mind is the fact that Watermill is not a framework. So, we&rsquo;ve covered it more in the previous episode about frameworks, but TLDR is that. We are not big fans of frameworks in Go for multiple reasons. And I think it also helps with building a library when this is not a framework.</p> <p><strong>Robert</strong> [25:14]: For one reason, because it&rsquo;s easier to adopt if it&rsquo;s just a library. Because when you are starting to use some kind of framework, it requires more changes in your code. And I guess that many people may be also not&hellip;</p> <p><strong>Miłosz</strong> [25:34]: So it locks you in.</p> <p><strong>Robert</strong> [25:36]: Sorry?</p> <p><strong>Miłosz</strong> [25:37]: It locks you in, so it&rsquo;s harder to migrate out of it. And we chose this way of being more independent. So it&rsquo;s easy to throw a water meal away, if you choose to do so, and replace it with something else. And that&rsquo;s my design, because we prefer working with smaller libraries that are not as heavyweight as frameworks.</p> <p><strong>Miłosz</strong> [26:13]: Maybe something similar is to how public API design matters. I think in Watermill the publisher and subscriber interfaces are quite good examples of how having a simple interface and the complexity hidden behind it can lead to nice API of the whole library. And we have the higher level components built on top of them. So it&rsquo;s&hellip; quite&hellip; easy to use and fun to try. Because you don&rsquo;t need to dig through the documentation to understand how it works. Use those two functions together and you say, oh yeah, okay, this is how this stuff should work.</p> <p><strong>Robert</strong> [27:09]: And I think it&rsquo;s also cool because it&rsquo;s giving you a lot of flexibility. Because we are giving you some building blocks in Watermill, but we know that there are a lot of different kind of projects. And it&rsquo;s just impossible to create a library that will fit everybody&rsquo;s needs. But if it&rsquo;s built in the modular way, it can be adopted much easier to totally different use cases. So you can just use some of the components, use some more lower-level components, for some cases, some higher-level components. So to give you some examples, so you have in WaterMill, you have high-level interface for handling events that is handling marshalling for you, but you have also lower-level API that you can use, interact with PubSub more directly. So it just will have message with slice of bytes payload, and that&rsquo;s it. And it may be a great choice for some low-level processing or data pipelines. It was hard people that they were using it in some strange cases, let&rsquo;s say.</p> <p><strong>Miłosz</strong> [28:17]: So you can choose what to use, basically.</p> <p><strong>Robert</strong> [28:20]: Yeah, and it&rsquo;s just making a list of use cases that we can support much, much bigger.</p> <p><strong>Miłosz</strong> [28:28]: And what about testing strategy? That&rsquo;s also something we use, a unique approach.</p> <p><strong>Robert</strong> [28:34]: Yes, it&rsquo;s interesting because if you think about the abstraction over PubSub, how it works in Watermill, so basically PubSub is implementation of one interface with two methods. PubSub publish and subscribe. So you can guess that publish method is responsible for handling publishing of your message, and subscribe. Yes, you guessed. It&rsquo;s for subscribing for those messages that you just published to your topic. And every PubSub implementation that we have basically needs to implement this interface. And at some point, when we&rsquo;ve been thinking about how to test that and how to implement many of the PubSubs, we found out that actually, okay, if every PubSub should implement this one interface, basically every PubSub should work in a similar way. So your publishing message with publish and subscribing, and it should run back. Obviously, it may sound like a pretty simple thing. At the end, I think we have probably like 30 different use cases, or 30, 50, I don&rsquo;t remember.</p> <p><strong>Miłosz</strong> [29:41]: You can configure them depending if this PubSub supports them or not. The funny thing here is how you can adjust and adapt many different systems to implement this interface. Probably one unusual thing is the HTTP-based PubSub. So you can just do it over HTTP requests and responses.</p> <p><strong>Robert</strong> [30:03]: It&rsquo;s basically webhook.</p> <p><strong>Miłosz</strong> [30:05]: Yeah.</p> <p><strong>Robert</strong> [30:07]: We have also PubSub, for example, based on the file system. I mean, io.Reader, io.Writer.</p> <p><strong>Miłosz</strong> [30:14]: So you can do it over files or whatever else you&hellip; We should probably build one over email.</p> <p><strong>Robert</strong> [30:25]: Yeah, it would be cool.</p> <p><strong>Miłosz</strong> [30:28]: But this goes back to this design of the API, right? If you choose this nice interface, the rest kind of falls in place automatically. And I see many libraries that don&rsquo;t care that much about it. And you have these public methods or functions that take many arguments, and you have to check how they work before you start using them. And they could probably use some more high-level APIs that would be easier to use. So if someone is wondering if there&rsquo;s still a place for some other libraries, I think there&rsquo;s plenty.</p> <p><strong>Robert</strong> [31:14]: And there&rsquo;s one cool thing about having this one test suite for every PubSub that we support. This is the fact that if you&rsquo;re implementing a new PubSub implementation, basically you don&rsquo;t need to write a test because tests are already there. And to be super exact, we have implemented, for example, Google Cloud PubSub at some point without having access to any production system that was working on Google Cloud PubSub. But we know that, OK, it&rsquo;s cool because it&rsquo;s hosted. And we implemented it basically just based on test. And I think a couple months later, we joined a company that was using Google Cloud PubSub. And we think, OK, let&rsquo;s try if it works with that. And we used this PubSub that we just implemented based on tests. And it just worked on production without, and a big adjust. So I think it was just some context cancelling or some really minor thing, but everything else, it just worked as expected. So it&rsquo;s a cool thing because it&rsquo;s ensuring consistency about multiple Babsops. So we can say that we have one standard of how PubSub implementation should work. And when you&rsquo;re implementing a new one, it&rsquo;s just much faster to do that, and even if you don&rsquo;t have access to any system that works on this PubSub.</p> <p><strong>Miłosz</strong> [32:29]: It&rsquo;s just a nice exercise to try to bring your own PubSub implementation because of those tests. So now you have this nice system that tells you what the code you wrote is correct or not.</p> <p><strong>Robert</strong> [32:45]: It&rsquo;s a bit like a quiz. You have a quiz and you need to solve multiple challenges to make it working.</p> <p><strong>Miłosz</strong> [32:54]: It&rsquo;s also quite easy to do it, right? I remember the NATS PubSub we created. We did it over one day on a hackathon with a couple of friends. Yeah, because it&rsquo;s just one interface. If you can implement it, then you&rsquo;re done.</p> <p><strong>Robert</strong> [33:10]: So if, for example, you are using some PubSub that we are not supporting in Watermill, well, we are open to accept contribution, obviously. But the most important thing, your tests needs to pass and be stable. If they&rsquo;re passing and they&rsquo;re stable, well, we can talk. But we&rsquo;re also happy to help with that because we know that in some systems, debugging is a bit harder. It&rsquo;s also useful to know how to use grep and pipe your logs to text files because sometimes&hellip; Oh, implementing PubSub may be not that easy when SDK is not best.</p> <p><strong>Miłosz</strong> [33:49]: But it&rsquo;s doable. Yeah, and you can start with the happy path. Because there are many different configuration options, usually. And once the implementation is successful, you have people asking for more features or changing the default behavior, stuff like this. But the HappyPath scenario is usually quite easy to do. So you can focus on this and later add the configuration.</p> <p><strong>Robert</strong> [34:20]: There&rsquo;s another thing that I think is also important when you are building a library that people should trust in. And this is some policy of how you are introducing breaking changes. For example, Watermill is since, I think, six years in the major version V1. So in other words, we didn&rsquo;t make any breaking changes in the core library. We did some in some PubSub implementations just because some SDK was updated or we had some good reason for doing that. But in general we are avoiding that and in the core we didn&rsquo;t make any breaking change so obviously it has a downsize because we need to even if we&rsquo;re deprecating some methods we need to keep them backward compatible keep old functions still working but from another side it&rsquo;s very, easy to update for every people and i remember that i was using some libraries that were doing some breaking changes without bumping a major version. It&rsquo;s not the best experience.</p> <p><strong>Miłosz</strong> [35:21]: Even with major bumps, people often tend to not migrate at all. Even if it&rsquo;s just a couple of methods that change, it&rsquo;s some effort. Especially if you use this library in some part of the system that&rsquo;s critical, like messaging, or maybe it&rsquo;s a part that is not used much, so you don&rsquo;t know if something breaks. Then you are more likely to postpone upgrading. So, yeah, we want to keep the v1 promise.</p> <p><strong>Robert</strong> [35:52]: And important disclaimer, so no breaking changes, it doesn&rsquo;t mean only that the public interface doesn&rsquo;t change, because it&rsquo;s also about keeping behavior as earlier. Because I would say that it&rsquo;s even worse, because if you are breaking your API in a way that the compiler will say you that, okay, it doesn&rsquo;t work, it&rsquo;s fine because you will see the compilation error and you can fix that. But if you&rsquo;re changing your library behavior, but without breaking interfaces, it&rsquo;s pretty bad because you may expect some, consistency in how it works. And in general, you don&rsquo;t want to surprise your library users.</p> <p><strong>Miłosz</strong> [36:35]: And probably the best way to avoid breaking changes is using configuration structs for the top-level methods. I think we had to bump the Kafka PubSub implementation to v2 very early, because we did more positional arguments and changing them is a breaking change always. Or you need to introduce a new constructor, which is annoying.</p> <p><strong>Robert</strong> [37:03]: Yeah, I think we also made the similar thing with SQL PubSub because we needed to add more parameters in some interfaces that we have in that library. PubSub. And by the way, if somebody is surprised, you have SQL PubSub. So yeah, we have SQL PubSub. So if you are in the stage that you cannot effort, let&rsquo;s say, to have extra infrastructure piece or Kafka, RabbitMQ or whatever, you can just use Postgres or MySQL. We have also benchmarks and we checked how fast it works. And for many systems, it will be more than enough. but it&rsquo;s just a bit off topic.</p> <p><strong>Miłosz</strong> [37:52]: On this topic, one more thing to add is to keep the V0.1 before V1 so you can experiment with the changes and people are aware this is an experimental version. But at some point, you probably want to release the 1.0 because it builds trust. If someone says your software is always in a beta release, they might not be eager to use it in the project they run. They will keep asking, is it production ready or not? But this buffer, which in our case was two years or something, quite a long time basically. But it was useful to experiment with different approaches, and then once we were ready to finalize the API, we could just froze it, and that&rsquo;s how it stayed over time. And we kept adding more features, of course, more high-level components, but they all worked with the low-level API.</p> <p><strong>Robert</strong> [39:03]: So the tactic that we often use is just keeping the old functions or old types that we had there, marking them as deprecated, and suggesting to use another one. So a good example is our SecureS component for handling events and commands. And at the beginning, we have some super weird, crazy, complex interface. Now, it&rsquo;s still there, but it&rsquo;s deprecated. And we have nicer API using generics now that you should use. But if you are using the old one and you don&rsquo;t have time to migrate, okay, that&rsquo;s totally fine. It&rsquo;s still working as it worked five years ago. So I think it&rsquo;s pretty cool.</p> <p><strong>Miłosz</strong> [39:43]: And it wasn&rsquo;t huge efforts to keep it backwards compatible. Most of the time, it&rsquo;s not. You just need some deprecation commands and maybe keep some old tests in the repository.</p> <p><strong>Robert</strong> [39:56]: I think the biggest challenge is often how to name the new version. Because often you use the good name already and deprecate that and have some good names. I think we didn&rsquo;t do that, so we didn&rsquo;t put any suffix like v2 or whatever, but in probably worst case, you can do it. I think we have also this with the protobuf marshaller that we have in Secure S component. So I think the old one was the protobuf and the new one is proto. Not perfect, but probably a bit better than v2 suffix or something like that. Yeah.</p> <p><strong>Miłosz</strong> [40:35]: Naming and choosing a good API is one of the challenges definitely. Another challenge that&rsquo;s kind of specific to Watermill is we support 12 PubSoaps right now. And most of those systems is quite complex. Like Kafka can be configured in many ways and used in many different scenarios. And we are not able to run all of those systems in production. We use some of them. So yeah, that&rsquo;s one of the challenges here is to keep up with the community. So you have to keep it in mind that once the library starts growing and future requests are coming and you need to keep up and review them. And if you are not using some Pub-Sub, in our case, in production, it&rsquo;s sometimes hard to tell if the changes make sense.</p> <p><strong>Miłosz</strong> [41:41]: In general, maintenance is important, because if someone sees a project that has not been updated since a few months, they might be afraid to use it again. Like, is it deprecated? Is it maintained? Because I&rsquo;ve seen there is no updates. So regular updates are often a good idea.</p> <p><strong>Robert</strong> [42:06]: Yeah, I think it&rsquo;s often also challenging when your project is kind of feature-complete. Like, okay, I wouldn&rsquo;t say that WaterMill is feature-complete because we have still many ideas what we can add there. But from the other side, the features that are there are feature-complete. I mean, we can probably just not touch WaterMill for the next 10 years and it will be enough. We maybe just can update other PubSub implementations, but that&rsquo;s it.</p> <p><strong>Miłosz</strong> [42:33]: It&rsquo;s stable.</p> <p><strong>Robert</strong> [42:35]: Yes, yes.</p> <p><strong>Miłosz</strong> [42:36]: Yeah, but very often some security updates also make sense or library bumps, version bumps.</p> <p><strong>Robert</strong> [42:44]: Or license changes.</p> <p><strong>Miłosz</strong> [42:46]: Yeah, that also happens. So that&rsquo;s one of the challenges. So it&rsquo;s not only challenging to start, but then you also need to maintain it over time.</p> <p><strong>Miłosz</strong> [42:58]: Maybe to change the topic a bit, we should talk about marketing. Which is probably not the first thing you think about when you consider starting an open source project.</p> <p><strong>Robert</strong> [43:11]: Yeah, but as we already mentioned, running open source project library, it&rsquo;s pretty close to building a product. So I think we&rsquo;ve been lucky to already build a couple less or more successful products. And I think it also helped us to learn multiple things. But I think one of the most important things is that it doesn&rsquo;t work. So just creating great product, great library isn&rsquo;t enough for people to use that. So you need to let other people know that it&rsquo;s great product, it&rsquo;s great library, because it doesn&rsquo;t work in the way that people will find it. No, they won&rsquo;t. The chance is really, really low.</p> <p><strong>Miłosz</strong> [43:54]: Yeah, and there&rsquo;s no easy way to track how popular the project is. GitHub stars are one way, which can be a vanity metric. It doesn&rsquo;t tell you very much.</p> <p><strong>Robert</strong> [44:07]: You can buy them.</p> <p><strong>Miłosz</strong> [44:08]: You can buy them, yeah. But it also gives you some social proof. And in general, if your library is used, people will be more eager to use it as well. I remember once, when we joined the company, at the time when we released the 1.0, I think. I remember we wanted to introduce it internally to other teams. And there was some kind of internal library like this before, but it was kind of limited and didn&rsquo;t have a nice API. So there was this meeting of senior engineers and this kind of meeting where you have 10 or 15 people that gather to decide a thing in one hour</p> <p><strong>Robert</strong> [45:02]: And it costs a lot of money.</p> <p><strong>Miłosz</strong> [45:04]: Yeah it never ends with a decision it often ends with another planned meeting and I remember we drifted from the main topic and just kept talking about which library we should use and then one of the principal engineers starts reading mail on his phone because he&rsquo;s bored and he finds Golang Weekly newsletter and water mirrors featured in there and he says out loud guys you are in Golang Weekly and somehow this ended the conversation it was like a proof that oh yeah this library I&rsquo;m sure it&rsquo;s worth something Which is funny, because it didn&rsquo;t mean some company used it on production or whatever. Maybe they did, but basically social proof is something you need to succeed in open source too. That&rsquo;s why often projects also add some logos of companies that use it to the readme or website. Because then you go like, oh, okay, this company uses it, so it must be a good library. But of course, they don&rsquo;t tell you that this is used by some obscure testing framework this company uses or something like that.</p> <p><strong>Robert</strong> [46:33]: Or it&rsquo;s just used in some service that two people are using in this company. But, you know, it&rsquo;s often like that.</p> <p><strong>Miłosz</strong> [46:41]: But long story short, you need to promote it, whether you like it or not. And the obvious answer is where to do it? Is Hacker News and Reddit? What other places you suggest? Maybe some conferences?</p> <p><strong>Robert</strong> [46:59]: Yeah, so this is the thing that, for example, I like to do a lot. Maybe some of you had the chance to see my presentation on some conferences. I really love to talk about watermelon conferences. Also on every bigger release that we have, we&rsquo;re trying to write some blog posts, that we can share what&rsquo;s new there.</p> <p><strong>Miłosz</strong> [47:24]: Also the examples are one way to promote it sometimes we have more complex examples and some blog posts around it, like the one with servers and events which also features Watermill, but it&rsquo;s not only about Watermill, it&rsquo;s how to do something very specific in your application.</p> <p><strong>Robert</strong> [47:47]: Yeah, and some of you may ask, all right, but I&rsquo;m posting that and nobody cares and nobody&rsquo;s avoiding that. And I think it&rsquo;s often all about going back to the one of the first points that we have. So maybe it&rsquo;s something that doesn&rsquo;t resonate with people, because maybe they don&rsquo;t understand how to use that, or maybe they don&rsquo;t have use case for this library. And yeah, I think that this is part of why Water on You is that popular. It&rsquo;s solving real problems that people have and is the key. Again, if people are not happy about using that, maybe it&rsquo;s not solving problems that they have. Maybe you should try to find another idea that you can help people with a form. Maybe people don&rsquo;t understand what this library is doing, what problems it&rsquo;s solving. Quite to focus on use cases, on emphasizing what problems the library solves. And it will help a lot when you&rsquo;re posting it to Hacker News or to Reddit.</p> <p><strong>Miłosz</strong> [48:46]: I was browsing GitHub today and I found a library with README and there was a big banner that says share your ideas for your use cases so I think that might be a sign you don&rsquo;t know why this library exists so if you can come up with some real world examples featuring your library, that&rsquo;s probably a good sign yeah,</p> <p><strong>Robert</strong> [49:14]: So watch out for looking for problems for solution that you have, because often it may be not the best thing yeah, All right, so we&rsquo;ve shared a couple things about watermills, what we believed it was successful. I hope that it will be pretty useful if you&rsquo;re building your own project or maybe you&rsquo;re looking for some inspiration. If also watermill and eventview application sounds useful for you, but you didn&rsquo;t have any chance to build such kind of system and you don&rsquo;t know how to do that, Obviously, we have our examples, we have our documentations, but we also know that just reading examples, reading documentation is not enough for learning. So we already covered it in the previous episode, I think. But if you are looking for something about event-driven applications, you&rsquo;re pretty lucky, probably, because twice a year we are running our online training about building Go event-driven applications in Go. And actually we&rsquo;ll be starting in next three weeks. And it&rsquo;s also a pretty special edition because we have a lot of updates. So it will be V2 of Go event-driven training. Breaking change. Yes, but yeah, it&rsquo;s exactly breaking.</p> <p><strong>Miłosz</strong> [50:43]: What didn&rsquo;t happen to Watermill. But sometimes it&rsquo;s better to do it.</p> <p><strong>Robert</strong> [50:47]: Yeah, yeah. So what we have made, we&rsquo;ve gathered all the feedback that we had from people from previous editions. and we spent a lot of time to implement them, and we believe that it will be better than it was earlier. This is also the first time when we will be doing it in the cohort way, so we will go with the content over four weeks with a group of other people. So let&rsquo;s see how this form will work. We believe that it will be much better.</p> <p><strong>Miłosz</strong> [51:23]: And everyone who bought it before will have access to this new one, like we promised. So we get all updates for free.</p> <p><strong>Robert</strong> [51:34]: Promises are important for us, so we&rsquo;re keeping that. One of the promises is that when you&rsquo;re buying at this point, it&rsquo;s the best price that you have all the time. So you can join in-house here, probably more or less, but it will be more expensive. So it&rsquo;s definitely we encourage to join earlier. And you have also lifetime access for that. So it&rsquo;s not like you need to finish it with this cohort now. You can do it in your own time. When you have time, you can do it now, in one year, whatever works for you.</p> <p><strong>Robert</strong> [52:09]: But yeah, it&rsquo;s just available from time to time. It&rsquo;s not available for the time. So if it sounds cool for you, just Google it. It&rsquo;s named Go Event Driven. There&rsquo;s a page about that. And then again, it will be available in three weeks. So stay tuned.</p> <p><strong>Miłosz</strong> [52:26]: And actually, Go Event Driven is also a nice synergy with Watermill. Because we feature Watermill in the training. Because it&rsquo;s about event-driven applications. And it lets us show how to build event-driven applications from a high-level perspective. But it&rsquo;s not a training about watermill so we don&rsquo;t make money on watermill directly but it&rsquo;s this synergy where we have this incentive to keep improving watermill to make the training even better so I think it&rsquo;s a quite unique example of how you can take an open source project and connect it to something commercial but also So it&rsquo;s not about sponsorships, or changing the license to closed source, or maybe even running an enterprise edition. Which often doesn&rsquo;t work, because some cloud takes your source code and does it cheaper.</p> <p><strong>Robert</strong> [53:30]: And it&rsquo;s also worth mentioning that we&rsquo;re also not stripping documentation just to force you to buy the training. I mean, if you already build event-driven applications in other programming languages, Watermill documentation will be totally enough for you. You can go over the documentation, understand how Watermill works, and that&rsquo;s it. Because some people may think that, oh, you need to buy training to understand Watermill. So it&rsquo;s separate. And it&rsquo;s also separate in the other way. So it&rsquo;s not training about Watermill, it&rsquo;s about universal patterns that works in every programming language. So we&rsquo;re using Watermill because it&rsquo;s making stuff easier, but we&rsquo;re planning in the future also to create additions in other programming languages. Because again, there&rsquo;s no training about Watermill. You can replicate it in any programming language and it&rsquo;s universal knowledge that works now, worked 20 years ago and probably will work in 20 years.</p> <p><strong>Miłosz</strong> [54:28]: Well, we will have even given AI systems.</p> <p><strong>Robert</strong> [54:32]: Maybe AI will learn from our training. And as always, when we&rsquo;re mentioning WaterMill, we always are sharing a big thank you to all contributors. We mentioned that we have more than 100. It&rsquo;s hard to say because it&rsquo;s across almost 20 repositories, and it&rsquo;s hard to deduplicate and count them, but I think it&rsquo;s more than 100 people across all repositories. Hard to say the exact number. But yeah, real thank you, because without you, Watermill wouldn&rsquo;t look like you now, because we&rsquo;re just two, we&rsquo;re 100. So it&rsquo;s really, really helpful.</p> <p><strong>Miłosz</strong> [55:14]: Right. Let&rsquo;s move on to the Q&amp;A then.</p> <p><strong>Robert</strong> [55:17]: Yeah, so if you have any questions about Watermill, about anything, just leave it on the chat, and we&rsquo;ll be more than happy to answer them. Yes, I see that there is some comment about focus on me. Yeah, so we&rsquo;re fighting with cameras to get the good settings, but yeah, I see. Probably it&rsquo;s pretty good now, but light may be not perfect. Although, yeah, we are trying to improve every episode. Adrian is also asking, when creating a project, do you consider performance at the start of the project? Is it wise to make it run first, then think about performance later? Because performance plus design do go together right. What do you think, Niosh?</p> <p><strong>Miłosz</strong> [56:14]: Yeah, good question.</p> <p><strong>Robert</strong> [56:16]: It depends.</p> <p><strong>Miłosz</strong> [56:19]: So for what I mean, we have benchmarks. and that&rsquo;s kind of objective way to measure how fast it is. But we never explicitly focused on performance, because most of the time it&rsquo;s not that critical. Even for the SQL PubSub, which is super slow, we calculated that you can process, I don&rsquo;t know, a few million messages per day or what was the count?</p> <p><strong>Robert</strong> [56:55]: Per hour probably, I don&rsquo;t remember.</p> <p><strong>Miłosz</strong> [56:57]: It&rsquo;s slow but still pretty fast for most use cases and I think we never had anyone complain about performance but it&rsquo;s a good comment about the design. I guess one example could be if the design didn&rsquo;t support batch requests, that could be pretty bad. That&rsquo;s probably something to keep in mind. And that&rsquo;s probably different in open source projects than some internal libraries. Because if you finalize the API and then it turns out it&rsquo;s a blocker for some performance, action, then it&rsquo;s a problem. So, for example, the publish of Watermill Publishers supports multiple messages in one batch. I guess that might be one example how it can help. If all you could do was publishing one by one, that might be a big factor for performance reasons.</p> <p><strong>Robert</strong> [57:58]: Yeah, but I think the most important thing is about thinking about use cases for the projects, because in some libraries it will just doesn&rsquo;t matter and in some cases it may bother yeah.</p> <p><strong>Miłosz</strong> [58:09]: Very often it won&rsquo;t matter</p> <p><strong>Robert</strong> [58:11]: Yeah for example if you&rsquo;re building i know some ai stuff probably doesn&rsquo;t matter because okay you can optimize every memory allocation order your struct fields to use as least memory as you can but when you just call lm it will be super slow and what you can do Oh.</p> <p><strong>Miłosz</strong> [58:29]: I know. Good example. Very popular in Go ecosystem is doing benchmarks for HTTP router libraries. And each library claims they are the fastest. And they have charts where you see that they are, I don&rsquo;t know, faster in nanoseconds than the other ones. But the truth is, if you use HTTP, then you communicate over a network and each request takes, I don&rsquo;t know, 50 milliseconds or 100 milliseconds roundtrip. Then this micro-optimization is not that important really.</p> <p><strong>Robert</strong> [59:14]: And probably you&rsquo;re using JSON for communication. So come on, if you&rsquo;re using JSON for your API and you&rsquo;re talking about optimization, probably it&rsquo;s not the issue.</p> <p><strong>Miłosz</strong> [59:25]: Someone will now say they have a very specific use case for this and they need to process billions of messages per second. And yeah, maybe it&rsquo;s important. But for many projects, it won&rsquo;t be important. Yeah, I guess advice is keep it in mind in terms of API, but probably don&rsquo;t focus on it as the priority for the first release.</p> <p><strong>Robert</strong> [59:52]: So in other words, if you can optimize performance later without breaking changes, maybe it can wait for a while. Because it&rsquo;s all about not introducing breaking changes. Like in Watermill, we can probably optimize a couple of things in terms of performance, but it will not make any breaking change, basically.</p> <p><strong>Miłosz</strong> [1:00:13]: Unless maybe you want to create a similar library, like one that exists already but that is faster or makes zero allocations or something like this, then it might be useful.</p> <p><strong>Robert</strong> [1:00:31]: All right, so the next question, when creating a wrapper around, say, GCP pops, what separates from lower-level implementation versus higher-level implementation? What typical feature should be included?</p> <p><strong>Miłosz</strong> [1:00:58]: Higher level implementation. Do I get it right that it&rsquo;s about higher level GCP pubsub features? So basically, the Watermill wrapper around GCP pubsub is this&hellip; You implement this low-level interface only. So you implement the Publisher and Subscriber, and then all other components of Watermill work with those two interfaces. So you get the rest working out of the box. I&rsquo;m not sure if this is the question, but the typical features are basically publish and subscribe. That&rsquo;s the short story, but there are often very a PubSub-specific configuration you need to include. Okay, probably one very often feature that appears in most is consumer groups. Some kind. Or in the case of GCP PubSub, this is subscriptions. So you need a way to configure those. I&rsquo;m not sure if this is the answer. So if you have a Yeah, if you have a follow-up question, you can let us know if this is what you meant.</p> <p><strong>Robert</strong> [1:02:24]: Yes, Adrian mentioned, true, at the end of the day, which HTTP package we use, it may not be that important. Yeah, it&rsquo;s also worth mentioning that, you know, in this benchmark wars of HTTP routers or whatever, often the routers that are winning those wars are not fully compatible with HTTP protocol. It&rsquo;s also worth mentioning. It&rsquo;s cheating a bit, but for specific use cases, they are faster in it. But again, we are mentioning it very often that you can optimize allocations, struct, order, but just making one more SQL query will just kill all your optimizations. Again, there are cases when you&rsquo;re not doing SQL queries, you&rsquo;re just doing some CPU intensive operations.</p> <p><strong>Robert</strong> [1:03:20]: In this case, it totally makes sense to optimize. And if you&rsquo;re a big library, just think about value kpi in this case and you can optimize later. And I think it&rsquo;s also useful to if the library is when performance is important, just share benchmarks with people because, if they care about performance, it may be good. It may be just using the Go benchmark that is built-in. For these cases, it may work. For example, in Watermill we have special benchmark, because we are not measuring it in go-run time. We are more interested in how many messages we can publish or subscribe in one second.</p> <p><strong>Miłosz</strong> [1:04:01]: Yeah, I agree that the HTTP package is probably one of the least interesting things about the project I want to decide. I mean, I want a nice API and probably middleware support. But other than that, I want it to be like Watermill, so I can replace it with anything else if I want.</p> <p><strong>Robert</strong> [1:04:24]: If you feel that maybe you need to optimize your HTTP communication, maybe use gRPC, for example, that will be faster if you really need. Again, optimization when you&rsquo;re using JSON for communication is a bit&hellip; Yeah.</p> <p><strong>Miłosz</strong> [1:04:42]: Yeah, I wouldn&rsquo;t spend too much time deciding on the library, basically. Just pick something you know or you like, and that&rsquo;s it.</p> <p><strong>Robert</strong> [1:04:52]: Right, so it seems that this is the last question so far. So let&rsquo;s wait for a couple more seconds for questions.</p> <p><strong>Miłosz</strong> [1:05:06]: So probably a good time to remind you about the subscribe button or the five star writing button on</p> <p><strong>Robert</strong> [1:05:15]: Podcast applications or even better joining our newsletter so you will be sure that will not miss an episode notification so you can have notification before life one week before or summary and you know when you&rsquo;re clicking subscribe it&rsquo;s also cool but you are never sure that your notification will be there with our newsletter you are always sure plus you also know what all other updates that are doing uh what else so yeah five star reviews uh share with your friends so it would be also cool when this knowledge can go wider and if.</p> <p><strong>Miłosz</strong> [1:05:55]: You have any open source libraries share with us in the comments so we can take a look</p> <p><strong>Robert</strong> [1:06:01]: Or if you&rsquo;re using watermill production also please let us know in comments it would be cool to know if it&rsquo;s useful for you because again we don&rsquo;t know basically we just know from some people that we&rsquo;ve seen on conferences but, we don&rsquo;t have any telemetry or whatever so we don&rsquo;t know if you are using watermel.</p> <p><strong>Miłosz</strong> [1:06:22]: Right i think no more questions thank you everyone for joining us today</p> <p><strong>Robert</strong> [1:06:27]: Yeah thank you Miłosz for today. So see you in two weeks. In two weeks we are discussing what&rsquo;s the difference between sync and async architecture. When we recommend to use that, we&rsquo;ll also go deeper into the topic what&rsquo;s the difference between PubSub and Message Queue.</p> <p><strong>Miłosz</strong> [1:06:46]: So wehave a very related topic in two weeks.</p> <p><strong>Robert</strong> [1:06:50]: Thank you for being with us and see you in two weeks.</p> <p><strong>Miłosz</strong> [1:06:52]: Bye-bye.</p>Unpopular opinions about Gohttps://threedots.tech/episode/unpopular-opinions-about-go/Wed, 30 Apr 2025 16:00:00 +0000https://threedots.tech/episode/unpopular-opinions-about-go/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Simplicity isn&rsquo;t enough for complex applications</strong> - while Go&rsquo;s syntax is simple, complex applications still need proper design patterns; primitive code easily becomes spaghetti code in large projects.</li> <li><strong>Reading the standard library isn&rsquo;t the best way to learn Go</strong> - it&rsquo;s optimized for different goals than typical applications and might be confusing for beginners.</li> <li><strong>Router libraries are better than the standard HTTP package</strong> - libraries like Chi or Echo come with a nice high-level API.</li> <li><strong>Struct-based configuration is better than the &ldquo;optional pattern&rdquo;</strong> - structs are easier to document, discover, and maintain than the popular With-options approach.</li> <li><strong>There&rsquo;s no one best project structure</strong> - starting small and evolving your structure as needed is better than following a dogmatic approach like the unofficial &ldquo;Go project layout.&rdquo;</li> <li><strong>Writing stubs by hand is better than using mocking libraries</strong> - manually written stubs are easier to debug and encourage better interfaces than reflection-based mocking libraries.</li> <li><strong>Code generation is better than reflect</strong> - for ORMs or dependency injection, it gives you compile-time checks and better performance.</li> <li><strong>Generics are mostly useful for libraries, not application code</strong> - while everyone waited for them, they&rsquo;re rarely needed in typical service-level code.</li> <li><strong>Channels and goroutines can be overused</strong> - they add complexity and should only be used when concurrency is actually needed, not as a default approach.</li> <li><strong>Go&rsquo;s error handling is fine for most projects</strong> - explicit checks make code easier to read, though built-in stack traces would be helpful.</li> <li><strong>Memory optimizations are often premature</strong> - micro-optimizations waste time for typical API services where network latency is the bottleneck.</li> </ul> <h2 id="introduction">Introduction</h2> <p>In this episode of No Silver Bullet, we share some of our unpopular takes on the Go programming language. After working with Go for eight years on all kinds of projects, we&rsquo;ve seen many discussions about what idiomatic Go means. We talk about what worked for us, but we keep in mind that different projects have different needs. We question some common Go beliefs and share tips we&rsquo;ve picked up along the way.</p> <h2 id="notes">Notes</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">Wild Workouts</a> - Our example Go project that shows a more complex application structure</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill" target="_blank">Watermill</a> - Our Event-driven application library for Go</li> <li>HTTP Routers we recommend: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-chi%2Fchi" target="_blank">Chi</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Flabstack%2Fecho" target="_blank">Echo</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F" target="_blank">Go in One Evening</a> - Our hands-on Training for learning Go quickly</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fepisode%2Fis-clean-architecture-overengineering%2F">Clean Architecture episode</a> - Previous episode that goes deeper into project organization</li> <li><a href="proxy.php?url=https%3A%2F%2Fgo.dev%2Fblog%2Fsurvey2024-h2-results" target="_blank">Go Developer Survey Results</a> - Shows that ~75% of Go developers build API/RPC services</li> <li><a href="proxy.php?url=https%3A%2F%2Fgoogle.github.io%2Fstyleguide%2Fgo%2Findex" target="_blank">Google&rsquo;s Go Style Guide</a> - Many useful ideas, but be careful about being too dogmatic about it</li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>Go is simple, but sometimes it will end up as a disaster if you write too simple code for complex applications. You can write simple code, as long as we are building a simple application. But for something more complex, it starts to be more tough.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>The common question in the community is &lsquo;how to learn Go the best?&rsquo; One common suggestion is to read the standard library because that&rsquo;s where the idiomatic Go code lives. But the standard library has completely different goals than a web application or API server or CLI application.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>The biggest risk about having strong opinions is starting to be dogmatic and hard to be convinced by somebody else. We&rsquo;ve seen people that were very strongly opinionated, and when you try to convince those people, it was super hard, even if your arguments were pretty logical.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>This podcast is called No Silver Bullet because we believe there are no easy answers. To grow as an engineer, you need to keep an open mind. You can&rsquo;t get stuck on a single fixed point of view because things get updated very fast What works in one context may not work in another.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>When you create your application code, you definitely should not start with something generic. When you start with something generic, it will be over-abstracted and much more complicated than it needs to be. After a year, you&rsquo;ll find out it didn&rsquo;t need to be generic because you just have one use case.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>Universal patterns exist for some reason. Probably someone wanted to solve some issue before. Design patterns are especially problematic - many people have this allergic reaction when you create a strategy pattern in Go and some people might react, &ldquo;Oh no, this is not Java, please.&rdquo;</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>Is structuring your project important? Ask a question: Will it help? Maybe it will not help. Maybe it doesn&rsquo;t matter.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>If the network roundtrip takes 100 milliseconds, why would optimizing the nanoseconds matter? Most of the time it&rsquo;s a waste of time.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <p> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D0s">00:00:00 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D320s">00:05:20 - Simplicity is not enough</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D655s">00:10:55 - Don&#39;t read the stdlib to learn Go</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D938s">00:15:38 - net/http vs other libraries</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D1124s">00:18:44 - Avoid the options pattern for config</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D1308s">00:21:48 - What&#39;s the best project structure?</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D2078s">00:34:38 - Avoid mocking libraries</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D2333s">00:38:53 - Code generation is better than reflect</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D2672s">00:44:32 - Generics are overrated</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D2883s">00:48:03 - Channels and goroutines can be overused</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D3190s">00:53:10 - Error handling is fine</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D3538s">00:58:58 - Memory optimizations are often premature</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D3834s">01:03:54 - BDD style testing libraries aren&#39;t helpful</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D4155s">01:09:15 - testify is useful for asserts</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D4378s">01:12:58 - Interface naming conventions</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0jjZjlAvG_o%26amp%3Bt%3D4526s">01:15:26 - Q&amp;A</a></li> </p> <h2 id="transcript">Transcript</h2> <p><strong>Miłosz</strong> [0:00]: Do you know what is the best project structure? Should everyone read the standard library? And will error handling in Go ever improve?</p> <p><strong>Miłosz</strong> [0:07]: Go may be a simple programming language, but there&rsquo;s a never-ending debate about what idiomatic Go means. Today we discuss our takes on some of the most controversial topics. I am Miłosz.</p> <p><strong>Robert</strong> [0:19]: And I&rsquo;m Robert. And this is No Silver Bullet Live podcast, where we discuss mindful backend engineering. We spent almost 20 years working together across different projects and teams. We have seen Go evolve from its early days to what it is now. We learned that following advice like always do X or never do Y doesn&rsquo;t work and it can limit your growth. In this show, we share multiple perspectives that will help you to make smart choices and grow into principal engineer level.</p> <p><strong>Miłosz</strong> [0:48]: And depending on your background and experiences, some of the opinions we share might seem as popular or unpopular. and this is fine. We share what worked for us before. So keep in mind, it&rsquo;s our opinion, but let us know in the comments what you think, what&rsquo;s your take on the topics and share your questions for the end when we will have Q&amp;A and if you have any controversial opinions of your own, that&rsquo;s great. We can discuss them together.</p> <p><strong>Robert</strong> [1:24]: Yeah, totally. would love to hear your unpopular opinions and include it to this episode.</p> <p><strong>Robert</strong> [1:31]: And yeah, probably it&rsquo;s worth starting why this episode is about Go. So if you don&rsquo;t know us for a longer time, Go is our language of the choice, let&rsquo;s say. So I think it&rsquo;s the first language where we stayed for longer and we feel that it&rsquo;s, we feel productive with that and we&rsquo;re quite happy about this language. But as every language, our technology it&rsquo;s not perfect and there are some things that are not that</p> <p><strong>Miłosz</strong> [1:58]: Perfect it&rsquo;s probably quite common story I&rsquo;ve heard it a few times before but someone was moving from one language to the other and they stumbled on Go at some point and just stayed and maybe the reason is it&rsquo;s a nice, get stuff done language so you know you can focus on what&rsquo;s important and not about and the language specifics or features and so on.</p> <p><strong>Robert</strong> [2:26]: Yeah, but it&rsquo;s definitely the best one from the worst. If we assume that every language has some downsides. But I think also probably many things that we&rsquo;ll cover today should apply to other programming languages. So if you&rsquo;re not writing in Go, don&rsquo;t leave us, because you may also learn something that maybe can apply to your other programming languages.</p> <p><strong>Miłosz</strong> [2:51]: And we&rsquo;ve been working with Go for about eight years now on production professionally. So everything we talk about, we base on this experience. And have we seen it all? Probably not. But we&rsquo;ve seen different kinds of projects and teams and companies. So basically, the rule is simple. If we say that something works for us, It means we&rsquo;ve seen it work in a few different contexts. But it doesn&rsquo;t mean it is the same in every other context. This podcast is called No Silver Bullet because we believe there are no easy answers like this. Once again, show your perspective. Maybe there&rsquo;s something we consider as a good practice and you&rsquo;ve seen it doesn&rsquo;t work. So that would be great to hear. Especially to grow as an engineer, I think you need to keep an open mind. You can&rsquo;t get stuck on a single fixed point of view because things get updated very fast. So what works in one context in one project may not work in another.</p> <p><strong>Robert</strong> [4:13]: Yes, I think it&rsquo;s important to be open-minded and not be dogmatic because I think the biggest risk about having strong opinions is starting to be dogmatic and hard to be convinced from somebody else because often we&rsquo;ve seen people that were very, very strongly opinionated about something and when you try to convince those people it was super hard, even if your arguments were pretty logical.</p> <p><strong>Miłosz</strong> [4:40]: And on the other hand, there are people who start writing in Go and they worry that they are not doing it in the idiomatic way, which can be also a trap because it&rsquo;s hard to tell what&rsquo;s idiomatic without the full context. So it may be also helpful to hear different perspectives today.</p> <p><strong>Robert</strong> [5:02]: Yeah, and if you would like to, we already covered this topic pretty deeply in, I think it was first episode about low quality code. So please check this one. So we discussed more about this, what&rsquo;s worth doing</p> <p><strong>Robert</strong> [5:15]: and when it&rsquo;s worth doing shortcuts. Okay, I think we can start with the first unpopular opinion about Go, and it&rsquo;s that simplicity is not enough. So one of the selling points of Go is that it&rsquo;s a simple language, so some people are going pretty far with that and saying that, okay, Go is simple, you should write simple code.</p> <p><strong>Robert</strong> [5:38]: I kind of agree that I think we should write as simple code as possible, but no simpler, because usually it will end up as a disaster. So some examples that we&rsquo;ve often seen was trying to build some complex applications with some complex domain logic, like a financial application, like a crowd. And okay, maybe it will work for a while, but after adding more and more logic, it will suffer a lot. And it&rsquo;s not that language specific, I think, because, okay, we&rsquo;re now talking in context of Go. Again, a lot of people are saying that it&rsquo;s simple language, but I think it&rsquo;s something that is happening in every language. So Go have pretty simple syntax, that&rsquo;s correct, but if you are writing complex logic, it doesn&rsquo;t matter if you are doing it in Go, in PHP, or Java. But if you approach it in a too simple way, as a crud, it will just start to be overcomplicated.</p> <p><strong>Miłosz</strong> [6:37]: Maybe the reason Go is more prone to this is people come to it from other languages, maybe some very complex projects where someone went too far with patterns and it was all overcomplicated. So when you start to write this simple syntax, it feels great, like a fresh air. So that&rsquo;s pretty cool. But you can also go too far and try to do it. Trivialize it, write primitive code, let&rsquo;s say.</p> <p><strong>Robert</strong> [7:10]: Yes, like none of design patterns are useful, just write simple code and it will be simple and nice. And that&rsquo;s true, as long as we are building simple application. Because for simple application it will totally work. But for something more complex, it&rsquo;s starting to be more tough.</p> <p><strong>Miłosz</strong> [7:28]: Design patterns are especially problematic. I think many people have this allergic reaction when you create, I don&rsquo;t know, a strategy pattern in Go and some people might react. Oh no, no, this is not Java, please.</p> <p><strong>Robert</strong> [7:47]: Don&rsquo;t make Java in Go.</p> <p><strong>Miłosz</strong> [7:51]: You probably don&rsquo;t want to do it if it&rsquo;s too early, but if you have an else-if structure that goes to 20 cases, maybe you could refactor it into a pattern that&rsquo;s easier to read and maintain and so on.</p> <p><strong>Robert</strong> [8:06]: Yeah, totally. I would say, again, it&rsquo;s important here to not be dogmatic. It&rsquo;s important to be mindful, let&rsquo;s say, know those patterns, not say like those patterns are stupid and obsolete. It&rsquo;s not the case. I mean try to go with simple code, but know what to do if you have some more complicated cases to tackle.</p> <p><strong>Miłosz</strong> [8:27]: The universal patterns exist for some reason. Probably someone wanted to solve some issue before.</p> <p><strong>Robert</strong> [8:34]: And they were doing it like 50 years ago.</p> <p><strong>Miłosz</strong> [8:37]: In a language that&rsquo;s not so far from on-go looks like right now exactly it&rsquo;s still the same paradigm i think if you compare java a few years ago we&rsquo;ll go it probably it&rsquo;s not that far with syntax so yeah it&rsquo;s a similar case, Maybe besides patterns, what comes to my mind is constructors. So I know some people also don&rsquo;t like creating them in Go because, yeah, you can create just a type with proper zero value. And it often works well, sure. But sometimes you want validation. In web applications, very often you want the most entities.</p> <p><strong>Robert</strong> [9:22]: Yeah, and if you&rsquo;re building, again, simple application like your team, if it&rsquo;s three people, five people, okay, it&rsquo;s totally fine. You can live without constructors. You can watch out to not break things. But if your team is 10 people, 20 people, 30 people, it&rsquo;s starting to be critical because anybody can actually touch your code. And you need to have some way to ensure that this person will not break this code.</p> <p><strong>Miłosz</strong> [9:49]: And this validation will be in some place. The question is, do you want it in one place behind a constructor or do you want it scattered somewhere in your handler?</p> <p><strong>Robert</strong> [9:58]: And this is a good example. I mean, simple code to&hellip; People that love simple code may say that no, no, no constructors. And I know that it may sound crazy, but unfortunately it&rsquo;s what sometimes we&rsquo;ve seen.</p> <p><strong>Miłosz</strong> [10:15]: So simplicity won&rsquo;t save you without the proper design. It&rsquo;s possible to create spaghetti code in Go as well.</p> <p><strong>Robert</strong> [10:26]: Exactly. I think it&rsquo;s an important point that Go is not different in this case. You can create spaghetti code in every language. Maybe in some languages it&rsquo;s a bit easier because you can write obscure code. In Go it&rsquo;s a bit harder to do. But again, you can still have code with very big cyclomatic complexity like City, and it will be also unreadable.</p> <p><strong>Miłosz</strong> [10:54]: Okay my next take is about ways to learn go and it is a common question in the community how to learn go the best and there are many many options but one of the common suggestions i&rsquo;ve seen is, you should read the standard library because this is where the idiomatic go code lives, And I guess it might be because in the early days, there were not many open source libraries. It was hard to see some real life examples of Go. But I think for learning, it is probably not the best way to learn it. Because the standard library has completely different goals than a web application or API server or CLI application. or whatever you create.</p> <p><strong>Robert</strong> [11:49]: Yeah, probably you can compare it to advice like if you would like to learn driving a car, you should go to Car Factory and learn how to drive a car.</p> <p><strong>Miłosz</strong> [12:00]: Yeah, I would. That&rsquo;s a good analogy. Unless you want to create a standard library for your own language, that is a great choice.</p> <p><strong>Robert</strong> [12:08]: Or your own car.</p> <p><strong>Miłosz</strong> [12:10]: And there are many great ideas in standard library, of course. So it makes sense to check how something is done. But I think if you are just starting out, it might be confusing because some of the code is not that readable. It&rsquo;s optimized to be fast, to be generic, to cover all possible use cases. It has to be forward compatible because Go will never break the API, I promise. And, yeah, it&rsquo;s probably not what you want to learn first. And one example might be that, speaking about constructors, I think the standard library quite rarely uses them, which might be fine because the zero value has some same defaults. For example, the default HTTP client works like this. You don&rsquo;t need an instructor because the zero value is fine, which is a cool design. But sometimes you need to validate the inputs, basically, and it&rsquo;s something you want in your application, probably. So I will be careful with this.</p> <p><strong>Robert</strong> [13:31]: I think the thing that you mentioned a bit earlier, the promise of backward compatibility, it&rsquo;s making it a bit harder. So, if you&rsquo;re making constructor, it&rsquo;s harder to add another parameter because it will be a breaking change. So, we know it from the library that we are building Watermill, and sometimes it&rsquo;s actually pretty tough to avoid some breaking changes. And yeah, if you&rsquo;re doing a standard constructor, it&rsquo;s just hard to add one parameter. But if you&rsquo;re building web services or any kind of web applications, you basically can You can do breaking changes in your internal code because compiler will say that it&rsquo;s a breaking change, it doesn&rsquo;t compile, you can update all the code, but it&rsquo;s not a standard library.</p> <p><strong>Miłosz</strong> [14:20]: So what to do instead? There are tons of resources and books, two of Go, documentation, videos and so on. We are a bit biased because we like learning hands-on, which we mentioned in the previous episode. So if you want to learn or check it.</p> <p><strong>Miłosz</strong> [14:38]: We also have the going one evening training where you can use hands-on approach to learn by writing code.</p> <p><strong>Robert</strong> [14:47]: And we know that a lot of people don&rsquo;t believe that Go, you can learn Go in an evening, no way. But yeah, you can do that.</p> <p><strong>Miłosz</strong> [14:54]: Yeah, but if you like to just read instead, there are tons of good resources. And there are many, many open source projects right now that you can take a look at how they work.</p> <p><strong>Robert</strong> [15:04]: We actually have R1s also, Wild Workouts. It&rsquo;s a pretty popular Y, I think, on GitHub. I don&rsquo;t know how many stars it has now, like 5,000, 6,000, something like that. Also worth checking because you can learn from a project that looks like a project that you will do in your job probably as long as you&rsquo;re not building a language or libraries and you can just see how to do it properly and not learn from your mistakes but from our</p> <p><strong>Miłosz</strong> [15:36]: Staying with standard library there is this opinion that the standard HTTP package which NetHttp is quite powerful and good to go compared to other languages, which I agree with. But I would also add that it is not that great to be used by default in most projects when you start. Because the API is not that great to work with compared to router libraries, and it doesn&rsquo;t give you much to stick to the defaults, unless you care about dependencies size or something like this. So what we like the most is either GoChi or Echo, which gives you a very lightweight router out of the box. And Qi is compatible with the standard library. Echo is not, but gives you more sane error handling.</p> <p><strong>Robert</strong> [16:41]: It&rsquo;s actually pretty compatible. I mean, it&rsquo;s not using totally different types. You still have access to the standard library types. Yes, the HTTP handlers are not fully compatible, but you can have some magic adapters, let&rsquo;s say.</p> <p><strong>Miłosz</strong> [16:54]: Yeah, so in the very beginning, we stick to 3 because we wanted to be compatible with the standard library for some reason. I&rsquo;m not sure why. Probably because it felt idiomatic to do it. And then we switched to echo because the error handling was promising.</p> <p><strong>Robert</strong> [17:11]: In other words, I think it&rsquo;s pretty cool that you can return error from HTTP handlers. Because from a practical point of view, it&rsquo;s a very common thing that you&rsquo;re returning error from your HTTP handler. And it&rsquo;s just easy to mess it out if you not return from HTTP handler. So if you have checking if the error happened, but you will forget to return. It happened probably to any person and it&rsquo;s not perfect.</p> <p><strong>Miłosz</strong> [17:37]: The error handling gets verbose otherwise. And I think once we try to do it ourselves, like create our own wrapper on top of G to have this error handling. But Echo does it for you, so that&rsquo;s pretty cool. And maybe Barton to mention that those are just routing libraries, not full frameworks as some other. And we prefer it this way because it&rsquo;s easier to replace or doesn&rsquo;t interfere with the rest of your code. Just HTTP handlers.</p> <p><strong>Robert</strong> [18:09]: So we covered it a bit in the episode about frameworks. But TLDR is that you can pretty easily replace that. So if you decide that, okay, I don&rsquo;t like Qi or I don&rsquo;t like Echo. You can switch to any of this and it will take you probably less than a day for a pretty big project. So that&rsquo;s cool. And I will be not afraid to be vendor locked in.</p> <p><strong>Miłosz</strong> [18:32]: Unless you couple it with your application. Hopefully not.</p> <p><strong>Robert</strong> [18:36]: Yeah, but you can also do it with the standard library. You spend some time.</p> <p><strong>Robert</strong> [18:42]: Alright, so next unpopular opinion from me is using the optional pattern for configuring things. So you know those with XYZ configuration instead of using struct. And there are a couple of reasons why we don&rsquo;t like this pattern. So the first reason is it&rsquo;s hard to find what options you can pass. So you need to go to file and&hellip;</p> <p><strong>Miłosz</strong> [19:07]: Sometimes it has the same package as the constructor. Sometimes it&rsquo;s a completely different one called options so you need to look it up sometimes.</p> <p><strong>Robert</strong> [19:15]: You have two packages for example in cmp library for comparing stuff you have two packages where options are stored</p> <p><strong>Miłosz</strong> [19:24]: Many libraries also use options package for keeping those options which is cool but we have a big project with many dependencies and in your ide when you type options it gives you 10 different libraries.</p> <p><strong>Robert</strong> [19:40]: Yes, and I actually don&rsquo;t have a good idea why this pattern is so popular.</p> <p><strong>Miłosz</strong> [19:45]: Yeah, I&rsquo;m the same. I think I&rsquo;m missing something. Why is it that much used?</p> <p><strong>Robert</strong> [19:52]: But long story short, what we are doing in, for example, Watermill, we are just using structs because it&rsquo;s super simple. So you are just navigating to the struct. You see all the options that you can pass. You can add a method for set defaults, for example, so you can see what are the defaults. That&rsquo;s it. It&rsquo;s deadly simple, just works great. I&rsquo;m not sure why it&rsquo;s not the standard. I hope it will be, but&hellip;</p> <p><strong>Miłosz</strong> [20:16]: It&rsquo;s easy to find the documentation. You just jump to the structure. You have all the fields there. What I also like to do is an IDE. You can have this command, fill all. So you just create empty struct, fill all and delete what you don&rsquo;t need. Exactly. It&rsquo;s very easy to configure then. and zero values can have the same defaults anyway. So it&rsquo;s not that different from using the with options. It&rsquo;s also easier to conditionally change some configuration. For example, if you have some input parameter, depending on which you want to use different options or not. If you use the options, you need to create a slice of options first, and then append to it. And with config, you just set the value and that&rsquo;s it. And so if you&rsquo;re just some fun of using this pattern in the chat, please let us know. What are we missing? Because maybe there&rsquo;s some use cases that we didn&rsquo;t see yet.</p> <p><strong>Robert</strong> [21:19]: I think we can just recommend using struct because again in Watermill we are using it with zero problems, zero backward compatibility issues. I think standard library is also not using this optional configuration pattern.</p> <p><strong>Miłosz</strong> [21:34]: A good example where simpler is actually the simpler option. I mean, when it&rsquo;s useful to use a simpler one, just a struct.</p> <p><strong>Robert</strong> [21:44]: Yep, definitely. All right, so the next unpopular opinion is about structuring your project. And I think&hellip;</p> <p><strong>Miłosz</strong> [21:52]: Oh, most common discussion.</p> <p><strong>Robert</strong> [21:55]: Yeah, it&rsquo;s a controversial thing. So I think&hellip; It&rsquo;s the thing that structuring your project is the thing that a lot of people are often very dogmatic and blindly following some patterns that they don&rsquo;t have idea why they are doing that. So I have one good example, the Golang standards project layout that a lot of people are following.</p> <p><strong>Miłosz</strong> [22:22]: Renamed.</p> <p><strong>Robert</strong> [22:24]: Yeah, yeah. And it&rsquo;s quite funny because a lot of people are following that thinking that it&rsquo;s official guideline, but it&rsquo;s not. If you go to issues, there is one closed issue that Golang by trainers are saying that it&rsquo;s not official. Maybe you should rename it or something because it&rsquo;s misleading so this is one thing that i often found the other thing that is sometimes controversial is about using some more complex structure like clean architecture or hexagonal architecture because many people are saying no no it&rsquo;s making java from go we touched it very deeply in the previous episode, I think, about architecture.</p> <p><strong>Miłosz</strong> [23:10]: Two episodes ago, yeah.</p> <p><strong>Robert</strong> [23:12]: OK. If you are interested, you can just search for it.</p> <p><strong>Miłosz</strong> [23:17]: And this is also a common question. I think maybe because of this empty page syndrome, when you start a project, you have nothing in your editor. So you feel like you need some foundation in place to start. And many people try to find the best way to start the project, to organize it. And you don&rsquo;t really need to do it yet before you know how the project will look like in the future. So we recommend starting small and not sticking to any particular project structure if you don&rsquo;t need it yet. Because it&rsquo;s easy to go too far and overcomplicate it before it&rsquo;s already needed. There are some nice, maybe not patterns, but some tips people use, like the cmd package for keeping all the binaries. I think that one is pretty cool, because it&rsquo;s very easy to find the entry points to your application. Unless you create a library, maybe. I would probably use it in most projects.</p> <p><strong>Robert</strong> [24:28]: Yeah, it&rsquo;s also good to remember that you can use internal package, So the package that you can only import from the same or deeper level of your application. So if you are building a library or some services that are shared in the codebase, for example, you have modular or monolith, it&rsquo;s pretty useful to use this package.</p> <p><strong>Miłosz</strong> [24:49]: Some people call it PKG or SRC. I don&rsquo;t really care that much, but I like to have this separate directory in the project, Because very often you have other stuff like Docker or Dev or Tools or API definitions. Basically some other files that are no Go files in your project.</p> <p><strong>Robert</strong> [25:16]: Yeah, and it&rsquo;s starting to be pretty messy if you&rsquo;re just keeping each of the directory.</p> <p><strong>Miłosz</strong> [25:20]: Yeah, especially for someone who is just joining the project, it might be very hard for them to understand where is the Go code, There is some helpers, some not ready to go files. So whatever you call this internal package, I think is a pretty cool idea. And it helps to find your way around the project. Also depending on the project type it might be a bit different right for applications for cli apps for libraries you probably will have a bit different structure, so it doesn&rsquo;t make sense to stick to one everywhere you don&rsquo;t need to look for one perfect solution and.</p> <p><strong>Robert</strong> [26:06]: I think it&rsquo;s probably also not that important that as people are</p> <p><strong>Miłosz</strong> [26:10]: Exactly going.</p> <p><strong>Robert</strong> [26:12]: Into very deep discussions i mean it&rsquo;s just structure it just directories you can change it pretty quickly that&rsquo;s it</p> <p><strong>Miłosz</strong> [26:23]: But there is one package structure i dislike and it is the flood structure and, maybe it is also because of how the standard library looks like So I&rsquo;ve seen many projects use one level of packages, and it&rsquo;s supposed to be simple. But I think this is exactly this issue where trying to be simple tends to be something that&rsquo;s hard to grasp. And I recall projects that have the same level, have packages like app, user models, database, handler, all kinds of names. And all those packages import each other in different configurations. So it&rsquo;s quite hard to understand what&rsquo;s going on. And again, especially for a newcomer, you just land on this project and you have to basically read it end-to-end to understand what&rsquo;s going on. So that&rsquo;s one structure I would not recommend, probably.</p> <p><strong>Robert</strong> [27:41]: And yeah, I remember that there are some people thinking that, okay, if you&rsquo;re, I mean, that project structure can work for really simple services, and it&rsquo;s fine. If you have really, really simple service, and it for some reason needs to be a separate service, it&rsquo;s fine. But I think it can also lead to some kind of pathology when if you see that, okay, my service is too complicated to be in one package, I need to split another microservice. And it&rsquo;s a trap because you start to have really granular microservices and will start to have more microservices than people in the team.</p> <p><strong>Miłosz</strong> [28:20]: Jim in the chat mentions that maybe we should read Robert Martin&rsquo;s packaging principles. So I think this is clean architecture, basically what we covered in the previous episode. And yeah, this is what we usually suggest for more complex applications. And in short, it&rsquo;s just grouping the packages in a few layers. So it&rsquo;s easier to understand where&rsquo;s your logic code, where&rsquo;s your database implementation, and where&rsquo;s your HTTP handler. And even if you look at standard library, there are some grouping packages like encoding for JSON and other encoders and HTTP in net. So it&rsquo;s a similar idea.</p> <p><strong>Robert</strong> [29:04]: And I know that people are sometimes thinking that, okay, it&rsquo;s actually not like that in standard library. In standard library, you have package per one thing. But yeah, I agree. that it&rsquo;s totally compatible and it can be idiomatic in Go. It&rsquo;s just about not going too far.</p> <p><strong>Miłosz</strong> [29:27]: Another package that&rsquo;s often called unnecessary or forbidden is the common package. Which is an interesting idea because it&rsquo;s hard to imagine a project where there isn&rsquo;t common code. So I guess the standard recommendation here is to just move your code from common to where it belongs. Sometimes you can but sometimes it doesn&rsquo;t make much sense and I think it&rsquo;s pretty fine to keep this common package for stuff that don&rsquo;t belong anywhere and I don&rsquo;t remember it being a problem except for where you keep your entities and logic there that is pretty much it&rsquo;s.</p> <p><strong>Robert</strong> [30:15]: Just you know this hard discussion what domain logic is and if it should be there or not. But I think what&rsquo;s pretty useful here is thinking in terms like if you are building your common package, is it something you could publish and other people can use in totally different projects?</p> <p><strong>Miłosz</strong> [30:38]: Yeah, that&rsquo;s a good idea.</p> <p><strong>Robert</strong> [30:39]: Yeah, this filter can be pretty useful for filtering out things that are your business logic of your application and are not. And sometimes also some people are recommending that instead of creating some shared code, you can copy it. And okay, sometimes it works, but it doesn&rsquo;t work for things that should change together. Because there is sometimes some logic that should always work the same, and it will be not a problem when it will be replicated in multiple places. But sometimes you have common logic that you need to be sure that it&rsquo;s working the same way across entire codebase. And if you forget to update it in other places, you may have a problem.</p> <p><strong>Miłosz</strong> [31:26]: Yeah, so maybe the issue here is not having the common package in itself, but deciding what goes there. If you just put too much in it and too many domain-rated stuff, then it becomes a problem. Similarly, you will create a common library that all services use. So I prefer to use the smaller libraries. And similarly in a package, you don&rsquo;t need to put everything inside the common package directly. You can use sub packages. But I think this grouping is quite handy.</p> <p><strong>Robert</strong> [31:59]: And there is a related question. So if you have considered packaging by feature, not a technology layer. So I would say that it&rsquo;s a different layer, it&rsquo;s a different level of packaging. So usually in a more complex project, we have, let&rsquo;s say, two levels of modularization. So one is by, let&rsquo;s say, technology or layer that you have like application, interfaces, adapters, etc. And above that, you have like users, orders, okay, it&rsquo;s probably a very bad example.</p> <p><strong>Miłosz</strong> [32:32]: More like modules, maybe?</p> <p><strong>Robert</strong> [32:34]: Yeah, yeah.</p> <p><strong>Miłosz</strong> [32:34]: I&rsquo;m not sure if this is not about vertical slice architecture where you slice the project by each feature. Which I tried to play with it i didn&rsquo;t come up with anything that i liked yet so I can&rsquo;t really tell if it&rsquo;s a great idea maybe in the future but.</p> <p><strong>Robert</strong> [32:57]: Uh you know also separating by um so i said that you know users orders etc it&rsquo;s basically example because it&rsquo;s actually in practice it often doesn&rsquo;t work.</p> <p><strong>Miłosz</strong> [33:09]: It sounds too easy, too naive.</p> <p><strong>Robert</strong> [33:12]: Unfortunately, but I think the proper modularization is really close to separating by feature. You should separate your project by independent things that you can modify to add some functionality. So for example, you are modifying some functionality, you should not need to modify 10 modules for example for that. You should be able to do it in one module and it means that your modularization is proper. So I think that it can actually be close to that. Unfortunately, often it&rsquo;s not the case. But from the other side, you don&rsquo;t need to also have very granular modularization for simpler projects because it can just overcomplicate stuff. And I think I&rsquo;ve&hellip; Okay, it&rsquo;s hard to say because I&rsquo;ve seen multiple projects where they&rsquo;re too granular or they were&hellip; Well, they could be more granular because it was just building with the time with more features in one module. So, unfortunately, it&rsquo;s sometimes hard to get balance here. Well, yeah, I think it&rsquo;s something around that. So even if you have modules, probably it should be pretty close to features.</p> <p><strong>Miłosz</strong> [34:19]: Yeah, probably. I will try vertical slides at some point.</p> <p><strong>Robert</strong> [34:24]: Easier to say than do. Unfortunately.</p> <p><strong>Robert</strong> [34:29]: Alright, so time to next Antipolar Opinion, if you don&rsquo;t have anything to add to the previous one.</p> <p><strong>Miłosz</strong> [34:35]: Let&rsquo;s go.</p> <p><strong>Robert</strong> [34:36]: So, if you are with us for a longer time, you may know that we are not big fans of mocking libraries in Go. And there are multiple reasons for that. The first reason is that most of them, or I would say all of them that we tried are just hard to use because of a lot of empty interfaces, a lot of reflection. So because of that it&rsquo;s pretty hard to work with that because you can basically pass anything to the assert or to the implementation and it&rsquo;s really hard to debug that. And our solution for that is quite simple and I know that it&rsquo;s a bit controversial, but our solution for that is actually writing stops by hand. And some people are saying that, but okay, it will be super hard, sometimes interfaces are super big, it will take ages to write those stops. I think I would think about that in another direction. Like if you have big interfaces, it&rsquo;s not a problem of your stops. It&rsquo;s a problem of your big interfaces.</p> <p><strong>Miłosz</strong> [35:39]: Small interfaces make it much easier.</p> <p><strong>Robert</strong> [35:41]: Yeah, yeah. And we already in some episodes were covering that it&rsquo;s a similar problem with DI. So if your DI is complicated, it&rsquo;s not a problem of your DI. It&rsquo;s a problem of your dependency graph, basically. So the similar situation is with mocks. and okay sometimes in some projects where you are adding some tests it&rsquo;s just hard to write those stuff by hand okay it it may be fine to write the use some library that can generate it for you it&rsquo;s quite fine but i would recommend to try to write them by hand and i know that for some people it&rsquo;s not obvious but in this case you can just keep this stub implementation next to the real implementation so thanks to that you don&rsquo;t need to copy this implementation over multiple places so it doesn&rsquo;t require much more effort to use that than using real mocks and at the end it&rsquo;s giving you much more flexibility and it&rsquo;s much easier to debug and that&rsquo;s nice and</p> <p><strong>Miłosz</strong> [36:47]: I think it also encourages you to test your your code and not not the mocks What I don&rsquo;t like about mocking is that many libraries give you those methods that you can test what method has been called and with what arguments and so on. And I think it kind of misses the point of testing because you don&rsquo;t really care what some method, what some repository or client has been called with. You care about the end result. So I like to design those fakes like in-memory repository. That works kind of similar to the original one. It&rsquo;s much simpler. And what&rsquo;s nice about it is you can replace the implementation and then run the tests on a very high level and everything should work as a production. And you don&rsquo;t need to care about what has been called, what hasn&rsquo;t been called. If the end result is correct, then it&rsquo;s fine.</p> <p><strong>Robert</strong> [37:47]: Yeah, it&rsquo;s the worst if you are changing some logic and you are running your unit tests and everything explodes because you called one method that were not called earlier and you need to fix 100 tests just because you called something extra. It&rsquo;s a disaster. And it&rsquo;s even worse if you&rsquo;re using your mocking library in the wrong way and it&rsquo;s not thread safe. So if you&rsquo;re running some tests in parallel, it explodes and you have no idea which test is failing. So yeah, if you wrote your stubs, well, the problem doesn&rsquo;t exist. And if you are looking for some inspiration how to do that, so on our blog you can check article named the Go libraries that never failed us, 22 libraries you need to know. I know that it doesn&rsquo;t sound like it covers how you should write stubs, but it&rsquo;s actually There&rsquo;s one example there that you don&rsquo;t need HF library for mocking.</p> <p><strong>Robert</strong> [38:51]: Okay, another thing that I think it&rsquo;s sometimes a bit controversial, especially for some people that are newcomers to the language. So this is the fact that in Go code generation is pretty popular, but I think it works pretty well. If you will take example of ORMs or dependency injection libraries. So it&rsquo;s similar case like with mocks. So if you are not using code generations in many things like your ORM, you end up with a lot of empty interfaces. You will have no compile type check format.</p> <p><strong>Miłosz</strong> [39:32]: You have no strong types in a language that has static types, so you lose some of the benefits of using Go in the first place.</p> <p><strong>Robert</strong> [39:39]: Exactly. And for ORMs, it has also one important thing that you probably care about performance pretty much. And if your ORM is pretty heavy on reflection, it will just slow. You cannot overcome that. And if it&rsquo;s generated, it&rsquo;s just static and it&rsquo;s fast. So it&rsquo;s quite cool.</p> <p><strong>Miłosz</strong> [39:58]: Also easier to understand. If you really like to know how it works under the hood, you can open the generated file and very often it&rsquo;s quite readable.</p> <p><strong>Robert</strong> [40:08]: Yeah. And a good example is dependency ejection. So for example, we quite like wire. So in our projects, we don&rsquo;t use a library for dependency injection. We are just doing it by hand. But if you really need to use any of it, we recommend wire because it&rsquo;s code generation based and you can just go to the generated file and see how everything is injected. So debugging of that is pretty simple. With reflection, good luck. No way.</p> <p><strong>Miłosz</strong> [40:34]: Yeah, it&rsquo;s quite hard to understand how Reflect works, especially in a library you don&rsquo;t know. So it&rsquo;s quite hard to debug. And one other thing, libraries using reflect tend to overuse i&rsquo;ve seen is struct tags so struct tags are pretty cool idea for marshalling it doesn&rsquo;t get in your way and you know it&rsquo;s easy to specify, but for other things i think it&rsquo;s questionable because it gives you this this magic, behavior that&rsquo;s not compile time checked one example is validation libraries i&rsquo;ve seen some quite popular ones that let you specify so many things there, like how long you expect a string to be. Basically, if you look at the syntax they let you specify in a strike tag, it&rsquo;s like a completely different language.</p> <p><strong>Robert</strong> [41:34]: I think when the fun starts, when you are using this kind of library and you are just not checking how long the string should be, It&rsquo;s a fun start begins when you are checking something if some field is set.</p> <p><strong>Miłosz</strong> [41:48]: Yes, they also let you do it. So you need to write tests for it, which you probably want to do anyway. But I think if you use Go for its static types and compilation and all this good stuff, It kind of misses the point to overuse Reflect like this. And ORMs are similar with how they let you specify field types and relations in struct tags and so on. Feels like a cool idea because you have this one model that describes everything, but it can be overused. One of the things I like Go for is that it&rsquo;s easy to read. You can take a look at the code and you understand what&rsquo;s going on. There is no magic behind the scenes like in dynamic languages, where a function call can trigger something you don&rsquo;t understand because there&rsquo;s some decorator used somewhere or something like that.</p> <p><strong>Robert</strong> [42:57]: Yeah i have ptsd when you are saying about it</p> <p><strong>Miłosz</strong> [43:00]: Yeah i remember such stuff from python um or.</p> <p><strong>Robert</strong> [43:05]: Php you remember magic methods</p> <p><strong>Miłosz</strong> [43:07]: Yeah so yeah so if you use go i would stick to the, classic code as much as you can and avoid to reflect entirely unless you really want to do it. I remember one project when we tried to create our custom tags and be smart and be smart and avoid writing code by hand and yeah it wasn&rsquo;t a good idea in the end, because just digging through the reflection code is not fun at all.</p> <p><strong>Robert</strong> [43:43]: Yeah so alternative if just write those ifs. It&rsquo;s not that hard. Maybe it looks super explicit, but it&rsquo;s not bad.</p> <p><strong>Miłosz</strong> [43:53]: Or generate your code, which is also quite easy to do with Go.</p> <p><strong>Robert</strong> [43:57]: But probably not for validation. I think for validation, I think it&rsquo;s just a matter of a couple ifs that, again, it will look boring, but don&rsquo;t be afraid of boring code. Boring code is not something bad it&rsquo;s something good actually</p> <p><strong>Miłosz</strong> [44:13]: Yeah and with you know with good ideas and with things like copilot becomes much easier to to write boilerplate so it&rsquo;s one less argument it shouldn&rsquo;t take you much time to do it anyway definitely all.</p> <p><strong>Robert</strong> [44:31]: Right another thing that i hope it will be controversial because you may remember that one of the most anticipated features on Go were generics.</p> <p><strong>Robert</strong> [44:41]: And you&rsquo;re here. We have generics for, I don&rsquo;t remember how many years.</p> <p><strong>Miłosz</strong> [44:47]: Two years, probably.</p> <p><strong>Robert</strong> [44:48]: Yeah, and well, how often you are using generics in your service code? I don&rsquo;t remember when I last did that.</p> <p><strong>Miłosz</strong> [44:59]: Yeah, not much. For some for some more internal libraries. I&rsquo;ve used them. We use them in Watermill for CQRS handlers. We also use them in event sourcing library. That was cool. But I think for service level code, Probably not that much.</p> <p><strong>Robert</strong> [45:23]: And I think the secret may be in the name. Generics. So like the name states, it&rsquo;s for some things that are generic. So for example, libraries that we mentioned. But usually when you are creating your application code, you definitely should not start with something generic. Because often when you start with something generic, it will be over abstracted and it will be just much more complicated at the end than it needs to be. And probably after a year you will find out that it didn&rsquo;t need to be generic because you just have one use case that works in exactly one way.</p> <p><strong>Robert</strong> [45:58]: And even if you have two cases just copying this part of code and not having it generic it will be much much easier and it will make your code much nicer. But for libraries definitely it&rsquo;s useful but I wouldn&rsquo;t say that it&rsquo;s that big game changer compared to how much people wanted to have generics in Go. And yeah it&rsquo;s cool that we have it because the secure component in WaterMule, it&rsquo;s cool because it&rsquo;s making the syntax much nicer. I love it. But I would say that I don&rsquo;t see that many more cases when it helps. And I&rsquo;ve seen a lot of cases when it just makes code unreadable. Really, if you know some generic quirks, you can do such ugly code that nobody will have an idea how it works and how to make it compiling. It&rsquo;s sometimes like when I play with TypeScript. I mean, I have no idea how to make it compiled, so I need to use compiled or whatever. But I&rsquo;ve seen such kind of code with GoGenerics sometimes, so watch out for that.</p> <p><strong>Miłosz</strong> [47:03]: So it&rsquo;s a similar rabbit hole as Reflect. It can be useful, but it can be overdone. So you can find yourself trying for a few days to make it work instead of just creating something straightforward, but for stuff where you want, It&rsquo;s something universal, that might be a good idea. I remember one thing on the service level we had was decorators for application level commands. So we had stuff like metrics, logging and tracing decorator. And it actually worked pretty cool with generics.</p> <p><strong>Robert</strong> [47:40]: Yeah, but it&rsquo;s also something that we&rsquo;re able to do with code generation. True that it was a bit tough because if you are doing your own code generation, Unfortunately, it&rsquo;s a bit hard to do, because there is some tooling, but it&rsquo;s not that great, so&hellip;</p> <p><strong>Miłosz</strong> [47:58]: Yeah. One more feature that&rsquo;s not always needed and can be overused are Tunnels and GoRoutines, which are one more great part of Go that everyone loves, I guess.</p> <p><strong>Miłosz</strong> [48:15]: And for running concurrent code, yeah, that&rsquo;s a great idea. But sometimes I&rsquo;ve seen projects where they were used for some unknown reason. Like instead of calling a function, you would have some worker in the background that would communicate with channels with some other function. And it may sound like a cool design, but remember that it also has lots of complexity, especially for stuff like synchronizing routines, closing channels, accessing some common memory and so on. So it can be also painful. And also the concurrent code is not always faster than the segmental code.</p> <p><strong>Robert</strong> [49:05]: Yeah remember that running a routine it also does some allocations</p> <p><strong>Miłosz</strong> [49:11]: And if your code is CPU-bound then you are limited by the cores anyway so it might not be the best idea.</p> <p><strong>Robert</strong> [49:18]: But i think it&rsquo;s similar case like with generics so when you are building libraries channels may be pretty cool to simplify your life and build pretty complex stuff. But in typical application code, maybe it&rsquo;s not that often useful. Sometimes it is, but often you can use, for example, error groups to do some synchronization. It also works nicely.</p> <p><strong>Miłosz</strong> [49:44]: And it has more high-level API. It&rsquo;s harder to misuse. And with channels, it&rsquo;s easy to have some unbuffered channel that makes you stuck at some point because no one is reading from it and so on. So you need more tests, you need to be more careful with design overall.</p> <p><strong>Robert</strong> [50:07]: That&rsquo;s true, but it can simplify live with libraries. like an example of WaterMule. So I think it&rsquo;s a nice example because we&rsquo;ve been able to use a lot of patterns from Go that make implementation much easier. But it&rsquo;s a library. I think we should do an episode about WaterMule.</p> <p><strong>Miłosz</strong> [50:24]: Let&rsquo;s make it the next one.</p> <p><strong>Robert</strong> [50:26]: Sounds like a good idea. And one more thing about channels is that if you are using it in your application, you need to also remember that they are not persistent. So, for example, if you are triggering some workflow from your HTTP handler to channels and your service may die, for example, now you&rsquo;re using Kubernetes, you never know. The service may die in the middle of processing your request. Your operation will be just lost. So remember that channels are nice, as long as losing this operation because it&rsquo;s in memory will be not a problem. And if it will be a problem, remember that something like Message Broker exists, like PubSub, like Redis,</p> <p><strong>Miłosz</strong> [51:12]: RabbitMQ, Kafka. Or cloud-based PubSub. I often see people ask about Watermill and the GoChannel PubSub we have. So we have this PubSub implemented in a water mill where it communicates through Go channels, so we don&rsquo;t need a separate infrastructure. And it seems like a good idea because you can just have it running with no other infra. But it&rsquo;s also quite limited. It&rsquo;s not the same as most PubSub are. We use it mostly for testing purposes or stuff like that. It might be tricky if you try to use it for some production-grade system, because there are some&hellip; Persistence is probably important, or another thing that comes to my mind is buffering. So if you use unbuffered channels or the buffer overflows, then your handler will hang. There&rsquo;s many complexities you have to think about.</p> <p><strong>Robert</strong> [52:17]: Yeah, and if you are not building that big application, like you are building a smaller application and you have persistent file system, you can also consider PubSub based on SQLite, because we will very soon have it in Watermill. Thanks, Dima, for implementing that. And I think it will be a pretty cool alternative for channels, because you can persist your PubSub with SQLite, that will be super fast, and you can just store it in your file system. And it&rsquo;s a nice alternative. If you are going into a more complex system, just use some production grade.</p> <p><strong>Miłosz</strong> [52:58]: Or use SQL-based PubSub, if you already have the database, you probably have.</p> <p><strong>Robert</strong> [53:02]: If you&rsquo;re in the middle, let&rsquo;s say, of your pop-up journey.</p> <p><strong>Miłosz</strong> [53:08]: Okay, my next one is will be either unpopular or super popular, depending on who you are. So my take is that error handling is completely fine. In go. The checks are boring to write, but they are great for reading the code, because you know what&rsquo;s happening. And you can just use a compiler ID shortcuts to generate them. But one thing we miss is stack traces by default. That would be cool.</p> <p><strong>Robert</strong> [53:43]: That&rsquo;s true. Because you always have this code that has no raps, and you&rsquo;re later spending minutes, hopefully not hours, to try to find from where this error went to where it went. It&rsquo;s tricky part. for stackraces, well, it could be helpful here.</p> <p><strong>Miłosz</strong> [54:02]: Yeah, I wonder if the next proposal will make it to the spec, because I&rsquo;ve seen there&rsquo;s another one for using some syntactic sugar. So, let&rsquo;s see.</p> <p><strong>Robert</strong> [54:16]: Okay, in general, for example, I&rsquo;m super happy about the error handling in Go because it&rsquo;s explicit. It&rsquo;s not like with exception, you&rsquo;re quite not sure what can be drawn from the bottom of the stack. And in Go, you&rsquo;re sure that, okay, this returns error. Should I do something with that? Shouldn&rsquo;t I do something with that? And it&rsquo;s super strict, and I think it&rsquo;s making implementations much more robust.</p> <p><strong>Miłosz</strong> [54:43]: Yeah people complain about handling the errors but the other side is if you have function that returns no errors that&rsquo;s actually super cool because you know it is you know pure function, and it doesn&rsquo;t have side effects probably.</p> <p><strong>Robert</strong> [54:59]: Or for example have some function that you&rsquo;d know that you don&rsquo;t care that it returns error so for example sometimes you have cases when you are converting string to boolean but for example you don&rsquo;t care if it&rsquo;s drawing error if it&rsquo;s just false. Okay, it&rsquo;s false. Whatever, I&rsquo;m just ignoring this error. But you&rsquo;re doing it explicitly. With exceptions, it will be a big chance that some exception will be thrown. It will go up to the stack to your handler and it will throw to person 500 just because you were not able to parse bullying.</p> <p><strong>Miłosz</strong> [55:28]: Like a panic. What do you think about panics?</p> <p><strong>Robert</strong> [55:31]: I love panics. So I know that a lot of people are like, don&rsquo;t panic. Yes, yes, I know. But I think it&rsquo;s not that easy. So there are many cases when you shouldn&rsquo;t panic. You definitely shouldn&rsquo;t panic in goroutine because i&rsquo;m not sure if all of you know but if you&rsquo;ll panic in your goroutine and you will not recover it your entire process will exit with non-zero code not the best thing in http servers not gracefully</p> <p><strong>Miłosz</strong> [55:57]: Shut down but just exit.</p> <p><strong>Robert</strong> [55:59]: Yeah yeah so it&rsquo;s if you are using channels for your asynchronous operations it will be lost so it&rsquo;s not the best thing. In general, if you&rsquo;re writing libraries, for this reason, probably you should watch out when you&rsquo;re panicking because you can just accidentally do this favor of killing service for somebody. And it&rsquo;s also pretty hard to debug. So not bad. But there are some cases when I like to use panic. So obviously in some dirty code, it&rsquo;s nice. So for example, in my IDE, I have shortcuts to do if error is not nil, panic. It&rsquo;s pretty useful. But there&rsquo;s one case when I really like panics and it&rsquo;s a production code and it&rsquo;s related to dependency injection. So if you&rsquo;re injecting things and when running some constructor in your main, return some error. And I think it&rsquo;s totally fine to panic in this case because it&rsquo;s a non-recoverable situation. So if you&rsquo;re not able to inject dependency, your service will not work. So it doesn&rsquo;t matter what you will do at the end. And panic is nice because it will give you the stack trace and it&rsquo;s making it a bit shorter. And I think in general, this is the situation when panics are good. So when you have some non-recoverable situation, basically.</p> <p><strong>Miłosz</strong> [57:19]: Yeah, sometimes also for a better API. We have a function that&rsquo;s very often called and handling the error there would be annoying. And there&rsquo;s just one error you need to check inside it, but it&rsquo;s basically a sanity check. It never happens, but just for completeness, you need to do this entity check, then I think it&rsquo;s pretty fine to&hellip;</p> <p><strong>Robert</strong> [57:48]: I no longer make errors like it should not happen because it always happens.</p> <p><strong>Miłosz</strong> [57:54]: What comes to mind from our project is about getting some config value inside the function.</p> <p><strong>Robert</strong> [58:03]: Yeah, and without this config, it will totally not work.</p> <p><strong>Miłosz</strong> [58:06]: So in theory, the config library should handle this for you, but you never know. So why not just throw a panic in this case instead of making your API harder to use in this case?</p> <p><strong>Robert</strong> [58:20]: Yeah, exactly.</p> <p><strong>Miłosz</strong> [58:22]: But it&rsquo;s an exception, not a rule.</p> <p><strong>Robert</strong> [58:26]: Always having exceptions in Go. Yeah, but again, I think what&rsquo;s important here, again, not be dogmatic, because I think a lot of people are dogmatic that don&rsquo;t panic, never ever panics are evil. But I think it&rsquo;s not like that. There are some cases when it&rsquo;s fine but just be mindful when you are doing it.</p> <p><strong>Miłosz</strong> [58:48]: And remember about recovers on many levels.</p> <p><strong>Robert</strong> [58:52]: Yeah, definitely.</p> <p><strong>Miłosz</strong> [58:56]: Cool. My next one is about memory optimizations or optimizations in general. So So, I fancy discussions like this, like knowing what makes more sense, what&rsquo;s a better trick. One example is struct field ordering. So if you order fields in your struct in a certain way, then they fit better into memory and it takes less memory allocation.</p> <p><strong>Robert</strong> [59:26]: And you feel so smart. I just ordered my struct field to use a bit less of memory.</p> <p><strong>Miłosz</strong> [59:33]: Yeah, and there are many such tiny improvements you can make, like pre-allocating slices instead of appending in a loop, let&rsquo;s say, if you copy a slice. Or using a buffer builder instead of appending to a string. like our, yeah, my other ideas. No. Oh, yeah, I know. The string with map that maps to an empty struct instead of a boolean.</p> <p><strong>Robert</strong> [1:00:10]: I actually remember one more. So concatenation of strings with this function. I don&rsquo;t remember the name. But to not concatenate strings with plus, but instead using this&hellip;</p> <p><strong>Miłosz</strong> [1:00:23]: The builder. Yeah. And, you know, GoTest has this benchmark feature that lets you write two functions and compare them and you can easily see, like, wow, this is five times faster. But, you know, if you take a look at the Go survey, I think 75% of people write API services in Go or web servers or something similar.</p> <p><strong>Robert</strong> [1:00:50]: And what&rsquo;s the bottleneck in this kind of servers?</p> <p><strong>Miłosz</strong> [1:00:53]: Yeah. So, you know, if the network roundtrip takes 100 milliseconds or 50 or something like that.</p> <p><strong>Robert</strong> [1:01:01]: Let&rsquo;s take one millisecond even.</p> <p><strong>Miłosz</strong> [1:01:04]: Yeah. So why would optimizing the nanoseconds matter in this case? So most of the time it&rsquo;s a waste of time. And of course, a huge disclaimer depends on what your application does. If you write some, I don&rsquo;t know.</p> <p><strong>Robert</strong> [1:01:20]: CPU intensive or memory intensive application.</p> <p><strong>Miłosz</strong> [1:01:23]: It&rsquo;s tough for microcontrollers or whatever, of course, it matters in many cases. But I&rsquo;m talking about when you create an HTTP handler and then you spend time discussing this in a PR, whether this should be pre-allocated, sliced or not. And it&rsquo;s probably a waste of time.</p> <p><strong>Robert</strong> [1:01:41]: Or maybe you are also writing a service that is handling really, really huge traffic and your servers&rsquo; costs are important, it may be also the case. Because sometimes it&rsquo;s the case. But again, from our experience, it&rsquo;s a smaller percent.</p> <p><strong>Miłosz</strong> [1:02:02]: But the difference is you optimize once you feel the pain. Instead of trying to predict that one year later we will have such huge scale and that we will need this optimization.</p> <p><strong>Robert</strong> [1:02:12]: And I think it&rsquo;s useful to actually use this benchmark in opposite ways. So if somebody is suggesting you like, oh, no, you&rsquo;re wasting such much of memory or CPU. You can use benchmark and show that it&rsquo;s just that small difference and compared to, for example, SQL query, it&rsquo;s like nothing.</p> <p><strong>Miłosz</strong> [1:02:34]: Yeah, maybe just add an index in your table and it will be much, much faster.</p> <p><strong>Robert</strong> [1:02:39]: Undo one less SQL query. Or maybe if you&rsquo;re calling some external service, maybe do it in Go routine if you can. and just do things in parallel. It will give you probably a couple orders of magnitude more and then optimizing some allocations.</p> <p><strong>Miłosz</strong> [1:03:01]: A similar concern is about garbage collection often. And I&rsquo;ve seen it most often in regard to games. Like this opinion that you can&rsquo;t make games in Go because of garbage collection. But chances are you&rsquo;re not creating a game or even matter. And it turns out it&rsquo;s actually pretty fine. There are many games created in Go that are all fine. And most of the time you don&rsquo;t need to care about it that much. Unless, of course, you build something that&rsquo;s very specialized. But I&rsquo;m talking about a very common scenario of creating API or HTTP servers.</p> <p><strong>Robert</strong> [1:03:39]: So again, be mindful. Just not apply some dogmas blindly.</p> <p><strong>Miłosz</strong> [1:03:48]: Yep.</p> <p><strong>Robert</strong> [1:03:52]: So maybe one more thing about testing again. So, the BDD style testing libraries are not helpful, or I would say even more, they are harmful, for multiple reasons. So the first reason, I think all of them are not compatible with standard libraries. So they are kind of using some testing from standard libraries, but in general they are not compatible. So you need to learn some new tools, how it works, and your assets will be not compatible. The new people in the team will need to learn something new, and it will be just less readable.</p> <p><strong>Miłosz</strong> [1:04:31]: They have nice outputs, though. That&rsquo;s a selling point most of the time, I think.</p> <p><strong>Robert</strong> [1:04:37]: That&rsquo;s true. But I think one of the biggest arguments for using this kind of frameworks is that it&rsquo;s making a nice structure of your tests and I agree. I think it&rsquo;s nice that you have this given, when, then, and it&rsquo;s nicely separated. But from the other side, you can achieve the exactly same concept in standard library. Because nobody is saying to you that you cannot split every of your tests to three parts. With given, when you are doing some prerequisites, in when you&rsquo;re doing some operation and at the end you&rsquo;re ensuring that everything, what should happen. And I think it&rsquo;s, I&rsquo;m trying to do that because it&rsquo;s making tests much more readable. But again, I don&rsquo;t need to have special library for that. I can just do it with the standard library testing. So I think the most important thing is mindset. And it may be actually a good idea to maybe try to use this kind of libraries to try to understand this mindset. But I would avoid to use those kind of libraries in your services.</p> <p><strong>Miłosz</strong> [1:05:46]: The big promise is using Garukin syntax to collaborate with your product owner or maybe some stakeholders or whoever. Yeah. We tried it before.</p> <p><strong>Robert</strong> [1:05:59]: Yeah.</p> <p><strong>Miłosz</strong> [1:05:59]: But we never made it to work really. So the idea is that you can create this framework for someone non-technical, I guess, to create the test cases for you in this given window scenario. And then you can just run those tests.</p> <p><strong>Miłosz</strong> [1:06:19]: But I have never seen it work out in practice. Usually it&rsquo;s developers who write those scenarios anyway.</p> <p><strong>Robert</strong> [1:06:27]: Yeah, and you&rsquo;re just wasting a lot of time on doing the mapping because you need to have parsers for this text, you need to do a lot of mapping. And at the end, when developers are adding those I would say now three times more time because you need to support that many things. Again, you can have very similar concepts in your standard Go library code tests. And again, maybe somebody like your product owner will be not able to edit that, or probably your product owner will anyway not write this. And if you follow those principles, you can even sit with your product person and if you did a good job with writing nice helpers, it can be even readable with your help to this person. So I think this actually worked for us a couple of times, so you can see with this.</p> <p><strong>Miłosz</strong> [1:07:20]: If it reads like in English, you can probably just show it to someone who&rsquo;s not technical and they will be able to read it and suggest what&rsquo;s missing.</p> <p><strong>Robert</strong> [1:07:29]: So again, I recommend to play with that, to maybe understand those principles and see how it can look like and give you some inspiration. But from our experience, it never fulfilled both promises that it gave.</p> <p><strong>Miłosz</strong> [1:07:45]: It was usually easy to start. And I think most examples in Garek can show you this very promising idea. You have this table with quite simple use case. We have something like, when I do this and do this, then something happens. But once you try to do something more complex with it, you need to add more cases and it starts to be kind of bloated. Hard to understand.</p> <p><strong>Robert</strong> [1:08:15]: Yeah, because Gherkin works nicely for some really high level tests, like end-to-end tests. But often it&rsquo;s not enough to have some HTML selectors in Gherkin because you need to match the element that you need to click to do something. And it doesn&rsquo;t make sense at the end. So you just</p> <p><strong>Miłosz</strong> [1:08:31]: Keep switching between the scenario and the test code to make it work together instead of just doing it in your tests.</p> <p><strong>Robert</strong> [1:08:40]: And parsing is harder and harder when you need to use some special characters.</p> <p><strong>Miłosz</strong> [1:08:46]: Yeah, well, maybe it&rsquo;s worth to take a look and try to at least to inspire yourself to write tests in this given one then approach. That&rsquo;s actually a pretty good idea.</p> <p><strong>Robert</strong> [1:08:57]: Yeah, and I think I would be happy to see it much more often because it&rsquo;s actually not as often used as it could be.</p> <p><strong>Miłosz</strong> [1:09:09]: Okay, maybe the same with testing libraries. I see a few people quoting Google&rsquo;s style guide which mentions that you shouldn&rsquo;t use assertion libraries. But we actually like Testify for asserts and it&rsquo;s pretty easy to use, doesn&rsquo;t get in your way. Most it does what you want.</p> <p><strong>Robert</strong> [1:09:34]: And I think we can probably also say that it&rsquo;s industry standard. In most companies it&rsquo;s just used, in many libraries it&rsquo;s also used.</p> <p><strong>Miłosz</strong> [1:09:46]: Yeah, and that&rsquo;s the thing about following those style guides or official guidelines. I think they are helpful, there are lots of useful ideas there, but it&rsquo;s also easy to take them too seriously and dogmatic. And you need to remember you are not Google. And the best example probably is how Go started with GoPath, which worked well for Google because they used the monorepo and so on but for everyone else probably almost it was painful and weird to work with Dependency management?</p> <p><strong>Robert</strong> [1:10:20]: Why you need that? This is everything on our monorepo</p> <p><strong>Miłosz</strong> [1:10:24]: It was this vendor, yeah I&rsquo;ve never seen why using Testify would be a problem it gives you nice outputs mostly out of the box you can add some extra context if it&rsquo;s missing.</p> <p><strong>Robert</strong> [1:10:43]: I think of an argument is that it&rsquo;s implicit, you need to learn how it works under the hood and I agree but from other side you will do it once But for something like equals,</p> <p><strong>Miłosz</strong> [1:10:55]: We don&rsquo;t really need to understand how it works.</p> <p><strong>Robert</strong> [1:10:57]: It&rsquo;s just equals, basically. And you have to remember that sometimes you need to use equal values, for example, if you&rsquo;re not asserting exact types, for example.</p> <p><strong>Miłosz</strong> [1:11:06]: Yes, there are some caveats, but maybe it&rsquo;s one library where actually Reflect is fine. But it&rsquo;s a testing library, so that&rsquo;s a bit different.</p> <p><strong>Robert</strong> [1:11:16]: It&rsquo;s a library.</p> <p><strong>Miłosz</strong> [1:11:18]: Yeah, you don&rsquo;t need to care about compile time checks in your tests because it&rsquo;s just testing your code.</p> <p><strong>Robert</strong> [1:11:24]: That&rsquo;s true, that&rsquo;s true. So again, I think it&rsquo;s good to have exceptions from&hellip;</p> <p><strong>Miłosz</strong> [1:11:31]: Guidelines, yeah, and maybe make it work for your project, your team. And not be dogmatic. Yeah, agree on something together, which is also a nice idea to have some internal guidelines instead of just committing 100% to something external that maybe is generic and works fine in many contexts, but for your specific use case, maybe not.</p> <p><strong>Robert</strong> [1:11:57]: And I think that there is one thing in Testify that I don&rsquo;t like. So those are testutes. I&rsquo;m not sure if it&rsquo;s still valid, but last time when I tried to use that, testutes have one big bad limitation. so they were not able to run tests in parallel. So basically, your tests needed to run sequentially. And from my perspective, it&rsquo;s a deal breaker because many of the tests just need to be able to run in parallel when there&rsquo;s any I.O. or any slow operation. And without that, your tests will be just extremely slow.</p> <p><strong>Miłosz</strong> [1:12:32]: Right, so we had to use the test main pattern.</p> <p><strong>Robert</strong> [1:12:36]: Exactly.</p> <p><strong>Miłosz</strong> [1:12:37]: But some years passed, so maybe we should take a look at it again.</p> <p><strong>Robert</strong> [1:12:40]: Yeah, and again, sometimes it&rsquo;s just a matter of running some helper function before or at the beginning of your test, and it&rsquo;s fine, and using subtests from the standard library. And I think our last unpopular opinion, so it&rsquo;s about naming. So you may read in, I think, Effective Go probably, that you should name your interfaces with suffix like error, like reader, writer, follower, user finder.</p> <p><strong>Miłosz</strong> [1:13:21]: It makes sense most of the time, but sometimes not. yeah.</p> <p><strong>Robert</strong> [1:13:25]: So for example if you have interface that have four methods it can lead to something strange</p> <p><strong>Miłosz</strong> [1:13:33]: And i&rsquo;ve seen people overuse it sometimes like you have a method called user by id and then the interface is user by idr come on can you just call it user finder or.</p> <p><strong>Robert</strong> [1:13:46]: Repository i mean it&rsquo;s just user repository.</p> <p><strong>Miłosz</strong> [1:13:50]: That&rsquo;s probably a very minor thing, but I&rsquo;ve seen it overused sometimes.</p> <p><strong>Robert</strong> [1:13:56]: That&rsquo;s true. I think it&rsquo;s again about this being mindful when you are writing software, and if you see that you have an interface named user by IDFIR, it&rsquo;s something wrong there because it&rsquo;s just hard to read it.</p> <p><strong>Miłosz</strong> [1:14:12]: Yeah, exactly. It won&rsquo;t tell you much about what it does. If you just stick to this pattern everywhere. Maybe just come up with something that describes it better, even if it doesn&rsquo;t follow this idea. And my final one about naming is, I don&rsquo;t care if you say Go or Golang, which for some reason is controversial. I don&rsquo;t know. It&rsquo;s like the common discussion. Someone says, how to do X in Golang? Someone comes in and says, first of all, it&rsquo;s not Golang, it&rsquo;s Go. I don&rsquo;t know why. I don&rsquo;t care. Everyone understands what it is.</p> <p><strong>Robert</strong> [1:14:59]: That&rsquo;s true. But I think it doesn&rsquo;t help that language name is not that unique around everything.</p> <p><strong>Miłosz</strong> [1:15:07]: Hey, it&rsquo;s not the worst name. It could be called JavaScript. Does it make sense? No. Naming is hard. Everyone knows it. Just live with it.</p> <p><strong>Robert</strong> [1:15:24]: All right, cool. So let&rsquo;s see what questions we have on the chat. All right, so Jim mentioned that obscure is not spaghetti, and spaghetti is not cyclomatic complexity. And yeah, that&rsquo;s right. I think maybe we mentioned that it&rsquo;s close to be equal, but no, it&rsquo;s not, I would say that it&rsquo;s maybe some shortcut, but it&rsquo;s often connected in some magical way that when you have obscure code and cyclomatic complexity is big, there is a big chance that it may spaghetti code but yeah it&rsquo;s</p> <p><strong>Miłosz</strong> [1:15:59]: Yeah i guess those are separate things obscure will be hard to understand spaghetti would be many side effects that you don&rsquo;t see something similar but maybe jim you can tell us more what do you mean what do you mean by this uh but yeah there are many aspects for sure and this comes to this discussion about simplicity right so it&rsquo;s it&rsquo;s hard to, to decide what&rsquo;s simple because of it, because there are many aspects.</p> <p><strong>Robert</strong> [1:16:32]: Another comment from Jim is that dry is tension with loose coupling. Any removal of the application implicitly creates coupling that didn&rsquo;t exist and may be a source of future problems. And it&rsquo;s true, but sometimes things are coherent. So basically if things are coherent you would like to have them coupled because it&rsquo;s kind of expected. So if you have one logic that is expected to work everywhere in some way in every place. Well, unfortunately, it&rsquo;s coupling, but you cannot close your eyes and say, oh no, there is no coupling there and just copy it everywhere because we&rsquo;ll have a lot of problems with keeping it up to date everywhere. Sometimes maybe it shouldn&rsquo;t be some library code. Maybe it should be in a service. It&rsquo;s hard to say because there are many examples, but I would say that I agree with that, but there&rsquo;s sometimes also one more complexity Sometimes things are just coupled because they need to be coupled and it&rsquo;s making your life easier. So I would say that coupling is not always bad. Coupling is sometimes coupling and it exists and we cannot ignore that.</p> <p><strong>Miłosz</strong> [1:17:43]: Yeah, but if you overuse dry, you can end up with too much coupling. I think that might be the point here. And yeah, duplication is the best way to lose the coupling as a tool. We have a blog post about this as well, on our blog. And yeah, we&rsquo;ve also seen that overusing dry is usually a bad idea, unless you know what you&rsquo;re doing.</p> <p><strong>Robert</strong> [1:18:10]: Yeah. And I think it&rsquo;s also one tricky part here that we&rsquo;ve seen a couple of times, that it&rsquo;s not silver bullet that you can always get common logic in library and just use it everywhere, or you can move it to some services and service can handle that. So in other words, sometimes it&rsquo;s better to have a service that is keeping this common logic and it&rsquo;s good, and sometimes it&rsquo;s better to keep it in the library that is shared across. Because sometimes we&rsquo;ve seen migrations in 2A. So for example, there was some library, some common code, it was moved to the service, just to figure out that actually having a service handling that is the worst idea, it&rsquo;s better to have a library that keeps this logic common somewhere. But again, it unfortunately depends. Another comment from Jim. So yes, the focus on testing should be almost always on results, not with internal functions or code.</p> <p><strong>Miłosz</strong> [1:19:19]: Yeah, yeah, that&rsquo;s why be careful with mocks and at least we prefer stabs or fakes instead of mocks for this. And black box testing.</p> <p><strong>Robert</strong> [1:19:34]: Right. Strange combination of opinions. Love panics, but hate exceptions. Love explicit error returns, but one stuck traces.</p> <p><strong>Miłosz</strong> [1:19:43]: I wouldn&rsquo;t say we love panics. They have some uses. We don&rsquo;t use them for normal errors.</p> <p><strong>Robert</strong> [1:19:52]: I hope it didn&rsquo;t sound like that.</p> <p><strong>Miłosz</strong> [1:19:54]: That&rsquo;s maybe the difference. Exceptions are just used everywhere in some languages instead of errors. And panics are just for the very exceptional for the exceptional cases not for errors yeah.</p> <p><strong>Robert</strong> [1:20:11]: And fact races so I would say it&rsquo;s a matter of pragmatism that well, error wrapping is cool but what you&rsquo;ll do if somebody forgot to do that and it&rsquo;s also not perfect that you need to do that again, co-pilot helps with that sometimes those messages are super stupid but the reality is that But spending hours on figuring out the wrap strings, it doesn&rsquo;t make sense.</p> <p><strong>Miłosz</strong> [1:20:38]: Maybe one of the issues where it shows is if you use a library that doesn&rsquo;t wrap errors and you receive some obscure error from it, and then you have to look at the sources and find what it is and understand what&rsquo;s going on. And what stack trace could help you. And it doesn&rsquo;t need to be stack trace of all the levels, just of the most inner level, where the error originated. So you see the previous function calls.</p> <p><strong>Robert</strong> [1:21:14]: Yeah, and I think I talked to somebody from Go team about the problem of stack traces with errors, and I think one of the problems was performance.</p> <p><strong>Miłosz</strong> [1:21:27]: And that&rsquo;s again the issue of standard library it needs to be universal yeah yeah probably in some projects it wouldn&rsquo;t make sense to allocate memory for it so yeah that&rsquo;s but.</p> <p><strong>Robert</strong> [1:21:40]: Yeah maybe someday because from other side we have the runtime tracing that it has it and it&rsquo;s super fast. The overhead is like 5%, 1%, I don&rsquo;t remember. Maybe someday. Please, please, please. Mate of Clive saying that I feel the least thing dev use Go is building web services. I always find jobs for DevOps engineers.</p> <p><strong>Miłosz</strong> [1:22:17]: So I mentioned the results of the Go developer survey in the official one. And if you take a look there, the top one is API servers, about 75%, and some other high percent for servers returning HTML and such. So maybe this means everyone writes web services, but they have no jobs.</p> <p><strong>Robert</strong> [1:22:45]: Yeah, but probably it depends.</p> <p><strong>Miłosz</strong> [1:22:47]: Yeah, I think this is an accurate observation that there are not that many jobs where you can see Go used for web apps.</p> <p><strong>Robert</strong> [1:22:56]: For other words, how many Kubernetes operators you can write? Come on so I think it may depend a lot on the market but yeah our observation is kind of consistent with the survey that we mentioned that I think most of the jobs were around services but again I think it may be also depending a lot on your bubble so if you are more into DevOps bubble you may see more jobs postings actually</p> <p><strong>Miłosz</strong> [1:23:26]: It made me think because it&rsquo;s true that But I don&rsquo;t see many jobs about using Go for web apps in general. So what do all those people write that reply in the survey about web services? What do they do? Maybe just some open source apps? I don&rsquo;t know. But at least, yeah, maybe we are biased, but we mostly write to web apps in Go and API services. We don&rsquo;t do any DevOps stuff.</p> <p><strong>Robert</strong> [1:24:05]: And we didn&rsquo;t struggle to find any web services, probably it&rsquo;s better words. Again, but it depends on the bubble a lot. Probably it depends a lot on your local market. If you&rsquo;re looking in the local market remotely, remote is different. Okay, and the last question so far. So if anybody has another question, we&rsquo;re waiting for a while. And the last question so far is saying I joined kind of late to this podcast and I&rsquo;m not sure if you mentioned about naming convention in Go using name variables with one letter or three as maximum I mean code should be readable I think it&rsquo;s actually a pretty good one we&rsquo;ll cover this one what do you think?</p> <p><strong>Miłosz</strong> [1:24:50]: Yeah, code should be readable I agree, it&rsquo;s hard to give some rule here here. Of course, short variables are nice if it&rsquo;s used in a small loop or something like that. Sometimes it&rsquo;s easier to read one word than four words together, but I wouldn&rsquo;t have any strong role here like three letters max. That&rsquo;s a bit crazy.</p> <p><strong>Robert</strong> [1:25:24]: Yeah, but I think sometimes it may improve, my opinion, readability a bit, but I think it&rsquo;s all about not overusing that. So sometimes when you are&hellip; obviously, if you have struct receivers, it makes sense because you know that you are within some struct and you have receiver of the struct, so you know that S is, for example, service or something like that and it will just make a lot of code much shorter but if you are using all variables that are at most three characters length it may be not readable so i think it&rsquo;s all about the context so if you see that in this context you know this small shortcut what it means it&rsquo;s fine but it&rsquo;s all about not going to extreme i</p> <p><strong>Miłosz</strong> [1:26:09]: Think there are some guidelines like this in effective go maybe or some other document where it&rsquo;s like this recommended exactly as you said. Depending on the context, just longer or shorter variables. If it&rsquo;s a long function, and you use the variable across it, many times maybe longer is better, so it&rsquo;s clear what it does.</p> <p><strong>Robert</strong> [1:26:29]: Yeah, so I think it&rsquo;s about some common sense. So if you see that it&rsquo;s not readable, just use the entire name. And I would say that in most cases, we&rsquo;re using the entire name to make code readable, but we&rsquo;re also not afraid of using shorter ones.</p> <p><strong>Miłosz</strong> [1:26:42]: I don&rsquo;t like rules for stuff like this, and a similar one is what I actually like from Google style guide is there is no definitive rule for maximum line length. It just says, you know, try to refactor if it&rsquo;s too long, but it&rsquo;s not something like you should use 80 characters per line. Because then you have this fictional, artificial rule that everyone tries to apply but doesn&rsquo;t help anyone. It seems similar with short names.</p> <p><strong>Robert</strong> [1:27:19]: I think we covered it in an episode about writing low-quality code. At the end of the day, ask a question. Will it help? Maybe it will not help. Maybe it doesn&rsquo;t matter. Safety doesn&rsquo;t matter. Whatever. if it&rsquo;s good if you have enough time to think about such things you have a lot of time if you</p> <p><strong>Miłosz</strong> [1:27:42]: See variables like A, B and C probably something is wrong we can probably improve it, you can also go to the other extreme and call everything by too long names which is also not readable that&rsquo;s true, I agree that code should be readable And everyone has their own opinion about it, so of course that&rsquo;s hard to tell.</p> <p><strong>Robert</strong> [1:28:09]: Yeah, so I think this readable, it&rsquo;s something in the middle. So you&rsquo;re just not going like writing off your state code with one character length of variables. But sometimes you can also just use those short ones and it can really, for example, for receivers.</p> <p><strong>Miłosz</strong> [1:28:32]: Yeah makes sense.</p> <p><strong>Robert</strong> [1:28:33]: Okay so jim is saying that he appreciate that balance in your commentary too much of go community ime in my experience oh makes sense quite simple with simplistic thank you thank you to jim for your comments thank you</p> <p><strong>Miłosz</strong> [1:28:49]: Jim i think that yeah the simple with simplistic comparison is a good one like simplistic might be too much simply is fine.</p> <p><strong>Robert</strong> [1:28:56]: Yeah yeah definitely cool so we have no more comments so a couple more seconds for last comments if you have any yeah</p> <p><strong>Miłosz</strong> [1:29:07]: So maybe we can mention if you know if someone&rsquo;s listening to this and doesn&rsquo;t know go yet or anyone would like to try we have going on evening training on our platform where you can learn by writing code.</p> <p><strong>Robert</strong> [1:29:19]: And it&rsquo;s not our opinion but some people are saying that it&rsquo;s the best thing that they did online for learning stuff, so I don&rsquo;t know. Hopefully. And yeah, it can be one evening because Go has simple syntax and we did really a lot of work to compress the most important knowledge that you need. So you don&rsquo;t need to read the entire Go standard library to learn to Go code because it&rsquo;s not needed usually. There are some things that are most useful. That we know from the teams that we&rsquo;re running because a lot of people actually that joined those teams never worked in the Go earlier and they needed like one week to be totally proficient in Go. Yeah, yeah. So it&rsquo;s cool with Go.</p> <p><strong>Miłosz</strong> [1:30:08]: Yeah.</p> <p><strong>Robert</strong> [1:30:09]: It&rsquo;s not perfect language, but no language is perfect. And I think Go is closest to this perfect one. So I recommend it if you don&rsquo;t know Go yet. Cool. OK, so no more questions.</p> <p><strong>Miłosz</strong> [1:30:26]: Yeah, what should people do if they want to not miss the next episode?</p> <p><strong>Robert</strong> [1:30:31]: So we recommend to join our newsletter for one reason. So Deadly Algorithm may not send you a notification when a new episode is going out. So yeah, our newsletter is the best way to be notified about that. But we&rsquo;re still under the rule of Deadly Algorithm, so it would be cool If you can leave thumbs up, leave a five-star review in podcast applications, leave some comments if you have any questions who are answering and reading all the comments. So if you have some extra questions, we&rsquo;ll be more than happy to help you. What else?</p> <p><strong>Miłosz</strong> [1:31:14]: There&rsquo;s one more comment. Let me read it. So is it too disrespectful to say that Go feels like Node.js?</p> <p><strong>Robert</strong> [1:31:24]: It is.</p> <p><strong>Miłosz</strong> [1:31:26]: When you start a project with them you start from scratch one index.js or main go and there is no structure from the beginning, yeah guess on how you approach starting out like there are some scripts in node i think that set up this scaffolding for you yeah that&rsquo;s quite common but the approach yeah i think it&rsquo;s pretty a sane approach. Start with one file and then see what happens. Start creating packages as the main guy becomes too big to handle.</p> <p><strong>Robert</strong> [1:32:04]: If you probably compare it to other languages, like, I don&rsquo;t know, there are some languages that you need to use some kind of framework to build application and you have a lot of portal plates to start with. In Go, it&rsquo;s like with You can start with something super simple and extend. So in this case, it&rsquo;s totally true, but yeah, I think Go is totally different in terms of typing system. So it&rsquo;s much more strict, let&rsquo;s say. And while you are maybe building some smaller projects, it&rsquo;s not a big deal, but when you are working on much more complex projects, when more people are working in, it&rsquo;s much, much, much easier. Not mentioning about concurrency primitives that, in my opinion, in Node.js and JavaScript are awful. Sorry, in my opinion, promises and actually running everything on one thread is a deal breaker. In Go, it&rsquo;s better. So you have an abstraction of GoRoutines that is very easy to understand and think about. And it&rsquo;s also super fast because it can utilize multiple cores. Yeah, it has more threads. Hurray! And it has also nice primitives to talk between threads, let&rsquo;s say. There are no threads because this is something special in GoRoutines, but communication between them is super nice, especially compared to Python.</p> <p><strong>Miłosz</strong> [1:33:33]: Easy to grasp, easy to read and understand.</p> <p><strong>Robert</strong> [1:33:37]: Yeah, I think it&rsquo;s also interesting to actually compare to Python because, again, communication between threads, I have PTSD and I&rsquo;ve asked about that, not mentioning global interpreter log. Come on.</p> <p><strong>Miłosz</strong> [1:33:50]: Yeah, so it&rsquo;s hard to beat Go&rsquo;s combination of easy of use and also how powerful it is.</p> <p><strong>Robert</strong> [1:33:58]: Yeah, and what&rsquo;s also nice is how lightweight it is, because under the hood, one goroutine, so it&rsquo;s not one thread. Without going to the details, it&rsquo;s using one thread for multiple goroutines, so thanks to that, it&rsquo;s super lightweight. So, for example, you can spawn like 1 million, 10 million goroutines, and it&rsquo;s very lightweight, and you can run it easily on laptop.</p> <p><strong>Miłosz</strong> [1:34:22]: So there&rsquo;s a follow-up. Yes, I meant the lack of structure. For example, in .NET Core, Spring Boot, you have an initial structure. So that&rsquo;s about frameworks, I guess, more than languages. And we had an episode on frameworks as well, where we talked about this. I guess you can find some frameworks in Go too, where it will create some project structure for you. And maybe that&rsquo;s why people are used to those structures, they start the project in Go, start with a single main Go and they are lost and they know what to do next.</p> <p><strong>Robert</strong> [1:35:00]: Okay, if you are looking for some inspiration, so on our blog we have articles about clean architecture, how to dig pragmatically, and there is also this wild workouts project that we created. We&rsquo;ll add link to this project in the episode description and in the episode summary on our blog.</p> <p><strong>Miłosz</strong> [1:35:22]: All right, thank you for the questions and thank you for joining us.</p> <p><strong>Robert</strong> [1:35:27]: Thank you Miłosz.</p> <p><strong>Miłosz</strong> [1:35:27]: Leave a comment, let us know about your controversial opinions.</p> <p><strong>Robert</strong> [1:35:33]: In two weeks we&rsquo;ll talk about Watermill as we promised a bit earlier. So Watermill, we mentioned this library already a couple times in this episode. So this is an event-driven, not only event-driven, but mostly event-driven library for building event-driven applications in Go. It seems that this is the most popular library for building this kind of architecture in Go, and I think it has now more than 8000 stars on GitHub, and I think it&rsquo;s pretty popular and pretty successful and we&rsquo;ll share some magic stories about watermills and why we believe that it&rsquo;s successful.</p> <p><strong>Miłosz</strong> [1:36:14]: So if you plan to start an open source project, it might be interesting for you.</p> <p><strong>Robert</strong> [1:36:20]: And after that we&rsquo;ll talk about sync versus async architecture. So connected to watermills.</p> <p><strong>Miłosz</strong> [1:36:30]: Where one even different architecture shines and when it&rsquo;s better to keep to the standard synchronous operations.</p> <p><strong>Robert</strong> [1:36:38]: And it will be over-engineering. See you in two weeks, see you in four weeks. Thank you very much.</p> <p><strong>Miłosz</strong> [1:36:43]: See you then. Thanks, everyone. Bye-bye.</p> <p><strong>Robert</strong> [1:36:45]: Bye.</p>Learning Software Skills fast: what worked for us best in the last 15 yearshttps://threedots.tech/episode/learning-software-skills-fast/Wed, 16 Apr 2025 16:00:00 +0000https://threedots.tech/episode/learning-software-skills-fast/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Focus on applying what you learn</strong> - reading books or watching videos isn&rsquo;t enough without practice</li> <li><strong>Build real projects that challenge you</strong> - trivial examples don&rsquo;t expose you to the hidden complexities you&rsquo;ll face in actual work</li> <li><strong>Expect and embrace frustration</strong> - feeling stuck often means you&rsquo;re learning something valuable</li> <li><strong>Learn timeless concepts over framework-specific details</strong> - aim for universal software skills like modularization</li> <li><strong>Mix theory with practice in small chunks</strong> - read a bit and code a bit, rather than consuming large amounts of content at once</li> </ul> <h2 id="introduction">Introduction</h2> <p>In this episode, we discuss how to learn effectively as a software engineer. Why some people seem to learn faster than others? What are some practical ways to speed up your learning? Instead of promising magical shortcuts to becoming a principal engineer in months, we focus on a more balanced approach that helps you build skills by mixing theory with practice.</p> <h2 id="notes">Notes</h2> <ul> <li>We mentioned our <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Flearn%2F" target="_blank">learning platform</a></li> <li>Domain-Driven Design was referenced throughout - check &ldquo;Implementing Domain-Driven Design&rdquo; by Vaughn Vernon</li> <li>The Pragmatic Programmer by Andrew Hunt and David Thomas is a great book for timeless software skills</li> <li>Watermill - our open-source library mentioned as an example of a project that taught us while helping others: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill" target="_blank">github.com/ThreeDotsLabs/watermill</a></li> <li>Event-Driven Architecture traces back to 1950s, but was formalized about 20 years ago</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go" target="_blank">The Repository pattern blog post</a></li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>Just going fast is a trap, because you can go fast in the wrong direction and spend years learning things that are useless in practice or maybe are useful in the short term but not helping you during your entire career.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>Once you tackle building something more complex that you&rsquo;ve never built before, or maybe never even seen before, that&rsquo;s when things get interesting, and that&rsquo;s where your skills are tested.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>I learned the most with the hardest projects that I worked on. The projects that were easy, greenfield, and everything was known - okay, it was nice, fun, but I wouldn&rsquo;t say that I learned that much at the end.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>If you feel frustrated while learning, you are probably onto something. Because you are frustrated that you can&rsquo;t make it work. And then it also motivates you to push through.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>What helps to learn much faster is trying to focus on things that are timeless. You can get benefit from the compounding knowledge over time.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>Until you start fighting the obstacles, struggling to make it work, you probably won&rsquo;t learn much. The hard part is figuring out what to build that&rsquo;s a bit more advanced and will test your skills. If you pick up an idea and think &lsquo;I have no idea how to do it&rsquo; - that is probably a good direction.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>What also helps is sometimes feeling as the dumbest person in the room. Work with people that are more experienced than you. And if you sometimes feel that you&rsquo;re not the dumbest person in the room, maybe it&rsquo;s time to try someplace where you will be.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>If you work alone, you don&rsquo;t need to care that much about code readability. If you can fit the entire context of what the application does in your memory, you can use a single file that&rsquo;s 5000 lines long. But as you work with someone else, it starts to get more complicated.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfJyHkoyl0eg%26amp%3Bt%3D0s">00:00:00 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfJyHkoyl0eg%26amp%3Bt%3D313s">00:05:13 - What is real learning?</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfJyHkoyl0eg%26amp%3Bt%3D730s">00:12:10 - Theory vs practice</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfJyHkoyl0eg%26amp%3Bt%3D929s">00:15:29 - Learn by building projects</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfJyHkoyl0eg%26amp%3Bt%3D1653s">00:27:33 - AI for learning</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfJyHkoyl0eg%26amp%3Bt%3D2179s">00:36:19 - Is learning supposed to be fun?</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfJyHkoyl0eg%26amp%3Bt%3D2542s">00:42:22 - Learning from social media</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfJyHkoyl0eg%26amp%3Bt%3D3017s">00:50:17 - Timeless skills</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfJyHkoyl0eg%26amp%3Bt%3D3491s">00:58:11 - Diverse skills</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfJyHkoyl0eg%26amp%3Bt%3D3852s">01:04:12 - Making notes and teaching others</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfJyHkoyl0eg%26amp%3Bt%3D4116s">01:08:36 - Mentors and communities</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DfJyHkoyl0eg%26amp%3Bt%3D4314s">01:11:54 - Q&amp;A session</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Robert</strong> [0:07]: Ever worked with someone who&rsquo;s been coding for more than 10 years but still makes mistakes? Or have you seen a junior engineer write code that blows your mind? It&rsquo;s not always about intelligence. The real secret sauce is how you learn. Learning the wrong things can feel productive, but it won&rsquo;t get you far. In this episode, we&rsquo;ll explore the art of fast, effective learning. I&rsquo;m Robert.</p> <p><strong>Miłosz</strong> [0:27]: And I am Miłosz, and this is the No Silver Bullet live podcast, where we discuss mindful blackend engineering. We spent almost 20 years working together in different projects and teams. During that time, we learned that following advice like &ldquo;always do X&rdquo; or &ldquo;never do Y&rdquo; doesn&rsquo;t work and can limit your growth. In this show, we share multiple perspectives that will help you make smart choices and grow.</p> <p><strong>Robert</strong> [0:55]: Yeah so if you have any questions please leave them on the chat we&rsquo;ll try to address them on demand if not we&rsquo;ll have Q&amp;A session at the end of the live so we can answer other questions, anynthing to add?</p> <p><strong>Miłosz</strong> [1:11]: Good let&rsquo;s go</p> <p><strong>Robert</strong> [1:12]: All right so I think it&rsquo;s important to start with setting expectations because sometimes you may heard from many people that there are some magic way to learn faster you can be principal engineer in one month or maybe half year but well unfortunately it&rsquo;s not that simple so everything what we&rsquo;ll mention in this episode wouldn&rsquo;t be advice like that it wouldn&rsquo;t give you any magical power to be principal engineer in one year but definitely it can accelerate your car and it should definitely help you with not staying junior or mid engineer for four years because i think everybody probably met such person during working with software that as we mentioned at the beginning this was mid this mid engineer for forever yeah.</p> <p><strong>Miłosz</strong> [2:04]: And magic recipes usually don&rsquo;t work, and often means someone wants to sell you something, so watch out for silver bullets.</p> <p><strong>Robert</strong> [2:16]: But still, sometimes there are people that learn really, really fast. And today we&rsquo;ll share a couple hints that, from our observations, made them very effective learners. And again, it&rsquo;s not about being smart, because from my experience, sometimes being smart can be even a trap, because you may try to be too smart and try to maybe dig too deep to some things, but it&rsquo;s not always the best idea.</p> <p><strong>Miłosz</strong> [2:55]: Yeah, it depends on what you are smart about. But you can easily see this in different people you work with, especially in teams. Some people seem to learn much faster than others. And seems there must be some secret behind it or at least some ideas that can help you learn faster</p> <p><strong>Robert</strong> [3:22]: I&rsquo;m not sure if you agree but from my experience it&rsquo;s often not about what to do but actually the opposite so what you shouldn&rsquo;t do to actually not be slower so I think we can probably compare it to going somewhere by the car so let&rsquo;s imagine that you have Lamborghini and, slower car, but you don&rsquo;t have a map when you&rsquo;re going with Lamborghini. And in this case, you might get somewhere really fast, but not in the place that you need to go, actually.</p> <p><strong>Miłosz</strong> [3:52]: Right. So the destination matters, basically.</p> <p><strong>Robert</strong> [3:54]: Yes, yes. Because just going fast is a trap, because you can go fast to the wrong direction and spend years on learning things that are useless in practice or maybe are useful in short term. But they&rsquo;re not helping you during the entire career because probably you work as an engineer for&hellip;</p> <p><strong>Miłosz</strong> [4:17]: Yeah, and it&rsquo;s hard to differentiate because it&rsquo;s easy to feel like you&rsquo;re learning something. You can accommodate some trivia or ideas but you never put them in use. So I guess what we want to talk about today is learning skills.</p> <p><strong>Robert</strong> [4:36]: Something.</p> <p><strong>Miłosz</strong> [4:37]: You can put into practice you can use somehow in your work or just projects in general</p> <p><strong>Robert</strong> [4:45]: Yeah so probably it would make sense to start with some kind of maybe high level maybe not definition but idea what learning, means because you can do many activities that may sound like learning but it may not that&rsquo;s useful for you to use in Rework.</p> <p><strong>Miłosz</strong> [5:13]: So maybe the fact that you can use a skill to build something in practice would be the primary one for me. So it&rsquo;s not only about having the knowledge about some tool, library or programming language, but being able to use it in work to build something actually useful. And probably not a trivial to-do app or something most tutorials teach you. Right, so it&rsquo;s quite easy to use any programming language or library to spin up a trivial web application, because there are tons of tutorials like this. And nowadays AI can generate this for you right away, because it could learn on so many materials on this. But once you tackle building something more complex that you&rsquo;ve never built before, or maybe never even seen before, that&rsquo;s when things get interesting, and that&rsquo;s where your skills are tested.</p> <p><strong>Robert</strong> [6:25]: Yes, and I think maybe it may sound familiar for you, when, for example, you did the opposite. So, for example, you&rsquo;ve been on a conference, you&rsquo;ve seen some cool idea, or you read a book or article about this cool idea, and you&rsquo;re trying to apply it later in work, and for some reason, it doesn&rsquo;t work as it was promised in&hellip;</p> <p><strong>Miłosz</strong> [6:49]: Yeah, because you missed some hidden complexity or edge cases that were not mentioned there, or maybe the tutorial didn&rsquo;t cover it, and now you&rsquo;re stuck. That&rsquo;s a pretty usual experience when starting to learn something new in software especially.</p> <p><strong>Robert</strong> [7:09]: I think we can probably compare it to playing an instrument or driving a car. Let&rsquo;s imagine that you are trying to learn driving a car by just reading a book or watching some video tutorial on how it works. Okay, maybe it can give you some useful hints how to do that, but as long as you will not really do that and, put it into practice, you cannot really say that you can play an instrument or drive a car. Because in practice you discover a lot of hidden complexity that, okay, maybe on the movie it looks simple, but in practice there is much, much more and it&rsquo;s harder than it looks.</p> <p><strong>Miłosz</strong> [7:54]: And there is some danger, because you can feel like you actually know how to drive or how to play a guitar, let&rsquo;s say, after just reading about it. But then in practice you don&rsquo;t have any experience, so you won&rsquo;t be able to do it. And it&rsquo;s quite similar to reading books maybe, on programming especially. And I think we read quite a lot, especially 15 years ago when there weren&rsquo;t many online resources, videos and so on. The books were primary learning material for us. And sometimes the theory gives you nice ideas how to work with codes or mental models, but you have to actually start writing code to see if you learned anything.</p> <p><strong>Robert</strong> [8:48]: And it&rsquo;s because you can read about many things, but often what&rsquo;s described in the books it&rsquo;s applied in some very specific context. And the project that you are working at this time may not really fit this, or it may fit, but it&rsquo;s a bit different problem to solve. Because there are many similarities between multiple projects, but there are also some small things that are a bit different, but it can make problems much, much more different.</p> <p><strong>Miłosz</strong> [9:20]: And because books are focused on just one area, they can&rsquo;t give you the entire context. And I think we felt this because we came up with many examples to teach people some concepts. And it&rsquo;s very hard to come up with something that&rsquo;s not this trivial to the application, that&rsquo;s not complex at all, and come up with something that&rsquo;s kind of like in real life, but also can be used to teach someone something. And same issue with books many books try to introduce this idea of a project or something like that But it&rsquo;s quite hard to do it well. And to cover some real-life scenarios that someone can later put to use in their own projects.</p> <p><strong>Robert</strong> [10:09]: Yeah, and there is also one thing from our perspective of people that we&rsquo;re creating online examples, articles, book. It&rsquo;s also hard balance because, okay, in theory we can create an application that is as complex as a super complex application built by 100 people. But from the other side, it will be also a bit hard to work with that and learn from that. So it&rsquo;s a hard balance that we are trying to achieve. The other thing that is also coming to my mind is that it&rsquo;s connected to the thing that context in the book may be a bit different than yours. For example, when a couple years ago we were switching to Go programming language, we were working in some projects that required some more advanced tactics to write software like domain-driven design. The other side was that there were not all the materials. Actually, I would say that all the materials that were out there about domain-driven design was totally wrong.</p> <p><strong>Miłosz</strong> [11:10]: Yeah, it was a very long time ago. Yeah. It was very in the early stages back then.</p> <p><strong>Robert</strong> [11:17]: It still didn&rsquo;t change that much now. There are some materials that are good, but still, most of them are still missing the point. But anyway, I think we spent probably half a year to figure it out, how to do it in a way that is compatible with Go programming language. And you can say that, OK, it&rsquo;s just using it in a different programming language, but in practice, it requires time. So it&rsquo;s not that easy.</p> <p><strong>Miłosz</strong> [11:42]: Yeah, especially if you do it on a live product. in a team where no one has experience like this. Thankfully, we could use some books, like implementing domain-driven design back then, which was helpful. But still, there was a lot of iteration, and I remember the first approaches were much different than what we ended up with. And probably not that great.</p> <p><strong>Robert</strong> [12:10]: Yes, probably we can summarize this first anti-pattern as this theory is not practice, and we&rsquo;re taught to not be the person that is reading 50 books and think that, okay, I know how to solve many problems. Because just reading about that, it may be not enough. Because on paper, everything looks great, usually, but in practice, it&rsquo;s totally different stories. And I know some people that were like that, that they were reading about many things, and when it comes to solving some problem, they were like, oh, let&rsquo;s just do it like that. I wrote about it. It will be simple. But I know already that it&rsquo;s a bit different in practice.</p> <p><strong>Miłosz</strong> [12:50]: Yeah, sometimes it&rsquo;s fine to read just for fun if you are curious about something. But it doesn&rsquo;t mean you have to use it in your day work. But it&rsquo;s much different than learning skills, I would say.</p> <p><strong>Robert</strong> [13:05]: So it&rsquo;s all about extremes. So don&rsquo;t be a terrorist that is detached from reality. Don&rsquo;t be like an Envoy Tower architect that knows everything best from theory but doesn&rsquo;t touch this code and doesn&rsquo;t need to write it.</p> <p><strong>Miłosz</strong> [13:18]: If reading doesn&rsquo;t work, maybe video cursors. Do you like video lessons?</p> <p><strong>Robert</strong> [13:26]: No. No, because I think it has a pretty similar problem. So maybe it can give some more feeling like you&rsquo;re looking like somebody is implementing something and it&rsquo;s a bit closer maybe to feeling that you&rsquo;re really implementing that. But it also has one risk that if you&rsquo;re looking, if somebody more experienced is writing something, this person is probably skipping a lot of complexities that the person already know or just going straightly forward to the solution because, well, the person already know how to do that and doesn&rsquo;t struggle about that.</p> <p><strong>Miłosz</strong> [14:04]: If they show you a big project on screen, you probably won&rsquo;t be able to grasp the entire context just by watching the video. Although I think it&rsquo;s a nice format if you want to give a quick overview of some code, or if you want someone to see how to do something exactly, like refactor something. What comes to my mind is live coding on presentations. That&rsquo;s quite a nice format because it is like 5 or 10 minute video. Not video, it&rsquo;s live on presentations of course but it&rsquo;s a different format that can be useful but what I don&rsquo;t like is a video course that I open and I see there&rsquo;s 24 hours of video watching ahead of me and I am already bored I didn&rsquo;t even start and I know there will be lots of filler content in there and yeah, that doesn&rsquo;t make me want to learn.</p> <p><strong>Robert</strong> [15:14]: Yeah, so what are the solutions here?</p> <p><strong>Miłosz</strong> [15:18]: Probably build something.</p> <p><strong>Robert</strong> [15:20]: Oh no, I need to get my hands dirty. Touch the keyboard and implement stuff. That&rsquo;s so bad.</p> <p><strong>Miłosz</strong> [15:29]: Yeah, it&rsquo;s basically what we mentioned about this iteration while learning from a book. Until you start fighting the obstacles struggling to make it work you probably won&rsquo;t learn much and probably the hard part here is figuring out what to build right because again you can pick up a tutorial and build some easy application some trivial application but it won&rsquo;t teach you much so you have to pick up something that&rsquo;s a bit more advanced and that probably will stress you a bit i mean test your skills so if you pick up an idea and you think like i have no idea how to do it that is probably a good direction i would say because you will need to figure it out yeah</p> <p><strong>Robert</strong> [16:27]: I think we can compare it to like playing instruments with planning to play a gig. It&rsquo;s probably similar to how you should approach learning. For example, if you&rsquo;d like to play a gig, you probably should start with something simple, play guitar yourself, later some rehearsals with your band, and later going to a gig and playing that. Obviously, it&rsquo;s not easy, it requires a lot of time, but let&rsquo;s imagine that instead you are watching a video on how to play guitar and you&rsquo;re going strictly to directly to play a geek it will be probably not the best idea yeah and you&rsquo;ll be super stressed and probably it will be a disaster but it&rsquo;s similar thing it&rsquo;s similar thing with learning programming skills so your geek is well the job that you&rsquo;re working in and again you can try to learn something from just book from video and try to do your geek directly and okay, it will be a bit maybe faster, but from other side the result will be much worse.</p> <p><strong>Miłosz</strong> [17:35]: A parallel that comes to me is in software world it&rsquo;s like your employer gives you a video course buys it for you you watch some 20 hour video and then you&rsquo;re expected to apply it in the company&rsquo;s project which you&rsquo;re probably not ready to do it yet and you will experiment on a real</p> <p><strong>Miłosz</strong> [18:02]: product that&rsquo;s used by real people. So there&rsquo;s a risk that you will not apply it correctly and it won&rsquo;t end up that well.</p> <p><strong>Robert</strong> [18:12]: And it&rsquo;s another risk that I can see. So if you have some concept and the best place that you&rsquo;ll see to apply that is the project that you&rsquo;re working on your job, there&rsquo;s a risk that you may end up with doing the CV-driven development. So basically, you are applying all patterns that you can see in your job project, and it will be probably a disaster for the problem.</p> <p><strong>Miłosz</strong> [18:39]: Of course, I applied the saga pattern in my previous year.</p> <p><strong>Robert</strong> [18:43]: Yeah, and probably somebody is applying it to the context that it&rsquo;s not helping. So it&rsquo;s making the project worse, and also you&rsquo;re not relearning. Because if you are not applying those patterns for the right problems, you will also not learn how to do it properly, because you just apply it in the wrong context.</p> <p><strong>Miłosz</strong> [19:05]: So the ideal scenario, I think, is to have a pet project, but one that solves some real problem. Sometimes it&rsquo;s hard to come up with something like this, but it doesn&rsquo;t have to be super complex, but at least if it&rsquo;s something that does something real, it gives you this kind of illusion of working on something more serious than a simple to-do app that&rsquo;s free handlers and that&rsquo;s it. It&rsquo;s best if you can run in production and treat it as a production-grade deployment in general. So you have a domain and some servers or cloud or setup or whatever, set up some alerts, monitoring and so on. If you treat it like this, then you will start finding all the issues that can happen in a real-life project too. The hard part is finding this idea of what to build.</p> <p><strong>Robert</strong> [20:16]: But it&rsquo;s probably one even harder problem, because you can test it on some pet projects, But I think it&rsquo;s also essential to, at the end, apply it to some real project with some team. Because I think we&rsquo;ve been trying multiple patterns in some smaller projects. But after we have used it with teams, it also changed how we&rsquo;ve seen all that. Because if you&rsquo;re working on a project alone, it&rsquo;s a totally different dynamic, let&rsquo;s say. Because it&rsquo;s just much simpler. because if you overcomplicate something, you probably know how to go over it. But if you&rsquo;re working with a team, and if you overcomplicate something, you&rsquo;ll be not the only person that struggles. Your teammates will struggle much more, and they will also not be super happy about it.</p> <p><strong>Miłosz</strong> [21:10]: That&rsquo;s a good point, since if you work alone, you don&rsquo;t need to care that much about code readability, for example. If you can fit the entire context of what the application does in your memory you can use a single file that&rsquo;s 5000 lines long and you don&rsquo;t need many patterns there because you just know where everything is but as you work with someone else it starts to get more complicated it. So maybe that&rsquo;s a good advice too, to seek someone else you can share a project with. I think that&rsquo;s also how we started our more serious projects when we worked together.</p> <p><strong>Robert</strong> [22:03]: So there is a question on the chat that, like with playing guitar, so it&rsquo;s good to have friends to play with, but what if you have no friend to play together or practice with? How you motivate yourself to learn guitar until you join a gig. So we&rsquo;ll have some solution for that. So we and Milos are probably pretty lucky that we know each other and we&rsquo;re building projects together for almost 20 years. But yeah, we know that it&rsquo;s not the thing that everybody, not everybody has this luck, but we&rsquo;ll have some hints that can help with that definitely.</p> <p><strong>Miłosz</strong> [22:41]: I guess the primary one would be you can find a community online and join someone with building a project but</p> <p><strong>Robert</strong> [22:51]: We&rsquo;ll touch on that a bit later.</p> <p><strong>Miłosz</strong> [22:56]: Do you want to go over the two comments we have?</p> <p><strong>Robert</strong> [23:01]: Yes let me just check what from that we&rsquo;ll cover later, I think we&rsquo;ll cover so we can cover them a bit later when we&rsquo;ll have something in those contexts so let&rsquo;s wait for a while but yeah, we have in on our list.</p> <p><strong>Miłosz</strong> [23:27]: Martin mentions that he feels what I said about video courses and that they are like a torture yeah, I feel the same I think it&rsquo;s interesting format for shorter things, showcases or screencasts, for example. But, yeah, like 20-hour video courses are&hellip;</p> <p><strong>Robert</strong> [23:51]: Or maybe for some, let&rsquo;s say, more soft skill-related&hellip; Maybe not really soft skill, but not coding-related skills that are also important, but they&rsquo;re not about writing the codes. And yeah going maybe into the problems that are there so finding the friends to go together or, maybe you don&rsquo;t have a time because we know how we know how it&rsquo;s changing with the time so when you are younger you can code over nights and over the weekends but if you&rsquo;re getting a bit older you may have family, you can have multiple more duties. And it&rsquo;s a bit harder because, there are multiple obstacles that you have. So the first one is that you need to find some project to work in. And we already mentioned that this is the hard part of it, because Because it&rsquo;s sometimes hard to find a good problem to solve that is fitting the pattern that you&rsquo;re trying to use.</p> <p><strong>Miłosz</strong> [25:03]: Not some artificial problem, but something that actually solves something.</p> <p><strong>Robert</strong> [25:08]: And it doesn&rsquo;t oversimplify that. Because if you learn that, okay, get some simple problem and try to apply domain-driven design for that, and you will try to move it to real projects, it will just end up with some over-engineering stuff and it will be not the best. The other thing is probably also related to this torture with videos that you have super big chunk of knowledge to ingest, like 20 hours of video, you are watching that and later trying to apply that. And it&rsquo;s super hard. And sometimes you maybe don&rsquo;t have one or two hours to watch this video. It would be better to just have some smaller chunks to work in. And already I&rsquo;ve seen on the chat that somebody mentioned that my main struggle when learning new skills is finding clear, consistent, practical and consistent material. I hate huge as boring text-only documentations and more visual and neat short tutorials. So this is basically it. It&rsquo;s not easy to find this.</p> <p><strong>Miłosz</strong> [26:22]: Documentation especially. so it varies widely between projects. Some projects have really nice docs that you can get started easily, by the way. Give you some short examples and such. But some, yeah, not so great. You have to dig through the docs yourself and figure it out. So it&rsquo;s definitely not easy.</p> <p><strong>Robert</strong> [26:47]: It&rsquo;s also sometimes hard to find one place that is going from A to Z and explaining everything. And sometimes you need to go over 100 websites to find how to do that. Some alternative is Ask AI for that, but also you know that sometimes it can give you some hint, but it can lead you into the trouble.</p> <p><strong>Miłosz</strong> [27:09]: AI itself is a topic that suffers from this issue exactly. Because it evolves so fast, there&rsquo;s probably no one place you can go and learn about how to use it. It just depends on which model you use and so on. So you have to go through many documentations and websites probably to learn it well.</p> <p><strong>Robert</strong> [27:35]: And there is actually a question. So what&rsquo;s the AI impact for New West that skip the regular learning process? Do you recommend AI for using it? There are, starting learning.</p> <p><strong>Miłosz</strong> [27:48]: Software development?</p> <p><strong>Robert</strong> [27:49]: Yeah.</p> <p><strong>Miłosz</strong> [27:50]: So basically the question is how about using AI for learning?</p> <p><strong>Robert</strong> [27:56]: What do you think?</p> <p><strong>Miłosz</strong> [27:57]: I think there&rsquo;s The same issue as with books on one hand, so it can teach you theory, and maybe even worse, sometimes it can give you ready answers, which you just copy paste or use directly in your IDE and you don&rsquo;t touch the code, which can work, but you also probably won&rsquo;t learn as much if you don&rsquo;t struggle. And the other thing would be hallucinations, which is the primary reason I don&rsquo;t use them much. For example, learning a library. I tried a few times to use AI to generate some use case for a new library I didn&rsquo;t know, or an SDK. But very often it makes up function names, or just sort of invents something that doesn&rsquo;t exist. And in the end, I just prefer to read the docs myself and do it from scratch, because it&rsquo;s often confusing and I I don&rsquo;t like the fact that I have to double-check everything it does for me.</p> <p><strong>Robert</strong> [29:21]: Yeah so i i think i have pretty similar point of view that i would say that it&rsquo;s double-edged sword so it can help for many cases but you need to be really careful about what it&rsquo;s saying and double check that okay because sometimes it&rsquo;s helping to discover some thing that it may be harder to find somewhere in the internet or in the documentation and it&rsquo;s helpful but it&rsquo;s all about, verification that, okay, what it said, it makes sense. And I think it&rsquo;s probably easier to use for more senior people that already know more context and more understand how, what&rsquo;s the impact of the things that it&rsquo;s generating. I can imagine that if I would be more junior developer, it would be the risk that, okay, I&rsquo;m getting what it&rsquo;s giving me and putting it to the project, And I&rsquo;m spending ages to debug that and understand what&rsquo;s happening. And if I would just follow the way that you mentioned that I would do it from scratch, it will be just much faster.</p> <p><strong>Miłosz</strong> [30:26]: If you want to learn the skill, I mean, you can argue that you won&rsquo;t need the skill in the future because of AI. That&rsquo;s a separate discussion. For now, no one knows for sure, so let&rsquo;s leave it. But I also think there&rsquo;s so many use cases for how to use AI for learning it&rsquo;s very easy to give one answer because making it write the code for you is probably not the best idea if you want to learn some skills but I can imagine you could use it to give you some roadmap on what to learn you could prompt it with something like I know JavaScript and I want to learn Go recommend some concepts I should read about probably more helpful than just copy-pasting the code it gives you.</p> <p><strong>Robert</strong> [31:22]: Yeah, and I think there is one thing that with AI can be pretty helpful, and this is something that we&rsquo;re actually working for last more or less one month. Because there&rsquo;s also one problem when you&rsquo;re learning something, you may be stuck on some stupid typo or some stupid mistakes, and sometimes AI I can help you with being unblocked. So actually, since one month, we are now working on implementing that kind of mentor functionality in our learning platform, because we noticed that some of you were stuck on some, again, some typos or some simple mistakes. And it can be actually pretty nice in finding those typos or mistakes and unblocking you. But from the other side, it can also say some stupid things. And I would say that this is the biggest challenge that we are fighting now, is ensuring that it would not give you some false advice and learn you something false, basically.</p> <p><strong>Miłosz</strong> [32:23]: And that&rsquo;s something that&rsquo;s very hard to understand if you are not an expert in the field. Because it will sound very confident. And if you have no experience with what it talks about, you want to know better. So I will be careful there.</p> <p><strong>Robert</strong> [32:41]: The good news is that we found some ideas what we can make to make it less wrong and pretty moderated on what it&rsquo;s saying. But again, it will be never like 100% accurate. But let&rsquo;s imagine that probably you remember some cases like you were building something for fun and you were stuck for 10 hours because of some stupid typo. So it will be just super useful to be unlocked because it&rsquo;s not something productive let&rsquo;s say to be stuck because of type of our 10 hours and it&rsquo;s just frustrating.</p> <p><strong>Miłosz</strong> [33:13]: Yeah that&rsquo;s a good point so also important to not be not go too far to the other extreme and say I won&rsquo;t use AI ever because it&rsquo;s not a good idea just use it as a tool as any other tool</p> <p><strong>Robert</strong> [33:32]: And I think it&rsquo;s in general useful to making this tough process of learning by building stuff a bit nicer. Because we&rsquo;ve been doing that and sometimes we&rsquo;ve been really frustrated by the structure of building something. So basically we are trying to make this process more nice. So we are also, in our learning platform, giving some projects that you can work on. So you don&rsquo;t need to find out the problems for the techniques that you are learning. And I think it&rsquo;s also pretty cool because, again, in our life we built a lot of different projects and it took us some time to find some nice projects to learn stuff. But we know that it&rsquo;s sometimes hard, especially when you don&rsquo;t have that much free private time or sometimes you maybe would like to just have five minutes to work on something. And we&rsquo;re trying to cut this learning to some small steps to give you ability to just spend five minutes a day and progress a bit on this small project that we&rsquo;re giving.</p> <p><strong>Miłosz</strong> [34:48]: I like the fact that we recreated the way we used to work in the beginning. So you read some theory and then you solve some exercise, you write some code and you see it applied live.</p> <p><strong>Miłosz</strong> [35:07]: And still to this day, I think, is the best way I know of learning. You need some theory and then some practice. So this is what the other platform gives you as well.</p> <p><strong>Robert</strong> [35:19]: I think it&rsquo;s also nice that it has some path with some proven techniques and you are just following those techniques. And you don&rsquo;t need to look on hundreds of websites to find how to, for example, do event-driven architecture or something like that. But again, it&rsquo;s not for everybody. So when we&rsquo;ve been younger, we have just time to figure it out, go over internet and try to find all these techniques, try multiple different techniques, try to find a project to implement. But yeah, it&rsquo;s also fun. Yeah, yeah, definitely.</p> <p><strong>Miłosz</strong> [35:53]: We can spend the time.</p> <p><strong>Robert</strong> [35:55]: It&rsquo;s not for everybody, but again, if you are looking for some nice learning resources, go to our websites and it&rsquo;s tab, trainings and courses. Again, it&rsquo;s not for everybody. We&rsquo;ve been not using such a platform because it didn&rsquo;t exist, but again, it&rsquo;s also not only way to learn, so you can easily replicate it yourself. Okay so the next thing about learning that i think i heard pretty often is that learning needs to be fun and interesting yeah and i think it&rsquo;s also a bit risky because, it&rsquo;s easy to start to optimize for fun in learning and in my opinion it&rsquo;s not the best thing so if you compare it to school so well school is not about making so primary goal of school is not making to make learning fun it&rsquo;s about learning if you would like if you are optimizing for fun not learning it&rsquo;s kinder regarded, what do you think about yeah.</p> <p><strong>Miłosz</strong> [37:05]: And what comes to my mind is this gamification of you know different learning products so you get some prompts or badges and so on.</p> <p><strong>Robert</strong> [37:17]: Like Duolingo, for example. I&rsquo;m not sure if you&rsquo;ve been playing with Duolingo.</p> <p><strong>Miłosz</strong> [37:21]: A few years ago.</p> <p><strong>Robert</strong> [37:23]: I remember that it was actually quite cool that you have badges, you can have some frozen&hellip;</p> <p><strong>Miłosz</strong> [37:29]: You can invite friends and do it together. It seems like fun.</p> <p><strong>Robert</strong> [37:34]: And at some point you&rsquo;re actually realizing that you&rsquo;re not learning that much. I mean, you&rsquo;re clicking some funny stuff every day and you have to take 100, days, it&rsquo;s great, but it&rsquo;s also like, okay, it&rsquo;s very repetitive, and maybe it&rsquo;s fun, but okay, I don&rsquo;t feel that I went very far away.</p> <p><strong>Miłosz</strong> [37:55]: So I actually have a recent realization I had. So we were working on this AI stuff for our platform, integrating LLMs. And it&rsquo;s a new technology, you know, there&rsquo;s many things that have no standards yet, you have to discover many things and so on. And initially, I was very frustrated while working with it, because it&rsquo;s a different mental model too. For example, you mentioned about hallucinations. We are used to programs being very strict and predictable. But with LLMs it&rsquo;s just the other way. It&rsquo;s a bit random at times. And it was initially frustrating. And then we figured out ways to make it work, basically. And it hit me that this frustrating part is usually the sign that you are learning something, you know.</p> <p><strong>Robert</strong> [39:07]: But I think it&rsquo;s interesting because it&rsquo;s often frustrating from one side, but it&rsquo;s also a bit exciting. I&rsquo;m not sure how to describe that, but&hellip;</p> <p><strong>Miłosz</strong> [39:16]: It is exciting after you struggle.</p> <p><strong>Robert</strong> [39:19]: Yeah, yeah, maybe, maybe. And at the end, you don&rsquo;t remember that much this frustrating part.</p> <p><strong>Miłosz</strong> [39:24]: Yeah, yeah. So, I think that&rsquo;s actually what you should be striving for, maybe? I&rsquo;m not sure if that&rsquo;s a good word to put it, but basically if you feel frustrated while learning, you are probably onto something. Because you are frustrated that you can&rsquo;t make it work. That&rsquo;s how I feel about it. And then it also motivates you maybe to push through, or maybe demotivates you to give up. But I think it&rsquo;s a better sign than having fun. You can be having fun vibe coding. It&rsquo;s a fun thing to do.</p> <p><strong>Robert</strong> [40:08]: Or making games. So for example, something that you recommend.</p> <p><strong>Miłosz</strong> [40:12]: Yeah, making games is fun too. But also can be frustrating if you don&rsquo;t know how to do something. It&rsquo;s a similar thing.</p> <p><strong>Robert</strong> [40:19]: And I think it&rsquo;s similar when you&rsquo;re working in some projects. So probably we worked sometimes in the same projects. And I&rsquo;m not sure if you agree, but from my perspective, I learned the most with the hardest project that I worked. So the projects that were easy, greenfield, and everything was known. Okay, it was nice, fun, but I wouldn&rsquo;t say that I learned at the end that much. I think I learned the most with the projects that, you know, it was sweat, blood, and at the end we succeeded to kind of overcome that. But there were times of this project that it was really hard to just go over it. And well it was tough it was frustrating but at the end we&rsquo;ve been able to overcome that and i think it only works probably when to also be clear here so it&rsquo;s not only about projects that it&rsquo;s hard tough and it&rsquo;s you&rsquo;re not doing about anything about that because this is a death march it&rsquo;s not learning but if you are in the hard project and you have some plan how to improve that and you are implementing this plan, you are learning a lot. For example, you have some super legacy application used by many people with super complex logic, and it will require a lot of skills on multiple levels to just overcome it.</p> <p><strong>Miłosz</strong> [41:41]: And you just have to figure it out. That&rsquo;s the hard part and also the best learning moment. Like when you lose sleep because you fight with some issue you can&rsquo;t figure out the solution for and then you the next day you wake up or you take a shower and you somehow grasp it unconsciously but that comes after this struggling part it&rsquo;s not, you know, you are not just happy about it and having fun the entire day you think hard about how to solve this.</p> <p><strong>Robert</strong> [42:23]: And I think the opposite, I can sometimes see the opposite in people that think that are learning from social media, let&rsquo;s say. So you on X are observing multiple programmers that are doing some cool stuff and you read that and you feel that, okay, I know how to overcome some problems that people shared. But I think it&rsquo;s also dangerous because it&rsquo;s worth mentioning and reminding that social media is full of shows, let&rsquo;s say, making grass greener than it is, and a lot of populism.</p> <p><strong>Miłosz</strong> [43:06]: Yeah, so basically extremes.</p> <p><strong>Robert</strong> [43:09]: Yes, so what works on social media is writing about something that is controversial. Many people will disagree, comment on it, but unfortunately this is what works. And you may think that, okay, I&rsquo;m reading those comments, ideas, and I&rsquo;m learning a lot from that. But I think it&rsquo;s not like this in practice. And if you have more experience, you see that a lot of that is just making grass greener and hiding the bad parts of some approaches.</p> <p><strong>Miłosz</strong> [43:44]: Yeah, it&rsquo;s not really learning according to what we talked about initially. Because you don&rsquo;t use the skills and you don&rsquo;t learn any skills actually. Just learn some trivia maybe or some strong techs of some tech influencer.</p> <p><strong>Robert</strong> [44:03]: Especially those a bit less experienced that they don&rsquo;t know that they doesn&rsquo;t know and they think that, okay, you are overcomplicating stuff. You don&rsquo;t need to use X. It&rsquo;s not that, just go simple.</p> <p><strong>Miłosz</strong> [44:17]: Yeah, it may also seem like you contact only the extreme views on some topic. Because that&rsquo;s what attracts most comments and trolls and discussion in general online. You are very extreme on one end of the spectrum about something. In reality, there are probably not so many easy answers, and it depends on the context. Our last episode about clean architecture is probably a good example of this, where you can find as many people who love it as those who don&rsquo;t want to work with it ever again.</p> <p><strong>Robert</strong> [45:10]: But again, it&rsquo;s a risk, because it&rsquo;s easier to just scroll through social media and have something of learning, but again, it&rsquo;s not really learning. It&rsquo;s probably also similar to watching some fun programming streams and people doing some fun on coding. So it&rsquo;s probably easier to ingest and it may be fun. But again, I would separate the fun from learning.</p> <p><strong>Miłosz</strong> [45:42]: It&rsquo;s entertainment.</p> <p><strong>Robert</strong> [45:44]: Exactly.</p> <p><strong>Miłosz</strong> [45:45]: For example, my YouTube feed is now full of drummers doing some white tricks. But I&rsquo;m not even playing drums. It&rsquo;s something that&rsquo;s cool to watch, but it&rsquo;s not learning.</p> <p><strong>Robert</strong> [46:03]: So again, what&rsquo;s our tactic here? I would say that entertainment is also important, but it&rsquo;s also good to separate some time for some deep dive content that is not making grass greener and making ideal world in places where it&rsquo;s not. It&rsquo;s better to sometimes find some deep deep dive videos or articles, basically something that we believe that is teaching you a lot. And this is actually something that we&rsquo;re doing on our blog. So for example, we have multiple blog posts that are super long and they&rsquo;re probably not as easy to read as some five minutes articles. But from the other side, the alternative is reading some simplified articles and later struggling to implement it on production, because it&rsquo;s usually how it&rsquo;s ending up.</p> <p><strong>Miłosz</strong> [46:59]: And even if you read this deep dive article, you still need to apply it somewhere to truly grasp it. Right so it gives you a more more more tools more context but still it&rsquo;s best if you just use it yourself</p> <p><strong>Robert</strong> [47:16]: Yeah and don&rsquo;t take us wrong so it all doesn&rsquo;t mean that learning doesn&rsquo;t need to be fun it&rsquo;s just about putting the priorities so the first is learning because if you spend time on learning wrong stuff you just lose a lot of time and for example if you&rsquo;re prior to prioritize, fun but i would maybe say that you know you should not you should not optimize for fun but for excitement rather so find some exciting project that you can work on that will excite you to go forward and even if you&rsquo;ll be frustrated this excitement will try to you.</p> <p><strong>Miłosz</strong> [47:58]: Want to see it live</p> <p><strong>Robert</strong> [47:59]: Yeah that&rsquo;s the motivation.</p> <p><strong>Miłosz</strong> [48:01]: Here that&rsquo;s very nice</p> <p><strong>Robert</strong> [48:03]: And i think that it&rsquo;s for example what which helps me often to overcome this hard part.</p> <p><strong>Miłosz</strong> [48:12]: When the frustration hits, you need to use this excitement to look forward and push through.</p> <p><strong>Robert</strong> [48:19]: And I think it also helps at the end when you spend a lot of time on this frustrating project, but at the end somebody&rsquo;s using that and you see that it&rsquo;s useful for people and it&rsquo;s like, oh, okay, it was better. And I think, for example, I have sometimes like this with Watermule, with the library that we created, that sometimes working on fixing some flaky tests or merging some PRs or spending time on doing some reviews, it&rsquo;s a bit sometimes boring. Maybe it&rsquo;s a bit sometimes tough to do. But at the end, when we see that we&rsquo;re making a new release and people are happy about it and people are using this, it&rsquo;s giving some kind of fuel to progress with it.</p> <p><strong>Miłosz</strong> [49:02]: And it also started as a small library that solved our specific use case maybe also a good example of how we can start working on something that is useful for someone else and can help you learn something because we also learned a lot about maintaining an open source project over the years thanks to it definitely Definitely.</p> <p><strong>Robert</strong> [49:30]: So, okay. Now we, I think, covered pretty deeply what is the good direction, what you should basically learn. So, let&rsquo;s imagine that you have your map now, but let&rsquo;s now discuss for a while how you can get faster to this place that map is showing you to go.</p> <p><strong>Robert</strong> [50:01]: So, the first thing that I have in mind in this is, again, what you should learn. Or, okay, actually what you should learn. So, I think what helps a lot is focusing on things that won&rsquo;t get out of day pretty quickly. Because to take some example, let&rsquo;s imagine that you are a front-end developer and you are some framework specialist. And the risk is that your knowledge may become obsolete much faster if we compare to learning it some universal skills. So in other words, I think what helps to learn much faster is trying to focus on things that are timeless. And basically, you can get benefit from the compounding knowledge over the time.</p> <p><strong>Miłosz</strong> [51:10]: Maybe also not stick too long to one technology, if you see something else that you could use. Which on the other extreme is also not that nice if you just keep jumping from one programming language to another. Even take frameworks, it may be useful to know more than one, even if you primarily use one, even to just learn some different concepts. Maybe that&rsquo;s actually a good one about programming languages, but you can learn some just for for some different point of view even if you don&rsquo;t do it on production But it can also teach you those timeless concepts you mentioned.</p> <p><strong>Robert</strong> [52:03]: That&rsquo;s true. That&rsquo;s true. But I think one help that can help you evaluate&hellip; Because you are totally right. So it&rsquo;s important to not go into extremes like, okay, I&rsquo;m not learning any framework, I&rsquo;m just learning the things that are timeless. But it&rsquo;s more about not going in an extreme and trying to mix it and not being a framework specialist. I think we already mentioned it in previous episodes multiple times. And in terms of AI, you will be never better than AI in learning some framework quirks. Because it&rsquo;s just a limitation of your memory and it will be more updates than you can ingest. and you will be out of date sooner. But there are things that, well, AI will be never as good as you maybe. And like modularization or splitting bigger problems into smaller things like that. And it doesn&rsquo;t matter actually what programming language or what stack you are using. For example, if you learn how to modularize things or split some problems into smaller. It will always help you, independently what technology you will use.</p> <p><strong>Miłosz</strong> [53:23]: It&rsquo;s still relevant sometimes after decades since the invention.</p> <p><strong>Robert</strong> [53:29]: For example, I tried to check event-driven architecture that we are maybe not advocates, but we see many use cases where it&rsquo;s useful. So from what I found, the first origins was in 1915, so pretty long time ago. Current form, it was formalized around 20 years ago, so a pretty long time ago, but again, the idea is pretty old. And the cool thing is that probably when you learned in this eight years ago something about it, it will be highly relevant still. And if you learn that many of the things that are universal and timeless, it will just help you over the entire career.</p> <p><strong>Miłosz</strong> [54:22]: Yeah, you can apply it to other languages or frameworks or whatever. Sometimes it&rsquo;s fine to see how the solutions are reinvented in a different way. And this is like something fresh.</p> <p><strong>Robert</strong> [54:38]: Yeah, we&rsquo;ve been touching this in an episode with clean architecture, I think, at times.</p> <p><strong>Miłosz</strong> [54:45]: Or, let&rsquo;s say, like, Go channels, which seem quite fresh when coming from some language that doesn&rsquo;t have these concurrency primitives built-in, but they are, too, not some new concepts, right? It&rsquo;s just a concurrency model applied to a new programming language.</p> <p><strong>Robert</strong> [55:12]: I think it&rsquo;s also pretty interesting that Vibe coding is a pretty hot topic, let&rsquo;s say, now. And let&rsquo;s even imagine, so I think it will not happen soon or ever, but let&rsquo;s imagine a scenario when LLMs and Vibe coding totally replace programming. And still, in this world, skills like modularization or splitting problems into smaller, it will be still pretty useful. So you can try to hint this LLM a bit to how to split this problem into smaller, just to not have 5,000 lines of code, file, with everything. Because it will just struggle to do that. And it&rsquo;s visible now, when you&rsquo;re vibe-coding something and you&rsquo;re trying to put everything into one file, it&rsquo;s just a disaster. And it&rsquo;s not that obvious how to split it into smaller chunks, let&rsquo;s say. But if you have already this skill, it&rsquo;s much easier because you can hint this model how to split that and its performance will be just much better.</p> <p><strong>Miłosz</strong> [56:17]: And modularization is probably a good example of this topic you can&rsquo;t really learn that fast. And there are many extreme takes on it, like use microservices. And once you work with software for a while you know it&rsquo;s not that simple and it&rsquo;s also not something you can learn from a Twitter thread you learn in 30 seconds where someone says use this directory or whatever you just have to work through many different projects and learn some patterns learn some mental model that will help you later apply this approach.</p> <p><strong>Robert</strong> [57:02]: And I think it&rsquo;s a similar case with testing. So, again, let&rsquo;s imagine this work when we are no longer writing code, we are vibe-coding everything, fine. But it&rsquo;s still useful to know what kind of tests you would like to have, and, how they are made. Because even if your model is great, it&rsquo;s good to ensure that the next prompt will not do some regression in the logic that you had. And I would say that if this word will come, a bit won&rsquo;t, but never say never. I think it will be critical to have proper tests for that, to ensure that does other prompt will not break things that some previous prompt did?</p> <p><strong>Miłosz</strong> [57:48]: Yeah, we can see it with the work we did recently. But similar testing principles still apply. Exactly. Even if you use new tools.</p> <p><strong>Robert</strong> [58:00]: Don&rsquo;t be afraid. If AI will take your job, learn testing and modularization and techniques that you&rsquo;ll feel that will be timeless.</p> <p><strong>Miłosz</strong> [58:13]: Maybe it&rsquo;s a more general suggestion to have a diverse skill set. Probably everyone specializes in one area and it&rsquo;s fine, but it&rsquo;s useful to know a bit more about some fields similar to what you do. Like if you specialize in backend programming, you can still grasp some front-line ideas. So it makes it easier to communicate with teammates and solve more high-level problems than just looking at code.</p> <p><strong>Robert</strong> [58:50]: Yeah, and I think it&rsquo;s something especially good for some more senior people, let&rsquo;s say. So if you&rsquo;re a junior, okay, let&rsquo;s maybe try, you probably should try to master one area, let&rsquo;s say. What more senior you are becoming, Link. More you should learn some other areas because i think what&rsquo;s also useful is the fact that you can borrow from different areas let&rsquo;s say so if you are even doing back-end you can borrow some ideas from front-end maybe from data science maybe from i don&rsquo;t know ux or something like that so there are many nice ideas in different areas that can help you solve problems, or maybe even some areas that are totally not related to programming, like accounting, for example. You worked with some accounting domains. Actually, I think accountants have some cool ideas that you can apply to different domains, and it can help you to solve those problems nicely.</p> <p><strong>Miłosz</strong> [59:56]: Yeah, especially since the more senior you get, the more you work with people outside engineering. And you solve more high-level problems than just fixing bugs in the code. You have to figure out the entire process of a feature, let&rsquo;s say. So the more you know about some other fields, the better you can do it.</p> <p><strong>Robert</strong> [1:00:21]: And what I think helps here is, again, doing some pet projects that&hellip; It&rsquo;s probably our example. It&rsquo;s not that pet project because now we&rsquo;re doing it full-time, but before doing it full-time, we&rsquo;ve been creating those pet projects and we needed to learn how to do front-end, how to do some basic data science, how to do some basic UX, and I think it helped us over the years when we were working on just back-end engineering, mostly in a daily job. And also there&rsquo;s one thing that it&rsquo;s sometimes good to, learn is about some management basics. So if you&rsquo;re a software engineer, it&rsquo;s sometimes nice to work for a while as a manager, because you can also understand the wider picture and maybe understand how the component dynamics are working. Because often we can go into some extremes like, why those stupid people want something from us? And if you work as a manager for a while, you know that there is sometimes some reason for that and it actually makes sense.</p> <p><strong>Miłosz</strong> [1:01:38]: The dangerous extreme here is thinking of yourself as, I&rsquo;m just a developer. Tell me what to do and Don&rsquo;t make me figure out your issues. So, yeah, that&rsquo;s probably what you don&rsquo;t want to do, because then you become, well, easy to replace, basically, because you are just typing code.</p> <p><strong>Robert</strong> [1:02:13]: All right so we&rsquo;ve already said that uh from where you can to recap maybe from where you can learn so books so that this is the typical place from where you can uh learn from and it&rsquo;s good direction but remember to put the things that you learned in practice because you will just forget you learn pretty quickly and it will not give you that much uh that you read this book basically.</p> <p><strong>Miłosz</strong> [1:02:44]: What I like about books here is if they stood the time of test of time and became classics like the Pragmatic Programmer for example it&rsquo;s an easy way to figure out that there&rsquo;s something valuable in them but you have to be careful because the quality varies there are some books that probably could be could have been blog posts but there are also some timeless classics and it can also teach you some theory that&rsquo;s very nice to know.</p> <p><strong>Robert</strong> [1:03:17]: Yeah, so videos, as I said, we are not big fans of videos. There are some for programming knowledge. Maybe it&rsquo;s nice for some soft skills, but as somebody mentioned, it&rsquo;s sometimes a torture. And I think it&rsquo;s also a hard process of learning like that, because you cannot spend five keynote on watching that, switching to code and later watching, I think it&rsquo;s not working best, basically. Or for example, watching something for one hour and going to code.</p> <p><strong>Miłosz</strong> [1:03:50]: It&rsquo;s easy to get distracted or bored.</p> <p><strong>Robert</strong> [1:03:54]: Yeah, so I think we more recommend the way that, for example, we are promoting in our learning platform that you are learning some small part of knowledge, applying that learning piece of knowledge and applying that. But you can also do often similar things with the book, basically. What&rsquo;s also helped to remember stuff, at least for me, but I think probably for most people, is making the notes of the book. I think it may sound obvious, but sometimes you may not have enough time to do that. But I highly recommend that because without those notes, the chance that you forget what you just read is pretty big. What also works for me pretty nice is doing mind map for some more complex content for more complex or more complex concept content yes, because it&rsquo;s also helping to navigate through them easier and basically.</p> <p><strong>Miłosz</strong> [1:04:56]: Writing down in your own words often helps you think differently about the concept and also teaching others is very useful. Because then you try to figure out how to describe this in a simple way. It&rsquo;s often when I write about some topics on our blog that I start to get a deeper understanding of it. Because you have to construct this series of thoughts that someone can read and understand. So having your own blog or even writing in private is super useful for having a more deeper understanding of those ideas you learn. And you can go back to it later in the review. Even if you don&rsquo;t, it&rsquo;s worth the effort, I think, because it just helps you think better.</p> <p><strong>Robert</strong> [1:06:00]: And again, writing notes with your own words, I think it&rsquo;s very important because it&rsquo;s forcing you to do some thinking process and just remember it better.</p> <p><strong>Miłosz</strong> [1:06:14]: And something I really like to do is mixing theory with practice. We mentioned it before as well.</p> <p><strong>Miłosz</strong> [1:06:25]: So, you know, it&rsquo;s easy to go to the extremes here. So on one hand, you can be the person who just reads the entire documentation first and then starts applying it, which can take a long time and probably you will forget some parts of it. Or you can just try to blindly write it yourself and never consult the docs. It&rsquo;s also probably not a good idea because you can miss some concepts. So usually what I like to do is, let&rsquo;s say I use some new technology or library or something, to get the example from the documentation and have it running on my computer. That&rsquo;s the first contact you have with it. You can see it compiles, you see it running, you can play with it. That&rsquo;s how you learn how it works. And then you will probably run into some edge cases you don&rsquo;t understand. This is where the frustration begins. And this is when it&rsquo;s time to read the manual. Just go back to the docs, find maybe some theory of how it works and you just go back and forth between practice and reading. I think that&rsquo;s what works best.</p> <p><strong>Robert</strong> [1:07:52]: One thing that sometimes people are mentioning that it&rsquo;s great to learn from mistakes and That&rsquo;s true, but it&rsquo;s also not the most optimal way. So often you can lose a lot of time going&hellip; Around some simple solution that you don&rsquo;t know that exists and in perfect word it will be cool if you can have some mentor so sometimes you can have some mentor in the work that you&rsquo;re working on if not find some communities so for example like our discord that we have or there are some communities around our trainings maybe reddit and x may be tricky part because a lot of online communities are extreme and if you are less experienced, it may be tough. Some asking AI, it can be also sometimes.</p> <p><strong>Miłosz</strong> [1:08:47]: Yeah, it&rsquo;s a bit like this. If you don&rsquo;t know what you don&rsquo;t know yet, describing this to someone, maybe AI, maybe not, can be helpful. I remember how I was a junior programmer, it was super useful to have some seniors around that I could nag with questions during the first few months of work. I think it&rsquo;s a super fast way to learn. If you have some problems at hand, you need to solve, but you don&rsquo;t know how, so you ask someone who is an expert in this area, and they can explain why and how it works.</p> <p><strong>Robert</strong> [1:09:36]: Yeah, and I think from my experience, probably sometimes we missed this mentor, this person that could say us that just do it in this way, because we spend sometimes a lot of time to figure out some stuff, just to learn later that&hellip; This is the typical way of doing this, and if just somebody said to us, it would save a lot of time. On the other side, it&rsquo;s also a nice process sometimes to find a way to solve some problem and, it was also useful.</p> <p><strong>Miłosz</strong> [1:10:09]: But i</p> <p><strong>Robert</strong> [1:10:10]: Think it&rsquo;s important to have some balance basically here.</p> <p><strong>Miłosz</strong> [1:10:16]: Yeah but yeah maybe i i use for this can be a good idea sometimes because you not everyone can afford to have a more senior person sitting next to them and teaching them about some concepts so if you can have the the model explain some things or explain why something is done this way or another. Coming back to the previous discussion we had, if AI is good for learning, that might be one good use case.</p> <p><strong>Robert</strong> [1:10:47]: Watch out for that, because it can sometimes say some stupid stuff. What also helps? I think it&rsquo;s also good to sometimes feel as the dumbest person in the So basically, work with people that are more experienced with you. And if you sometimes feel that you&rsquo;re not the dumbest person in the room, and maybe it&rsquo;s time to try someplace where you will be the best person,</p> <p><strong>Robert</strong> [1:11:22]: and you have some people to learn from. Because this can really help you to accelerate your learning.</p> <p><strong>Miłosz</strong> [1:11:35]: So maybe online communities are a good example of that. If you don&rsquo;t get caught in a flame war.</p> <p><strong>Robert</strong> [1:11:44]: Yep. So watch out for that. All right. So something to add, Miłosz, or we can go to Q&amp;A session.</p> <p><strong>Miłosz</strong> [1:11:51]: Yeah, I think we can wrap up.</p> <p><strong>Robert</strong> [1:11:54]: So yeah, just a reminder. So if you have any questions now, please just leave on the chat, and we can answer that. Okay. Okay, so there is a question about our event-driven training. So it&rsquo;s one typical question that we have. So yeah, the plan about event-driven training is that, as we mentioned a bit earlier, so now we are working on our AI mentor that is integrated with our training platform. After that, we are planning to do big update to event-driven training before releasing that, and after that we will release that. So this is our plan. If somebody who is listening to that, if you already have event-driven if you already own go event-driven training, no worries you also have access to this because, as we promised you have lifetime access and lifetime updates of it basically this is the plan, we don&rsquo;t have exact dates because what is the reality of estimation so.</p> <p><strong>Miłosz</strong> [1:13:00]: We received many suggestions and feedback from trainees on the training.</p> <p><strong>Robert</strong> [1:13:09]: We also had a lot of feedback like this is the best training that I ever had but we all are aiming even higher.</p> <p><strong>Miłosz</strong> [1:13:16]: We also received some issues that we could solve and we wanted to build another training first but we probably will first update, go event driven before the next sale. So you will hopefully be able to learn even faster and better with it.</p> <p><strong>Robert</strong> [1:13:39]: If you are not in our newsletter, the best way to know about this is obviously joining that, so you&rsquo;ll be the first person to know. Again, we don&rsquo;t want to commit on any date because we don&rsquo;t know. When it&rsquo;s done, it&rsquo;s done, but, trust us it will be really cool update together with this uh ai mentor that we&rsquo;re working now so we are at the last mile let&rsquo;s say and it&rsquo;s really really helpful with many cases when you are basically stuck or you need some hint what&rsquo;s about what this exercise is it&rsquo;s it&rsquo;s cool okay the.</p> <p><strong>Miłosz</strong> [1:14:19]: Next one from ryan hi self-taught two years in career changer i know i need to build and learn as i go i&rsquo;ve been studying tdd bdd any tips to navigate the design of my data modeling user stories i&rsquo;m so noob don&rsquo;t worry and so this is about the modeling of Yeah, modeling of the design of the application, I guess. It&rsquo;s a bit like DDD, but maybe more general.</p> <p><strong>Robert</strong> [1:14:54]: Yeah, so I think we already have some blog posts about domain-driven design. They&rsquo;re not that big about modeling, I think, on our blog. So you can have some hints, but this is definitely the topic that we&rsquo;d like to explore deeper because it&rsquo;s useful for more complex domains. And we had some experience with that, so we&rsquo;ll be happy to share that. So we have somewhere the plans to write some new blog posts about that and have also a training that is around it but well as we said for the previous question so we have pretty long backlog before before that but.</p> <p><strong>Miłosz</strong> [1:15:31]: Maybe the follow-up is relevant because then royans says learning to deal with malleability of software and wanting to design things as from the beginning as possible is crippling me. So I think that&rsquo;s the core issue. You need to let it go. This is something we talked about in the episode about architecture. And maybe in some others as well, that you can&rsquo;t figure out the perfect design from the ground up. It never happens, because the software will change with time. So instead you need to adapt, try to start with something simple enough that works well, and see what the issues are, and then iterate.</p> <p><strong>Robert</strong> [1:16:18]: Yeah, and I think what&rsquo;s helpful is the approach that model will be never perfect. It needs to be useful. That&rsquo;s it. It&rsquo;s a bit like a map. So when you have map, we&rsquo;re talking about map multiple times during this episode, but when you have map, it&rsquo;s not a picture from satellite or it&rsquo;s not very, it&rsquo;s just detailed enough to know how to go from point A to point B. But it doesn&rsquo;t have all details, but again, it&rsquo;s useful. So I think thinking about models in a similar way is useful. that you can try to make it perfect but it will be never perfect as you said you change over time or won&rsquo;t.</p> <p><strong>Miłosz</strong> [1:16:58]: Be complete probably</p> <p><strong>Robert</strong> [1:17:00]: Yeah yeah but it also doesn&rsquo;t mean that you should just do YOLO and you&rsquo;ll improve it later because it&rsquo;s also not the best approach because if you just, take the old approach it will probably hurt yeah.</p> <p><strong>Miłosz</strong> [1:17:15]: This is the the hard balance between making software that&rsquo;s hard to extend and going to the other extreme where you try to foresee all possible future use cases and try to adapt your software to be universal so it&rsquo;s it&rsquo;s quite hard but it&rsquo;s like i think it&rsquo;s a good philosophy to start with that you know you want something good enough for now and something that is easy to change in the future and</p> <p><strong>Robert</strong> [1:17:44]: Make it as simple as possible but not simpler.</p> <p><strong>Miłosz</strong> [1:17:48]: Yeah that&rsquo;s that&rsquo;s hard to do i mean we touched on on this in the first episode about writing low quality code it is a bit similar topic um where to you know strive for this super high quality where everything is perfect but the reality is you probably won&rsquo;t get it and I think we&rsquo;ve seen it best in one system where we tried to model this accounting domain. And I remember, I anticipated this moment when we will finally cover all the edge cases and it will be finished. But then it turned out, even our stakeholders who were accountants didn&rsquo;t know how to do some things. And we just kept iterating and struggling for months. Turns out that&rsquo;s the reality of them.</p> <p><strong>Robert</strong> [1:18:46]: And don&rsquo;t be afraid that it will be not perfect because it will be not to set expectations. Also, when we are building some models that are far from being perfect, they are sometimes wrong. But again, as long as they are useful, it&rsquo;s good. And practice helps with time. So you&rsquo;ll have more idea how to get this pass. But, well, practice, practice, practice.</p> <p><strong>Miłosz</strong> [1:19:15]: The next comment is about this, exactly. I switched six years ago, completely self-thought. Plus about two years of theory courses before the actual switch. The only way for me is to get as much real-life experience as possible.</p> <p><strong>Robert</strong> [1:19:31]: Yeah that&rsquo;s it i see that also martin mentioned so yeah without.</p> <p><strong>Miłosz</strong> [1:19:37]: Practice no theory will stick</p> <p><strong>Robert</strong> [1:19:40]: Oh yeah i think i can confirm that it so you have the same feeling after 20 years thank.</p> <p><strong>Miłosz</strong> [1:19:46]: You for summarizing our episodes in one sentence that&rsquo;s exactly</p> <p><strong>Robert</strong> [1:19:51]: This.</p> <p><strong>Miłosz</strong> [1:19:54]: And as soon as you feel comfortable at something, it&rsquo;s time to learn something new. Good point. I&rsquo;ve switched to Go plus DDD a few months ago, and it&rsquo;s still frustrating to learn new things. I&rsquo;ll buy it a bit faster. Yeah, that&rsquo;s a good point as well. I shared before that I felt a bit like this, working with LLMs recently. Because for a long time we didn&rsquo;t touch much new technology i think we just know because we we&rsquo;ve seen that for many product issues very often technology is not the solution you can you can stick to very boring and stable technology and basically achieve what you need but yeah learning about something new is exactly this is exciting at the same time but also can be frustrating if you don&rsquo;t know how to move forward. The good part is, once you do satisfaction and you actually know you learned something.</p> <p><strong>Robert</strong> [1:21:04]: I think it&rsquo;s also really that something really new appears that can solve some new problems. I think LLMs are pretty different because it has some use cases that it wasn&rsquo;t possible to cover earlier, but from another side also a lot of companies are trying to find a problem for the solution. And it&rsquo;s also pretty problematic because it should be the opposite. So you have the problem and you&rsquo;re finding a solution for that.</p> <p><strong>Miłosz</strong> [1:21:35]: Okay and ryan wrap up props up um i also go back and review material frequently when i can remember something without looking it up um yeah</p> <p><strong>Robert</strong> [1:21:48]: I remember that when we worked in one company we have the book about ddd on our desks all the time and we were all the time opening the book and looking for stuff and it was like ah yeah it was something like that. But I think we learned during the time a lot. So it was just, okay, we had something in mind that it was solving similar problem. We were looking to this block. Unfortunately, the example was in Java. So it was like, okay, this is how it&rsquo;s done in Java. Let&rsquo;s try to figure out how to do it in Go.</p> <p><strong>Miłosz</strong> [1:22:23]: Yeah, but it sure helps to have this material Or documentation. Another way to use AI maybe is to have it summarize some parts of it for you.</p> <p><strong>Robert</strong> [1:22:34]: Or maybe put everything to RAG and query.</p> <p><strong>Miłosz</strong> [1:22:39]: That&rsquo;s another good use case probably. Because you won&rsquo;t host it as much if you provide the full context to it.</p> <p><strong>Robert</strong> [1:22:47]: It&rsquo;s a very fancy search, but it can do more things than search. Because it&rsquo;s not text search, it&rsquo;s more like meaning search.</p> <p><strong>Miłosz</strong> [1:22:55]: But when I think about it now, you know, going back to this project where we started using DDD, imagine we had an AI fed with this entire book and we could just ask questions about something specific or make it change the examples to go. That could be quite cool. But also it probably wouldn&rsquo;t be exactly what we arrived at. So I&rsquo;m not sure if that&rsquo;s a good or bad.</p> <p><strong>Robert</strong> [1:23:23]: Yeah, yeah. But it was definitely a value in figuring it out yourself. And, well, I think it may be hard to be junior now, basically, because, as Samadhi mentioned, it&rsquo;s probably trying to avoid LMs in some cases, but it&rsquo;s not always possible, let&rsquo;s say. Okay and the last question on the chat so well why how did you decide to teach it&rsquo;s more rewarding for you than just programming i want to teach and do presentations but i feel like i have no enough experience how would i start uh so yeah um so how we decided to do that i don&rsquo;t know how I.</p> <p><strong>Miłosz</strong> [1:24:20]: Remember that there was some New Year&rsquo;s Eve party, and I told you I thought about writing a blog, and you said, yeah, me too. Okay, let&rsquo;s do this. Sorry, created a blog.</p> <p><strong>Robert</strong> [1:24:36]: I don&rsquo;t have that good memory, but it sounds like a good beginning story.</p> <p><strong>Miłosz</strong> [1:24:42]: There was no grand plan, I think. For me, writing about stuff is, on one hand, satisfying and on the other, it also helps me think better about the subject. So that&rsquo;s what we mentioned about teaching others. So I think if you feel like it, definitely go do it.</p> <p><strong>Robert</strong> [1:25:04]: And I think you don&rsquo;t need to be super experienced to do that. So i think earlier you begin then better and don&rsquo;t be that much afraid that okay i will write something stupid or something like that it&rsquo;s not that big deal to be honest i mean it&rsquo;s hard to overcome that but at some point you will forget about that so if you start but so i think the good thing about is that it&rsquo;s as you said it&rsquo;s helping you to structurize the knowledge that you have and also i believe that skill of writing clearly it&rsquo;s useful in multiple cases so as we mentioned a bit earlier so it&rsquo;s helping you to convince for example your teammates to go in some direction write some design documents for some problems that you are doing and you can benefit in many fields from this skill, basically.</p> <p><strong>Miłosz</strong> [1:26:05]: And especially presentations are also not exactly about what you invented or discovered. There&rsquo;s also how you present, how entertaining it is, and so on. So you don&rsquo;t need to invent something great to be a good presenter or to be a good writer and yeah there&rsquo;s probably still like space for for new articles good articles because especially now I think we are you know scattered with this AI-generated trivial posts on topics and spend time to write something great.</p> <p><strong>Robert</strong> [1:26:57]: And yeah, I think it&rsquo;s again no silly bullet but what we recommend is creating the deep dive articles that are five times longer than average article in the topic and I think that this is some niche. And obviously, it requires more time to write that. Less people will read it. But I think in long term, it will just work nicely. And I think it&rsquo;s also good to write about timeless things. So we mentioned those timeless things multiple times. But to give you a good example, so the blog post that we wrote about repository pattern six years ago, I think, probably last month we have like 50,000 visits because it was just reposted on Reddit and it went over the entire internet. And again, it&rsquo;s probably since the start of the blog, I don&rsquo;t know how many people read that, probably, I don&rsquo;t know, 200,000 people or something like that. But it wasn&rsquo;t in this one year. It was over six years and it&rsquo;s still pretty valid and it will be valid probably in 10 years. So, but again, it took us some time to write that, but from other side, probably there are no other articles on repository pattern in Go that are that deep. So it&rsquo;s some niche, obviously. And you also asked if it&rsquo;s more rewarding than just programming.</p> <p><strong>Robert</strong> [1:28:22]: Oh yeah, definitely. And I think it&rsquo;s also worth mentioning that probably you are still programming more than teaching. Well, we love to write code and I think it&rsquo;s allowing us to have more ideas what we can teach, but it&rsquo;s about some balance. And I think it&rsquo;s just making it all more fun.</p> <p><strong>Miłosz</strong> [1:28:46]: Maybe we still do program what we teach through the platform. And maybe just one part of the story here is that we wrote a blog post, we had some example projects, and we wanted to recreate this idea. We talked about in this episode about being able to read some theory, write some code, and do it back and forth. To help people learn. That&rsquo;s how it happened.</p> <p><strong>Robert</strong> [1:29:24]: So TLDR, I recommend it.</p> <p><strong>Miłosz</strong> [1:29:28]: And share with us once you do.</p> <p><strong>Robert</strong> [1:29:30]: Yeah, definitely. If you would like to also have some feedback, we can also help a bit. Right, so it seems it was the last question that we had. So last minute to&hellip; To ask us questions.</p> <p><strong>Robert</strong> [1:29:57]: Alright, and so while we are waiting for the last questions, just in case if they will arrive, so just a reminder. So if you are not part of our newsletter, I recommend to join that, because we recommend to subscribe us on all platforms where you are watching us, listening to us but newsletter is the best way because we cannot control magic algorithm of other platforms but newsletter you will be sure that will notify you about new episodes, so remember to do that uh if you have any comments questions leave us comment on youtube we&rsquo;ll uh we are answering all the comments and we can also improve the and or touch it in the future episodes please don&rsquo;t forget to leave us thumbs up five star review because again deadly argo algorithms are are ruling the world and without that more people will not hear about about our live podcast so yeah that would be it for today i think so thank you miłosz for today&rsquo;s episode and see you in two weeks where we&rsquo;ll talk about the popular Go opinions we have.</p> <p><strong>Miłosz</strong> [1:31:16]: Exactly. And why are we controversial?</p> <p><strong>Robert</strong> [1:31:18]: Hopefully.</p> <p><strong>Miłosz</strong> [1:31:19]: Can do it.</p> <p><strong>Robert</strong> [1:31:20]: And if you are not writing in Go, join us as well. Maybe you&rsquo;ll learn something new.</p> <p><strong>Miłosz</strong> [1:31:26]: Or you can learn Go in two weeks using our learning platform.</p> <p><strong>Robert</strong> [1:31:30]: Or maybe in one evening.</p> <p><strong>Miłosz</strong> [1:31:33]: Okay. Thank you everyone for joining us today. Thank you, Robert. See you in two weeks.</p>Is Clean Architecture Overengineering?https://threedots.tech/episode/is-clean-architecture-overengineering/Wed, 02 Apr 2025 16:00:00 +0000https://threedots.tech/episode/is-clean-architecture-overengineering/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Clean Architecture is most beneficial for complex projects with larger teams</strong> - for small teams or simple projects, it can become overengineering.</li> <li><strong>Separation of concerns is the core benefit</strong> - keeping domain logic separate from implementation details makes code more maintainable.</li> <li><strong>It&rsquo;s easy to go too far</strong> - using too many interfaces or too many layers without a clear reason creates unnecessary complexity.</li> <li><strong>Start simple and evolve your architecture</strong> - don&rsquo;t force Clean Architecture from the start if your project doesn&rsquo;t need it yet.</li> <li><strong>Understanding the &ldquo;why&rdquo; behind the pattern</strong> is crucial - blindly following it without understanding leads to poor implementations.</li> </ul> <h2 id="introduction">Introduction</h2> <p>In this episode of the No Silver Bullet podcast, we discuss Clean Architecture and whether it&rsquo;s overengineering or a best practice for organizing code. We talk about why the pattern is often controversial, when it makes sense to use it, and how to implement it effectively. We share our experiences using Clean Architecture across different projects and teams, including the common concerns developers have when first seeing it.</p> <h2 id="notes">Notes</h2> <ul> <li><strong>Clean Architecture</strong>: a software design pattern that separates the code into layers, each with its own responsibility. <ul> <li>Introduced by Robert C. Martin (Uncle Bob)</li> </ul> </li> <li><strong>Related patterns:</strong> <ul> <li>Hexagonal Architecture / Ports and Adapters (Alistair Cockburn),</li> <li>Onion Architecture (Jeffrey Palermo).</li> <li>While slightly different, they share the core idea of separating domain from infrastructure.</li> </ul> </li> <li><strong>The Dependency Inversion Principle</strong>: one of SOLID principles.</li> <li>Blog post: <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F" target="_blank">Introducing Clean Architecture</a></li> <li>Blog post: <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-test-architecture%2F" target="_blank">Microservices test architecture.</a></li> <li>Blog post: <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F" target="_blank">Repository Pattern in Go</a></li> <li>Blog post: <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-cqrs-clean-architecture-combined%2F" target="_blank">Combining DDD, CQRS, and Clean Architecture in Go</a> (Mentioned as available)</li> <li>Example Go Project: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">Wild Workouts</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fgo-cleanarch" target="_blank">go-cleanarch Linter</a></li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>In general, if you ask many people about clean architecture they will say that it&rsquo;s overcomplicating things and making your project worse.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>One of the main points of Clean Arch is to separate the interesting code from the implementation details.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>The goal is not using any kind of architecture. But the goal is to use something that is useful for you. It supports your velocity because at the end of the day, every technique that we&rsquo;re using, it should allow you to code faster and that&rsquo;s it basically. So if you are using some architecture and it&rsquo;s slowing your down, you cannot iterate faster, it&rsquo;s probably the bad one.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>If you have been in a team that spent hours discussing in which directory to put some code, then you might assume you don&rsquo;t want to work with Clean Arch anymore because it&rsquo;s just it&rsquo;s too much hassle to keep discussing how to use it.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>If you are building more complex applications and you are starting to mix all this stuff it&rsquo;s super hard to maintain that. And this inversion of control helps you to cut this logic in multiple parts and make it easier to maintain.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>Don&rsquo;t do it because you want your code to be clean, but to better organize work in a complex project that will grow over time.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>Clean architecture is not that complex pattern. You have three, four layers depending on the complexity of your application. You&rsquo;re ensuring that layer from inside doesn&rsquo;t know about layer outside, and you&rsquo;re using interfaces to do inversion of control. That&rsquo;s it.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>The pattern is not exactly about project structure, which is also easy to fixate on. All the layers are more concepts, not exactly directories, although this is often the case.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHgoEY86vmlI%26amp%3Bt%3D0s">00:00:00 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHgoEY86vmlI%26amp%3Bt%3D88s">00:01:28 - What is Clean Architecture?</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHgoEY86vmlI%26amp%3Bt%3D932s">00:15:32 - Early Audience Comments</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHgoEY86vmlI%26amp%3Bt%3D1410s">00:23:30 - Controversies</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHgoEY86vmlI%26amp%3Bt%3D1796s">00:29:56 - Common Pitfalls</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHgoEY86vmlI%26amp%3Bt%3D3767s">01:02:47 - The Benefits of Using Clean Architecture</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHgoEY86vmlI%26amp%3Bt%3D4880s">01:21:20 - Team Collaboration &amp; Splitting Work</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHgoEY86vmlI%26amp%3Bt%3D5309s">01:28:29 - Can we Just Keep It Simple?</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHgoEY86vmlI%26amp%3Bt%3D5795s">01:36:35 - Alternatives</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHgoEY86vmlI%26amp%3Bt%3D5900s">01:38:20 - Go-Specific Implementation Tips</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHgoEY86vmlI%26amp%3Bt%3D6200s">01:43:20 - Summary</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DHgoEY86vmlI%26amp%3Bt%3D6411s">01:46:51 - Q&amp;A Session</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Miłosz</strong> [00:05]: Clean Architecture is an overengineering nightmare or the best way to organize projects. It all depends on who you ask. To make it work, you need to understand the &ldquo;why&rdquo; behind it and avoid the common anti-patterns. Today, we talk about why the pattern is so controversial and how it can help you in your projects. I&rsquo;m Miłosz.</p> <p><strong>Robert</strong> [00:25]: And I&rsquo;m Robert. And this is the No Silver Bullet live podcast where we discuss mindful backend engineering. We spent almost twenty years working together in different projects and teams. During that time, we learned that following advice like &ldquo;always do X&rdquo; or &ldquo;never do Y&rdquo; doesn&rsquo;t work and can limit your growth. In this show, we share multiple perspectives that will help you make smart choices that will help you to grow into principal engineer level.</p> <p><strong>Miłosz</strong> [00:55]: If you have any follow-up questions, you can ask them in the chat. We&rsquo;ll have a Q&amp;A session at the end as usual. And we might also pick up your comments during the discussion.</p> <p><strong>Robert</strong> [01:07]: This time, we&rsquo;ll try harder to look on the chat and pick up your questions, especially that I think the today&rsquo;s topic is pretty controversial one. And, yeah, I think it would be great to see some different point of views on that and also hear your experience with that. So we can also learn something maybe. How do you see that? And we can also create better content.</p> <p><strong>Miłosz</strong> [01:32]: Alright. So the title says Clean Architecture for today&rsquo;s topic but the pattern actually has a few other names like Hexagonal, Ports and Adapters, or Onion. They are not exactly the same thing. There are some differences in the implementation. But I think we can assume that the main idea, the core idea behind them is similar. Right? So similar that we can treat them as pretty much the same pattern. And we won&rsquo;t dive deep into the differences today, And we&rsquo;ll just stick to the clean architecture just because that&rsquo;s how we started calling it and I think it&rsquo;s quite popular anyway.</p> <p><strong>Robert</strong> [02:18]: Probably the most common term because, yeah, you could probably heard some terms like hexagonal architecture, ports and adapters, onion architecture. But, yeah, the idea on a high level is quite similar.</p> <p><strong>Miłosz</strong> [02:33]: Yeah, and while it&rsquo;s popular I think everyone understands it a bit differently which is common with complex patterns.</p> <p><strong>Robert</strong> [02:41]: And probably this is the reason why there are also a lot of misunderstanding about that because many people actually are messing clean architecture with domain driven design. So some people don&rsquo;t have a good idea how to connect that because, okay, in many clean architecture implementations you have the domain layer. So for some reason it&rsquo;s like, okay, you have domain layer so we are doing domain driven design. Or some reason also some people also think that okay, clean architecture is some Java or it&rsquo;s some enterprise. It&rsquo;s not something that you can use.</p> <p><strong>Miłosz</strong> [03:14]: That&rsquo;s only for enterprise super complex projects.</p> <p><strong>Robert</strong> [03:17]: Yeah, yeah. And in general, if you ask many people about clean architecture they will say that it&rsquo;s overcomplicating things and making your project worse.</p> <p><strong>Miłosz</strong> [03:29]: Which is problematic because if you don&rsquo;t understand something, chances are you won&rsquo;t implement it properly in your project. So that&rsquo;s risky. And I think often it&rsquo;s also a very controversial pattern. If you mention it to someone and they had bad experiences from the past usually they won&rsquo;t react well, like, they won&rsquo;t be happy about you using the pattern, let&rsquo;s say.</p> <p><strong>Robert</strong> [03:58]: Yeah, and I think it&rsquo;s interesting that we worked in multiple teams and usually we have some people that were not happy about using clean architecture but at the end we&rsquo;re kind of able to convince them that it&rsquo;s nice. Probably we can have one person confirm that. So Firmino said that he&rsquo;s missing working with Milosz. So it&rsquo;s a good sign. So at least one person is missing working with you. Maybe clean architecture helped a bit.</p> <p><strong>Miłosz</strong> [04:28]: Thank you Firmino, at least one person misses me. You can mention if you like clean architecture or not. So it might be one example for today&rsquo;s discussion. Maybe it&rsquo;s not that bad.</p> <p><strong>Robert</strong> [04:41]: What I think is interesting about that the thing that I already mentioned that you know, we were leading, multiple teams in our lives and it wasn&rsquo;t uncommon for us to have some people coming from other projects. And, well, we had pretty good experience with clean architecture and they were coming to our project and most of the people were like, oh, like, okay, it&rsquo;s cool. Please tell me how it&rsquo;s working. But some people, I think usually some people who have already done have more experience or thought, oh, no, clean architecture or over engineering stuff. But in most cases, it was like, okay, it took some time, like, it wasn&rsquo;t weeks. It was like sitting with this person for a half hour showing how everything works. And at the end, most of it people were saying, yeah, it&rsquo;s actually cool. I&rsquo;m using it in my projects after the job. Actually, I remember one person even said that he was building a game and he used the architecture in that because he could exchange, I think, engine or something like that. It was quite cool.</p> <p><strong>Miłosz</strong> [05:43]: So does it mean we should use it everywhere? Would you say it&rsquo;s a silver bullet?</p> <p><strong>Robert</strong> [05:52]: No. But, you know, I think pet projects after work, it&rsquo;s probably a pretty good place to experiment and do some crazy experiments. But, yeah, long story short, I think we&rsquo;re pretty surprised to later hear that it&rsquo;s not everywhere like that, like in our team. So there are people that try to clean architecture and at the end, they were saying that it&rsquo;s always bad. And we started to wonder what is the reason why people hate architecture in all projects.</p> <p><strong>Miłosz</strong> [06:24]: Maybe we can answer it today. But maybe let&rsquo;s start with a very short definition. Let&rsquo;s try to boil it down to the very core ideas because there are many flavors and, you know, even if you use one of the flavors, you can use it in a different way than someone else. So let&rsquo;s just try to agree on what&rsquo;s behind the idea. For me, the first one is separation of concerns, and it often comes with the layers idea in those architectures. But after years of using it, I think you could just summarize it as, you know, you agree that your- project has code of two categories, The boring code, which is the implementation details that you don&rsquo;t care that much about.</p> <p><strong>Robert</strong> [07:17]: Or you should not care. Because, unfortunately, often it&rsquo;s the opposite.</p> <p><strong>Miłosz</strong> [07:21]: Yeah, like the SQL queries you use or what database engine you use. It is important, of course, but probably doesn&rsquo;t make your business win automatically. And on the other hand, there is the the interesting code, the domain code, the logic of what your application does, sometimes called business logic. But I think this is a bit unfortunate term because it, you know, it again has those enterprise vibes. And I think people writing usual web applications maybe don&rsquo;t think of business code when they write the logic they do. But the fact is it is some kind of domain code anyway. Right?</p> <p><strong>Robert</strong> [08:05]: Yeah. But it&rsquo;s worth mentioning that it depends heavily on what kind of application you&rsquo;re working on. So just a kind reminder. So this is no silver bullet podcast, live podcast, and, yeah, it can suggest that it depends on different projects. And so as you said, so sometimes you have business logic, but sometimes it&rsquo;s as simple that you can have it in controllers mixed with SQL queries. But sometimes it&rsquo;s not that simple. I mean, you can go this way, but and we did it in our lives but it&rsquo;s a nightmare a bit later.</p> <p><strong>Miłosz</strong> [08:37]: But I think we can agree that the main point of one of the main points of CleanArch is to separate those two, right? To separate the interesting code from the implementation details.</p> <p><strong>Robert</strong> [08:51]: Yes, and probably it&rsquo;s worth mentioning how it&rsquo;s done. So it&rsquo;s all about the well, I think it&rsquo;s not the first time when we&rsquo;re mentioning that there are some old patterns that are still valid and the same is for, inversion of control. And the idea is that you can implement, you can implement so you can define interface on some behaviors that you expect for example, from the database, from external providers. So and thanks to having interface for that your implementation doesn&rsquo;t need to know really how it&rsquo;s implemented under the hood. And, again, if you&rsquo;re creating a simple application it&rsquo;s not a big deal because the logic is not that complicated needed there. But if you are building more complex applications and you are starting to mix all this stuff it&rsquo;s super hard to maintain that. And this inversion of control helps you to cut this logic in multiple parts and, well, make it easier to maintain.</p> <p><strong>Miłosz</strong> [09:52]: And it&rsquo;s not that complicated, right? Inversion of control sounds a bit complex but in reality this is accept interfaces instead of implementations.</p> <p><strong>Robert</strong> [10:03]: Yes. But we&rsquo;ll come into that because, well, like always the idea is simple but in practice people unfortunately are overcomplicating that and there are multiple ways on how you can overcomplicate that but we&rsquo;ll touch on that in a while.</p> <p><strong>Miłosz</strong> [10:19]: So we have, separation of concerns of the domain and implementation details and second inversion of control of dependencies. Often there is some concept of layers, which are basically groups of code you use that are dedicated to something. So often we have the Adapters, which are for, you know, external communication of external things like databases or API clients or even for entry points to your applications which we like to separate often.</p> <p><strong>Robert</strong> [10:58]: So in other words, it can be calling other microservice or other module. So it it can be database, it can be external API, external by not your company but well, if you are building real microservices, you know, other microservice should be kind of like separate, something totally separate for you.</p> <p><strong>Miłosz</strong> [11:17]: And this layer is quite boring usually. It just maps your code onto some external code and that&rsquo;s it. It should be boring. Then there is some kind of application or use cases or controllers maybe, layer where the logic is or orchestration of your application. And sometimes also domain layer which is this pure business logic layer, which does not always have to be.</p> <p><strong>Robert</strong> [11:49]: So the second layer is so again we&rsquo;ll use multiple names for that because as we said that at the beginning so there are multiple names for let&rsquo;s say similar idea like clean architecture, ports and adapters etc. So we&rsquo;ll just use multiple names to be compatible with more. So the second layer is interfaces or entry points or sometimes ports. And the idea is that this is the place where you&rsquo;re defining your HTTP handlers, your gRPC handlers, your message handlers, even CLI. So any entry point to your applications. The idea is that any operation that is coming into your application, it should go through this layer. And what&rsquo;s super cool about this, layer is that it&rsquo;s allowing you to have different entry point for the same operation. So, for example, let&rsquo;s imagine that you have application that allows you to create a customer and for some reason you need to create customers through message and also through HTTP API. It&rsquo;s nice because at the end you can do the same operation share a bit less code and support multiple different ways of interacting with your application from external world.</p> <p><strong>Miłosz</strong> [13:00]: I really like this approach because, you know, when when you take a look at the project, it&rsquo;s very easy to see what are the entry points. So it&rsquo;s very clear where is the API if you want to interact with it.</p> <p><strong>Robert</strong> [13:13]: And it works pretty nicely with the pattern that we touched in the previous episode. So MVC. So this is basically your controller from your MVC.</p> <p><strong>Miłosz</strong> [13:24]: But, given all those layers, it&rsquo;s also important to keep in mind that the pattern is not exactly about project structure, which is also easy to, you know, fixate on. So all the layers are more concepts, not exactly doesn&rsquo;t have to be directories, although this is often the case. But we probably will touch on it a bit later.</p> <p><strong>Robert</strong> [13:57]: So the next layer. I think this is pretty problematic layer because I think many people don&rsquo;t understand what this layer is but this is application layer. So this is the layer that is doing the orchestration of everything. So I think it&rsquo;s not probably the best place to describe how it can look in practice. At the end, we&rsquo;ll give some materials that we believe are covering how to implement application layer nicely. But, yeah, long story short, this is the orchestration of what you are doing in your application. And this should be called by your ports.</p> <p><strong>Miłosz</strong> [14:31]: Ports or interfaces or entry points.</p> <p><strong>Robert</strong> [14:35]: And it should call your domain layer if you have this layer, and your adapter&rsquo;s layer, long story short.</p> <p><strong>Miłosz</strong> [14:44]: So that&rsquo;s the parts of the puzzle, I guess. The separation of concerns, and inversion of dependencies and layers, right?</p> <p><strong>Robert</strong> [14:57]: And I think we forgot about one layer or maybe you mentioned it. So the domain layer. The optional one because I think it&rsquo;s worth mentioning that it&rsquo;s sometimes useful but not always. And but it yeah. It&rsquo;s the layer with the logic. So the layer that should be free of any database logic of any adapters, it should be, relevant. Again, it&rsquo;s probably something for I think it&rsquo;s better to read code than talk about that to show how domain layer works. But again, we&rsquo;ll give you some material.</p> <p><strong>Miłosz</strong> [15:28]: It&rsquo;s probably also not the place to go into details on application versus domain, but you may find some of it on our blog.</p> <p><strong>Robert</strong> [15:37]: Let&rsquo;s maybe take a look on the chat so I&rsquo;ve seen a lot of messages there so we can catch on it. So, yeah, there are some voices that, yeah, I like it often and use it often and like it. So, yeah, we have some positive things about clean architecture. The second comment, I love that clean architecture and your articles helped me to show how to code is not about language but about structure and functionality. Thank you.</p> <p><strong>Robert</strong> [16:05]: We will later link to some articles that we have on our blog. So, yeah, I think it will be just explaining better how those layers work because, again, it&rsquo;s pretty hard probably to just talk about that without showing the code. What else? Nice topic. I believe that we shouldn&rsquo;t face architectural choices in dogmatic sense. Definitely. We&rsquo;ll touch on that a bit later.</p> <p><strong>Miłosz</strong> [16:28]: Yeah, we&rsquo;ll touch on it.</p> <p><strong>Robert</strong> [16:30]: Oh, another person missing working with Miłosz.</p> <p><strong>Miłosz</strong> [16:34]: Even though we didn&rsquo;t work together. Yeah, maybe in the future Oleg, who knows.</p> <p><strong>Robert</strong> [16:40]: Greetings. Yes, so there&rsquo;s one comment about overengineering. I see when using interfaces a lot. A lot of projects are created with a single interface each struct. We&rsquo;ll touch on that also.</p> <p><strong>Miłosz</strong> [16:57]: Yeah, I think that&rsquo;s a very misunderstood part of the pattern like how to use interfaces.</p> <p><strong>Robert</strong> [17:04]: And it&rsquo;s actually, I think, pretty easy to overcome and also to have some way of understanding when needed problem. But no worries, we&rsquo;ll touch on that. It&rsquo;s like using abstraction without being abstract.</p> <p><strong>Miłosz</strong> [17:19]: Yeah, that&rsquo;s a good point.</p> <p><strong>Robert</strong> [17:20]: I think it&rsquo;s exactly that. But again, we&rsquo;ll go into that. As I understand clean architecture, you can&rsquo;t depend on layers that are deeper one level. So you have to duplicate structures every time that makes me sad. Yes, but not always. I think we&rsquo;ll also touch on it. Okay, so hi, I work at a big e-commerce company with 10k+ IT area with 4,000 microservices. That&rsquo;s the scale. Okay, to give some context, there is an area which is a clean architecture for most of their microservices. So this is probably around what we said a bit earlier so Or maybe not.</p> <p><strong>Miłosz</strong> [18:12]: And there is a follow-up, I think you need to include this.</p> <p><strong>Robert</strong> [18:15]: You can touch on it.</p> <p><strong>Miłosz</strong> [18:18]: Yeah, it&rsquo;s implemented across apps and libs, all Java of microservices. This fact implies that all the areas have an important learning curve to understand and add code here. Yeah. So you mean that there&rsquo;s, bigger learning curve to understand the code because of it? Which can happen, and we will also cover this.</p> <p><strong>Robert</strong> [18:51]: But I think it depends a lot on how you&rsquo;re implementing that. So again, I would say that it should be some hint like if it&rsquo;s taking a week to explain a person how can architecture clean architecture work in your company probably something went wrong and it&rsquo;s maybe not really clean architecture but something around. Because, again, explaining clean architecture and showing it in the code, it should be quite quick.</p> <p><strong>Miłosz</strong> [19:14]: Should be, yeah, but not always. I think people always start with this big chart of different circles and shapes and it can be confusing at first. Yeah. I think we will talk about controversies in a bit. So let&rsquo;s start there.</p> <p><strong>Robert</strong> [19:35]: So maybe let&rsquo;s finish with the comments. So, people write the interfaces next to concrete type when it should belong to the client code. Only including the methods itself depends on common mistake.</p> <p><strong>Miłosz</strong> [19:48]: So this is exactly this misuse of interfaces. Right? So if you do it this way, then there is no inversion of control, basically. Or maybe there is but gives you nothing in return.</p> <p><strong>Robert</strong> [20:06]: So another thing that we&rsquo;ll also cover, so no worries, is starting a new project with this architecture. We&rsquo;ll cover that. How to explain to my colleague that it&rsquo;s worth using this approach? I think we&rsquo;ll also cover that. And again, later I will also recommend checking the articles that we&rsquo;ll link a bit later. So we are also in those articles giving some context where when it works and why it&rsquo;s, makes sense. Okay. And to catch on the last comment so far, I&rsquo;m really proud of you guys. Oh, thank you. Because you have decided to run your own company as your main. Now, we&rsquo;re also pretty happy about that. Alright, cool. So, now, we&rsquo;re finished with the comments so far. Again, if you have anything to comment in, we&rsquo;ll be happy to go over it. Alright. So layers. We talked about layers. I already mentioned that it&rsquo;s not necessary to always use all layers and I think it&rsquo;s one reason of reason of one common mistake that people are always using the all layers and because of that it&rsquo;s making it more complicated. So somebody in the chat mentioned that it sometimes hurts because you need to copy structs between layers and it&rsquo;s a waste of time and it&rsquo;s true. But it&rsquo;s mostly true in cases when it&rsquo;s not needed because you&rsquo;re just using all layers when its project is not complex enough.</p> <p><strong>Miłosz</strong> [21:39]: You start with the big idea instead of seeing where the project leads you. Especially if you start small. Probably, it&rsquo;s better to let the architecture evolve over time than to force some complex ideas in the beginning.</p> <p><strong>Robert</strong> [21:55]: I think we&rsquo;re always mentioning that that, you know, you need to be pragmatic at the end of the day. So if you see that pattern is creating more mess and require more effort that it&rsquo;s giving you back probably it&rsquo;s something wrong because at the end of the day it should help you and, well, that&rsquo;s it. And it&rsquo;s connected to the dogmatic approach of that. I think dogmatic approach is pretty often the case why it&rsquo;s over complicated because often often there are some people that are saying no we need to do it in this way. It needs to be consistent with everything else.</p> <p><strong>Miłosz</strong> [22:29]: We like consistency. And I think the topic that we talk about in every episode just keeps coming back.</p> <p><strong>Robert</strong> [22:36]: But, if you didn&rsquo;t have chance to check our previous episode about the low quality code highly recommended because it&rsquo;s also connected to that. Right, but let&rsquo;s go back to the architecture.</p> <p><strong>Miłosz</strong> [22:52]: So maybe just to summarize, I think you can it&rsquo;s very easy to to say what will be the the opposite of the architecture. I think that will be having all your code in an HTTP handler. Your logic, your validation, and, your database code is all mixed in the same handler or controller. I think that will be the opposite, basically.</p> <p><strong>Robert</strong> [23:20]: And it&rsquo;s a better approach? No.</p> <p><strong>Miłosz</strong> [23:23]: It depends.</p> <p><strong>Robert</strong> [23:23]: We&rsquo;ve been doing that. Sometimes it was good enough. Sometimes, well, I hope nobody needs to maintain this codebase any longer.</p> <p><strong>Miłosz</strong> [23:37]: Somehow clean architecture is quite controversial, right? We already mentioned this. And I think for us it was quite surprising because when we started using it, we were quite happy about it and people in our teams were also quite happy. We even heard of them spreading the pattern to other companies as they left our team. Yeah. So why do you think it happens?</p> <p><strong>Robert</strong> [24:10]: I have one theory. So I think that the theory may be that we are trying to be extremely programmatic. So I heard that I heard it multiple times. So if we have seen that somewhere it was making our lives harder than more helping us it was probably we&rsquo;re not using this pattern in this case for you and we&rsquo;re not blindly following that and I know that it&rsquo;s opposite compared to other companies.</p> <p><strong>Miłosz</strong> [24:39]: You mean if someone forces you to use it? Or maybe someone someone has seen it implemented in a way that wasn&rsquo;t the best to work with?</p> <p><strong>Robert</strong> [24:50]: Or even worse, somebody is forcing you to use that without understanding it. And this is the ultimate combo.</p> <p><strong>Miłosz</strong> [24:57]: For the sake of consistency, for example. All microservices in our company will follow Clean Architecture. So, yeah, that might be controversial and that might scare people, I guess. And for some reason, this is tends to divide people. And often I hear something like, you must be Uncle Bob cultist because you use those patterns. So it starts to get almost political maybe, I don&rsquo;t know. It&rsquo;s funny.</p> <p><strong>Robert</strong> [25:38]: But for what I can see in the chat? I can see that some people actually also see it pretty pragmatic. So, I don&rsquo;t consider hexagonal architecture over engineering at all. When building a bigger than just couple of weeks project, first, it allows to register stubs for our own implementations, details, and tests. Yep, it&rsquo;s true. The second comment saying that I believe clean architectural learning is not that hard. Yep, that&rsquo;s true. And we believe that it&rsquo;s true. I have seen people moving to similar way of development without knowing those concepts exist. Yeah. But I think we can compare that. And actually, one team will have ultimate combo because we&rsquo;ve been building a project in Go that and that time Go wasn&rsquo;t that popular language and 90% of the team was learning Go and Clean architecture at the same moment and it wasn&rsquo;t an issue. Maybe we&rsquo;ve just had a great team. It was probably also important reason there and very open minded and great team. But, yeah, at the end, it was cool.</p> <p><strong>Miłosz</strong> [26:57]: I think you skipped one. I think you skipped one comment.</p> <p><strong>Robert</strong> [27:02]: Did I? Yeah. Hi folks, I&rsquo;m wondering how well clean architecture matches the lang&rsquo;s initial idea. From the little that I&rsquo;ve read about it, I had feeling that Go apps were supposed to be a little simpler. Oh, yeah.</p> <p><strong>Miłosz</strong> [27:24]: My favorite topic of keeping it simple and we will touch on this and on Go later. So stay with us, please.</p> <p><strong>Robert</strong> [27:36]: Right.</p> <p><strong>Miłosz</strong> [27:41]: Those comments are very much about the architecture not being that complex in the first place. I think maybe it is clear when when you learn it. But I think if someone doesn&rsquo;t know it yet and sees a codebase, then it can become quite confusing. Right? I remember once when our team was using it, but other team in the company was not, and people there were not aware how it works. And for some reason, they had to debug something in our codebase one night. And that was quite confusing for them because if you don&rsquo;t know the rules, know where the dependencies are, and so on, it can be quite confusing at first.</p> <p><strong>Robert</strong> [28:31]: Yeah. But I think important part of this is being kind of open minded because if somebody is like, oh, clean architecture is bad over engineering. I don&rsquo;t want to look on that. So obviously, if somebody have approach like that, well, it will not work. But if somebody&rsquo;s open minded and he&rsquo;s open to learn how it works, it helps a lot. Yeah. But And I think that that was part of the problem that other teams were like, no, I don&rsquo;t like this idea and why we are doing that?</p> <p><strong>Miłosz</strong> [29:03]: We like consistency, as we said. So, you know, if we use the simple Go mindset, for example, let&rsquo;s say, it can be it can add to this controversy of using it. I mean, that might be one of the reasons.</p> <p><strong>Robert</strong> [29:19]: And it&rsquo;s also worth mentioning that often within a company you have multiple teams solving different problems. So some of the teams may be solving some simpler problems that doesn&rsquo;t require some advanced techniques and some teams in the company may be working on very complicated domains. And it&rsquo;s not a matter of using any tool that it&rsquo;s complicated. It&rsquo;s just complicated by design and you can help to tackle that, but it&rsquo;s just helping you to make it simpler. But it has some base complexity, let&rsquo;s say, that you can try to tackle, but it will be always complex because just logic is super complex.</p> <p><strong>Miłosz</strong> [29:56]: So should we talk about some common issues maybe? How it fails basically?</p> <p><strong>Robert</strong> [30:08]: Yeah. Because we were discussing this topic a bit earlier with people on some social media or in real life. Yeah, it was a very common topic to understand why it&rsquo;s not working in multiple projects. So we gather multiple reasons that we know that it&rsquo;s leading to making the architecture overcomplicated or even project failing. By the way, if you have any, any stories from you, also please leave it on the chat so it will be also cool to hear about that. Right. So the first thing is, over engineering architecture. So as we said multiple times and it was also mentioned multiple times on the chat, clean architecture is not that complex pattern. So you&rsquo;ll have three, four layers. It depends on the complexity of your application. You&rsquo;re ensuring that layer from inside doesn&rsquo;t know about layer of in the outside, and you&rsquo;re using interfaces to do inversion of control. That&rsquo;s it. But, unfortunately, people are going sometimes a bit too far with that.</p> <p><strong>Miłosz</strong> [31:14]: Yes. Because what you said doesn&rsquo;t sound like proper architecture. It reminds me of this, what we discussed in previous episodes about this belief that we can figure out the architecture now, and in the future, we will just cookie cut the services or applications. It sounds a bit similar to me that we try to figure out the perfect architecture first, all the rows in place and layers and so on, and then just apply it. It sounds much much better than just following some simple rules of using interfaces. And it&rsquo;s easy to go too far, right, and become a purist and just force this idea everywhere.</p> <p><strong>Robert</strong> [32:06]: So, overconsistency, let&rsquo;s say.</p> <p><strong>Miłosz</strong> [32:09]: Yeah. This is the hard part, when you want to be consistent and when there is too much consistency.</p> <p><strong>Robert</strong> [32:19]: It&rsquo;s always a cost. so the second thing that I have in mind about clean architecture and I think this is probably the most common one so it&rsquo;s and I think somebody mentioned it on the chat so it&rsquo;s using too many interfaces. So in general, the interfaces are needed if you are kind of communicating between layers so those layers inside will not know how what&rsquo;s implemented then. But that&rsquo;s it. But very often I&rsquo;ve seen that within the layers before putting more interfaces. For example, you have domain layer and under the hood it has multiple interfaces. And what&rsquo;s even more funny often it&rsquo;s under the hood have only one implementation. And it&rsquo;s pretty problematic because one, it&rsquo;s harder to inject that because your dependency injection is starting to be super hard. And this is something that is also probably common thing common thing between between multiple episodes because people were saying that they have complex of complex problem with complex dependency injection and from my experience, it&rsquo;s often the problem is that your dependency tree is too complex. It&rsquo;s not a problem with some dependency injection framework or whatever you are using. So if you&rsquo;re not able to inject your dependencies by hand, probably something over complicated.</p> <p><strong>Miłosz</strong> [33:37]: It&rsquo;s part of the over engineering, I guess. You know, just forcing the ideas because there&rsquo;s a rule that says accept interfaces because you might want to change them with something else, and then you create some domain service, let&rsquo;s say, that accepts some some structure. You might you might go and think, oh, maybe I will use an interface here because I am supposed to use interfaces, right? Maybe in the future we will want to change the implementation. And it&rsquo;s exactly this trap that you take the base ideas and just use them everywhere even when it where it doesn&rsquo;t make sense. So maybe I no. A good a good a good heuristic here is to think of, what what you want to use mocks for in your tests. That&rsquo;s probably what you want to use an interface for.</p> <p><strong>Robert</strong> [34:30]: Yeah, and obviously between layers but this is a different case. And I think it&rsquo;s very important to mention that, okay, if you are mocking things you should probably mock only things that are IO in general. So it&rsquo;s database, some external API. You should mock not mock your domain logic because you have no benefit of that. So your test will be worse because you will be not checking the, the real code plus well, it will not speed up your tests basically. So mocking makes sense in most cases when you need to speed up your tests or or simulate some external logic. In your domain code, you should be able to do it through your public API.</p> <p><strong>Miłosz</strong> [35:13]: Exactly. You can tell you went too far if you&rsquo;re, you know, if you use too many mocks and all your testers do are checking some function calls on the mocks you used. It probably means you went too far and you don&rsquo;t need to mock that much.</p> <p><strong>Robert</strong> [35:30]: Yeah, but there are some exceptions. So there&rsquo;s no silver bullet. So exceptions are very very very complex domains. So sometimes you may have some complex domain and within the domain layer, you may have interfaces to hide this complexity somewhere. But you never should start with that because it will go in the wrong way. So I guess, I don&rsquo;t know, within one year probably it will be not possible to have such the complex domain code to, need to have really layers within layers. A couple years old codebase that is super complex, maybe, but I would watch out for that.</p> <p><strong>Miłosz</strong> [36:07]: Layers within layers actually sound like the Great idea. Yeah. Like the You&rsquo;re so smart. The next pitfall, right? It&rsquo;s again over engineering in the layers area. I remember this Reddit post. You know which one? The other day at the Golang sub subreddit there was a Reddit post where the OP said something like, Don&rsquo;t use pin architecture, it&rsquo;s over engineering. And of course it, you know, invited me to click. And in the content they said something like, you should have at most three layers in clean architecture. And I thought, yes, of course, what&rsquo;s the problem? Again, this is how different the implementations can look like. I guess what this person found in the code was, I don&rsquo;t know, six layers? I can&rsquo;t even imagine what was there. But that&rsquo;s another issue, you know, if you just take the idea of layers too far.</p> <p><strong>Robert</strong> [37:24]: Another surprise. I mean, if I would work in projects where you have six layers at least, I would also hate using that and would love to go with a simple approach, obviously.</p> <p><strong>Miłosz</strong> [37:36]: Yeah. So maybe it&rsquo;s good to start with the basics. So you want to separate the implementation details from the logic, from the domain code. And that&rsquo;s two layers for starters. It doesn&rsquo;t have to be much more. You can separate the entry points if you want. That&rsquo;s kind of a hard layer, but I would call it part of the adapters as well. It&rsquo;s a bit similar. I&rsquo;m not sure why would you use something beyond the domain layer. What&rsquo;s the fifth one? I don&rsquo;t know.</p> <p><strong>Robert</strong> [38:16]: Maybe you know somebody is using layers from hexagonal and onion architecture.</p> <p><strong>Miłosz</strong> [38:23]: Yeah, maybe. So if you see more than four, I would be very, very careful. I would ask some questions.</p> <p><strong>Robert</strong> [38:33]: Definitely. The next thing that I have in mind about the common problems about clean architecture is using it everywhere. So one point that we&rsquo;d like to, you know, give you with this live podcast is that there&rsquo;s no one single way to use something as we&rsquo;re mentioning, in the intro. So if somebody is saying you always do x or never do y, It&rsquo;s pretty big chance that this person just work in some specific type of projects, but it&rsquo;s not universal advice.</p> <p><strong>Miłosz</strong> [39:09]: Or they want to sell you something.</p> <p><strong>Robert</strong> [39:11]: Or this. Some shortcuts as usual. But what is the message? So the message is that, you know, clean architecture is nice, but it doesn&rsquo;t matter that you need to use it in an entire company. So the thing that I mentioned a bit earlier. So often in company, you have teams and services that are complex and simple and using the same approach for everything is just crazy because in some places it will be over engineering and sometimes it will just suffer from from the complexity that is not handled properly and it will be also the problem.</p> <p><strong>Miłosz</strong> [39:51]: Sometimes even in the same project in different modules, it also has some trivial domain that doesn&rsquo;t need it. Just what we have in the White Workouts, our example project, the Users module or service is basically a Scrub domain with no layers at all. Yeah, and it&rsquo;s different for your past projects because if</p> <p><strong>Robert</strong> [40:14]: You are working on something after work, it doesn&rsquo;t matter that much. I think it&rsquo;s worth trying multiple approaches to have your opinion on how it works. But in a job, it well, it&rsquo;s worth to be pretty mindful. And if your project is not complex maybe clean architecture may be not needed but sometimes it may be worth with starting with some simple one without domain layer, for example. It doesn&rsquo;t cost that much. But if you know that your team is bigger, then it can make sense to start with that.</p> <p><strong>Miłosz</strong> [40:46]: Yeah. And and some apps are are may most of them are not be a good fit or maybe if you create a library, for example, maybe it&rsquo;s not the best idea because the users might not be familiar with it or for things like CLI apps maybe or something infrastructure related.</p> <p><strong>Robert</strong> [41:06]: And you if you think about that, so libraries are usually living in inside of some other layers. They are not covering multiple of them. So usually, it&rsquo;s probably adapters or ports. Like, for example, Watermill library that we&rsquo;re maintainers are also It&rsquo;s living within ports and it&rsquo;s not going to other layers basically.</p> <p><strong>Miłosz</strong> [41:26]: Yeah, exactly. So you don&rsquo;t want to tie it with your application logic or the domain. A similar one is Project Structure, which we mentioned before. It also keeps coming up in our previous episodes that it&rsquo;s easy to focus too much on the project structure in the beginning. Again, it&rsquo;s this idea of, you know, let&rsquo;s prepare this great environment, great architecture, and then we can just keep adding features. Most of the time it&rsquo;s not that easy and the project will evolve anyway, in some way you don&rsquo;t know yet. And you can always refactor, right, if it changes. So I wouldn&rsquo;t start with thinking of the, you know, what&rsquo;s the perfect project structure or what&rsquo;s the perfect number of layers. Just start with the basics, something simple and then refactor as you go.</p> <p><strong>Robert</strong> [42:40]: And assume that you may be wrong. I mean, we have almost twenty years experience with coding in general. Also pretty much years, you know, with architecture and still we are not always right. But I I think it&rsquo;s important to know how you can be not right and how to handle that and not go too deep into the wrong architecture, basically.</p> <p><strong>Miłosz</strong> [43:05]: It&rsquo;s probably not possible to invent the perfect structure in the beginning anyway.</p> <p><strong>Robert</strong> [43:16]: If somebody is saying that he can do that, don&rsquo;t trust this person.</p> <p><strong>Miłosz</strong> [43:21]: They want to sell you something.</p> <p><strong>Robert</strong> [43:25]: And overall I think it&rsquo;s if you&rsquo;re thinking about some pitfalls of architecture or problems with that. So the most important thing that I believe made our teams happy about using this is keeping in mind that it should be helpful for you. So the goal is not using any kind of architecture.</p> <p><strong>Miłosz</strong> [43:44]: Oh, it&rsquo;s a big one.</p> <p><strong>Robert</strong> [43:47]: But the goal is to use something that is useful for you. It supports your velocity because at the end of the day, every technique that we&rsquo;re using, it should allow you to code faster and that&rsquo;s it basically. So if you are using some architecture and it&rsquo;s slowing your down, you cannot iterate faster, it&rsquo;s probably the bad one.</p> <p><strong>Miłosz</strong> [44:12]: I mean, it&rsquo;s very easy to start being dogmatic about it, to become like a purist. And it is tricky because we have a bigger team and it helps to have this consistency around the project. So Clean Architecture gives you this consistency in supports. But then there might be places or features where it&rsquo;s not a great fit. But if you want to break the rules, then someone needs to decide if it&rsquo;s fine to break the rules or not. And it often starts those endless discussions about it doesn&rsquo;t follow clean architecture or it breaks.</p> <p><strong>Robert</strong> [44:57]: Which layer it should be in? Is it domain logic or it should be in domain or application layer?</p> <p><strong>Miłosz</strong> [45:03]: Okay. Let&rsquo;s call it a meeting. A meeting to discuss is super important. And sometimes it is important but sometimes it&rsquo;s not. It&rsquo;s not. It&rsquo;s just, you know, a detail.</p> <p><strong>Robert</strong> [45:14]: And, unfortunately, this is the topic that we cannot give cannot give you a simple ask for like always do x. The best way, unfortunately, here is to just do it over time and learn by doing and probably this is the best advice that we can give here. But, well, it&rsquo;s hard to have any shortcuts here because there are just many different kinds of applications. And, again, it&rsquo;s important to be open minded. Also listen to your teammates because it&rsquo;s also easy to be, you know, the architect or the person that gives architecture. And if you are going to holidays and going back, you see that it was all removed. It&rsquo;s probably the sign that.</p> <p><strong>Miłosz</strong> [45:54]: Have to change everything.</p> <p><strong>Robert</strong> [45:55]: So, you know, it&rsquo;s also probably the sign that, okay, maybe something that you invented is not that helpful for people because if it would be if it would be helpful for them, they would not remove it during your holidays.</p> <p><strong>Miłosz</strong> [46:08]: I think this this factor is for sure part of the controversy around it. Because if you if you have been in a team that, you know, spent hours discussing in which directory to put some some codes, then you you might assume you don&rsquo;t want to work with Cleanarch anymore because it&rsquo;s just it&rsquo;s too much hassle to to keep discussing how to use it instead of this right code. Yeah. And especially if you didn&rsquo;t have the proper onboarding for new people</p> <p><strong>Robert</strong> [46:39]: In your team because often, you know, people are changing teams, changing companies. And this is also important thing to ensure that, okay, the new person is joining the team, the person will know how it works in your company and how to navigate about that. Because if you will not do this, the person may be not happy about it.</p> <p><strong>Miłosz</strong> [47:00]: Yeah, it&rsquo;s probably best to show them instead of having some documentation because I think it&rsquo;s easier to show it as an example maybe than reading about something. Definitely. But on the other hand, I think clean atch also enables some nice onboarding because you can have those you can have new joiners implement something quite simple because of the separation of layers. Like, you can give them an HTTP handler to implement and it will be a quite simple task. They can probably do it in one day and get started with the codebase.</p> <p><strong>Robert</strong> [47:41]: And again, we&rsquo;ve mentioned it already a couple of times but if explaining to some person your clean architecture takes more than half hour, probably it&rsquo;s something wrong there. So we can say probably it&rsquo;s some not very accurate metric but more or less like this.</p> <p><strong>Miłosz</strong> [48:06]: So overall, don&rsquo;t ignore developer experience. Think this would be the summary. Yes. I think it&rsquo;s very important because it&rsquo;s basically what everyone will be working with daily. So you just don&rsquo;t make a religion out of it at the end of the day and don&rsquo;t try to use arguments like this is not clean. It&rsquo;s the worst worst kind of discussions you want to have. You don&rsquo;t want to have them.</p> <p><strong>Robert</strong> [48:39]: It&rsquo;s probably worth mentioning to also set some expectations. So if you have everything in your HTTP handler your dependency injection can be as simple as it doesn&rsquo;t exist. If you have clean architecture, it&rsquo;s more complex because you need to inject more things. But again, it should be manageable by doing it by hand and okay, it depends on the language a lot. So with Milosz, we are mostly working with Go where there is it&rsquo;s compiled language. So it&rsquo;s a bit harder to do some magical dependency injection. But it&rsquo;s actually quite good because because you need to do it. So in our opinion, it&rsquo;s easiest to do it by hand. And if you&rsquo;re in the position when it&rsquo;s too complex to do it by hand, probably it&rsquo;s showing you the problem with your maybe clean architecture, maybe architecture in general, but it should be doable by hand. In other languages, we know that it depends. So we also worked a lot in PHP where it was working automatically. Sometimes it was nice but it was harder to debug with more complex dependency tree.</p> <p><strong>Miłosz</strong> [49:48]: There are some libraries usually. I&rsquo;ve seen someone in chat mentioned fx. We haven&rsquo;t used this one, but we used wire from Google. It was okay. But currently, we just wired wired it up manually in in the main function, basically. And it it grows. It&rsquo;s big. But most of the time, that&rsquo;s not an issue, really. You just fix it and if it compiles you&rsquo;re good to go.</p> <p><strong>Robert</strong> [50:18]: And I think it&rsquo;s one risk maybe that some people maybe don&rsquo;t see at the first sight that but, you know, it&rsquo;s something making your it&rsquo;s something making your super complex dependency tree easier to handle. It&rsquo;s actually opening some kind of gate of hell because you can do we can make this dependency tree more complex if something is helping you with that. Are going deeper and deeper into.</p> <p><strong>Miłosz</strong> [50:42]: Those layers within layers. Interfaces within interfaces.</p> <p><strong>Robert</strong> [50:46]: If you are doing it by hand probably you sooner see that something is wrong there.</p> <p><strong>Miłosz</strong> [50:53]: It&rsquo;s also a good general rule to start fixing the pain once you start feeling it because if there is no issue maybe you don&rsquo;t need all these solutions.</p> <p><strong>Robert</strong> [51:03]: And if you are starting to blame dependency injection framework probably the problem may be not in the dependency injection framework but between laptop and wall.</p> <p><strong>Miłosz</strong> [51:17]: Tough pill to swallow, but sometimes, yeah.</p> <p><strong>Robert</strong> [51:20]: Yes. Firmino is actually mentioning that even in Java, dependency injection can be easily done by hand but developers could use using auto wired from Sprint. Yep. That&rsquo;s true. But again, we&rsquo;re also not that against using such frameworks but again, you need to watch out. It&rsquo;s like with every tool. Okay, not every tool but there are some tools that are encouraging you to do bad things and probably dependency injection framework is on this list. But again, it all have its use cases like ORMs also, I would say. So many people also doesn&rsquo;t like ORMs because it can also lead to some issues. But, in my opinion, it&rsquo;s like with knife in the kitchen. I mean, you can hurt yourself with a sharp knife. But from other side, it&rsquo;s also pretty useful. I like sharp knives. I think you also like You need to just watch out. So that&rsquo;s it. It&rsquo;s much better than using blunt knife. And, yeah, the last thing that can lead to problems with clean architecture and we have some solution for that fortunately is using the wrong resources. So many resources that we&rsquo;ve seen in the internet are unfortunately missing the point of clean architecture and it&rsquo;s encouraging to use it in the wrong way that is far from being pragmatic. Probably doesn&rsquo;t help that many of those clear architecture hexagonal onion architecture they don&rsquo;t have super strict rules on how to implement that and it&rsquo;s nice because it&rsquo;s giving flexibility but it&rsquo;s not nice because it&rsquo;s giving flexibility on what you</p> <p><strong>Miłosz</strong> [52:58]: Have to decide for yourself. And it&rsquo;s exactly where this starts getting difficult because you&rsquo;re just starting out and there are no, definitive rules. And this is where you start thinking, okay, what&rsquo;s my perfect project structure? Which can be a trap. And I think many of those open source examples often miss the point or they show some very trivial domains like, you know, to do apps that have two entities and two endpoints. And if you take a look at this as such a simple domain implemented in the architecture, it will look like over engineering, of course, because it makes no sense if you can fit this into files to split it across the project. So you have to be careful of this.</p> <p><strong>Robert</strong> [53:53]: But there is also one, tricky part about this because so we have run repository in Go where we are showing how to you can connect everything. And I think it&rsquo;s, you know, fully worked application. It&rsquo;s not that complex still because it&rsquo;s still some more pet project even if it&rsquo;s fully functional. But also, if project is too complicated, it&rsquo;s also hard to grasp the most important parts of that. So it&rsquo;s it&rsquo;s all about the balance. So as mentioned multiple times, at the end of the we&rsquo;ll give you links to the useful resources. But, yeah, it&rsquo;s important to just use those resources.</p> <p><strong>Miłosz</strong> [54:35]: Yeah. I see an interesting question in the chat. So does clean architecture fit in projects with lots of tiny and simple microservices or better fits in a monolith application?</p> <p><strong>Robert</strong> [54:51]: It depends. There is no silver bullet.</p> <p><strong>Miłosz</strong> [54:54]: Yeah, what&rsquo;s a simple microservice? I guess that&rsquo;s that&rsquo;s the question. I I hope if you have, a monolith, it is split into modules just like those microservices, and then it&rsquo;s pretty much the same answer. If if the module or service is complex enough, to benefit from Cleanark, maybe a good idea to implement it. If it&rsquo;s truly trivial and crowd domain, probably not.</p> <p><strong>Robert</strong> [55:24]: Yeah. But I think there is one risk that I&rsquo;ve seen multiple in my life. You&rsquo;ve seen it as well. So because microservices are cool for splitting complex problems into smaller but the problem is that often people are splitting it to granular. So you have multiple so instead of having one service that can handle some, use case you have five, ten microservices that are handling that. It&rsquo;s okay. Maybe every service is pretty simple and you don&rsquo;t need to have clean architecture inside of that. But each time when you&rsquo;re implementing a new feature it takes changes in five or 10 microservices.</p> <p><strong>Miłosz</strong> [56:03]: Yeah, because because it&rsquo;s different entities, I guess. Yes. Yes. My favorite from the past is the Avatars microservice. Stored Avatars.</p> <p><strong>Robert</strong> [56:14]: And, you know, if you have sub granular microservices, okay, and then you split at your complexity but if implementing one functionality takes changes in 10 microservices, I&rsquo;m quite sure that it will be much easier to implement it within one service that is not maybe microservice, whatever microservice is. But doing it in one service will be easier. And okay, this one service will be more complex but Clean Architecture can help you with, handling this complexity because you don&rsquo;t need to split this complexity in multiple services. You can instead split it by layers. And it&rsquo;s making your so this is the thing that we&rsquo;ve done multiple times. So it was 10 microservicization operations. So moving multiple microservices into one service and obviously this one service was more complex but clean architecture was helpful to handle this complexity because we didn&rsquo;t have one big sum of everything but it was data in multiple layers and at the end implementing features was much easier. So, yeah, I hope it&rsquo;s useful.</p> <p><strong>Miłosz</strong> [57:21]: Yeah. Looking at the chat, I&rsquo;m gonna read the next one. So, Tony says, For me, the most difficult part of clean architecture is when implemented in a team and different people diverge in some details and the mess appears when little by little the project becomes less consistent. This is exactly the developer experience issues we talked about previously. I think it&rsquo;s very hard and probably what can help is having some team leader in place who can just cut the discussions and make a decision. Because otherwise, yeah, you can take long hours discussing, and it&rsquo;s natural that some people in your team will be like consistency more. There are many developers who strive for this consistency and like to have everything neat and tidy. And if someone starts doing something in a different way they will be annoyed and you want it to be the same way. And I think it&rsquo;s probably worth mentioning that,</p> <p><strong>Robert</strong> [58:33]: At least, I believe that democracy doesn&rsquo;t work in programming teams, unfortunately.</p> <p><strong>Miłosz</strong> [58:38]: Yeah, unless you have infinite time and can spend days discussing and voting.</p> <p><strong>Robert</strong> [58:42]: So probably sometimes usually something closer to totalitarianism works a bit better. But better to not overuse that obviously. But it sometimes helps with these discussions.</p> <p><strong>Miłosz</strong> [58:55]: It&rsquo;s a tough balance, I would say. You want some discussion with the team but you also need someone to say, &lsquo;Guys, let&rsquo;s just focus on important stuff.&rsquo; But it&rsquo;s very hard to make those know, exceptions because if everything is consistent, why would you use a different way in this in this case? You know, maybe it&rsquo;s just more easy this way. Maybe it&rsquo;s fine. But if you do advanced then, no, people exactly what what the comment says people might think that, oh, okay. It doesn&rsquo;t need to be consistent so let&rsquo;s just do whatever.</p> <p><strong>Robert</strong> [59:38]: And then also, if you&rsquo;re not a team leader but team leader is doing a bit of totalitarianism when you are going too deep into some discussions also don&rsquo;t be mad at this person because we&rsquo;ve been in this position and, you know, it&rsquo;s also not that hard. But well, after you are in this position, sometimes you see that it&rsquo;s easy to overdo some discussions and that&rsquo;s it. So sometimes at the end of the day, it&rsquo;s better to have something that works, but it&rsquo;s not perfect, but it works because sometimes you can throw it out. Probably you can listen more in the previous episode about code quality.</p> <p><strong>Miłosz</strong> [01:00:13]: Yeah. I think it&rsquo;s one of the most difficult parts of implementing clean arch. We don&rsquo;t know definitive answers here.</p> <p><strong>Robert</strong> [01:00:24]: Yeah, there&rsquo;s also a nice comment about frameworks. Frameworks like Rails that define a clean directory structure seems to be harder to apply that architecture and I would say it&rsquo;s still possible. So I don&rsquo;t have that much experience with Ruby on Rails but I worked more in PHP with PHP frameworks like Symfony or whatever and those frameworks usually okay, it requires some effort but usually you can hide them in some layers. So mostly those frameworks are about your ports but it&rsquo;s also sometimes a bit application layer but it&rsquo;s doable and it&rsquo;s often worth doing but often it&rsquo;s also not worth doing. So it&rsquo;s getting probably hard to give some universal advice here but you can do that in general. And sometimes it may be beneficial for more complex domains because those frameworks are nice and we&rsquo;re discussing it in the previous episode also about frameworks. But long story short is that they can give you some velocity at the beginning but later it may be harder to do some more context scenarios. And if you went with this framework structure all in, it&rsquo;s much harder to customize that as and sometimes, you know, the product after a couple years, it&rsquo;s still within the framework and we work sometimes in such companies and it&rsquo;s even impossible to get rid of this framework because it&rsquo;s so integrated with your code. And if somebody will split that in the screen architecture way, it will be maybe possible to replace that and maybe just not use it at all. And it will be possible to develop this, product further. But often, unfortunately, it&rsquo;s something like that. And it&rsquo;s super hard to develop the product further because it&rsquo;s so limited. But it&rsquo;s probably worth mentioning that often those projects were successful because at the beginning they&rsquo;re they&rsquo;re managed to have free fast velocity and win the market.</p> <p><strong>Miłosz</strong> [01:02:22]: Yeah. That&rsquo;s that&rsquo;s the benefit of using them. But if you can introduce even some, you know, thin domain layer or something that&rsquo;s separate from from the framework, that might be helpful if you want to move on. Not to use everything it gives you. But, of course, then then it&rsquo;s kind of, you know, defeats the purpose of of using the framework in the first place.</p> <p><strong>Robert</strong> [01:02:52]: Right. So we talked about, what are the common issues of clean architecture, what are the contraversions, but I think there are also some upsides that we&rsquo;ve seen multiple projects and some reasons why this maybe was invented because it&rsquo;s like with many techniques like it wasn&rsquo;t invented for no reason.</p> <p><strong>Miłosz</strong> [01:03:14]: Yeah, exactly. So the why behind it is important. So maybe let&rsquo;s start like we did, yeah, with the burning it down with separation of concerns. And, you know, it&rsquo;s the reason we we want to separate those concerns is this this logic independent of implementation details. Right? And, basically, the what it gives you is you&rsquo;re you&rsquo;re able to easily replace one implementation with another, which we talked about about mocking. That&rsquo;s one use case. Super useful. But also you can replace it with something completely different like changing the database.</p> <p><strong>Robert</strong> [01:04:16]: It&rsquo;s quite interesting because you can do that, but how often are you doing that?</p> <p><strong>Miłosz</strong> [01:04:19]: Yeah, that&rsquo;s a good question. It&rsquo;s often a counterargument I hear about clean architecture or interfaces in general. Like, yeah, yeah, you talk about how it&rsquo;s easy to change from one database to another, but in reality, it never happens. But it happened to us recently, so that&rsquo;s one example when it was useful. So we switched from Firestore to Postgres, and using Zaprouted was quite easy. Another nice example we did, in the past was starting with in memory implementation. So creating a domain domain code with some very simple, in memory implementation of the database. And then later when we know this is what we want we can just replace it with SQL or whatever implementation. It&rsquo;s also quite nice.</p> <p><strong>Robert</strong> [01:05:22]: Yes, but I think it&rsquo;s also worth mentioning that this is not the end of the architecture. So the end goal so it&rsquo;s designed. So if you can replace your database or port easily. It means that, okay, you did a good job with implementing architecture, but it&rsquo;s only showing you that it&rsquo;s done properly. But it&rsquo;s not the end goal in general.</p> <p><strong>Miłosz</strong> [01:05:51]: Yeah. In reality, you&rsquo;re not going to be changing the database every few weeks. So it&rsquo;s it&rsquo;s not why you do it. But I think but it gives you this nice separation of, you know, the implementation details from from the interesting code in your in your project.</p> <p><strong>Robert</strong> [01:06:11]: Yeah. And why it matters? So it&rsquo;s mostly because of, cognitive overload. So as I mentioned a bit earlier, so when you have multiple microservices that were too granular, you are joining it into one service. If you would be all in your handlers probably it will be very complicated. But if you&rsquo;re splitting it in multiple layers, it&rsquo;s allowing you to so as long as you know how the clean architecture works, it&rsquo;s it&rsquo;s making you a bit much easier to reason about the code base because you see what are the entry points of your applications, what use cases are supported, and how you&rsquo;re interacting with the database. If it&rsquo;s everything here in your HTTP controllers, for example, it&rsquo;s super hard to understand that because you can have some query that is called in some weird way, but it&rsquo;s hard to backtrack how it&rsquo;s called.</p> <p><strong>Miłosz</strong> [01:07:03]: Yeah. And and usually it spills over somehow to your to your logic.</p> <p><strong>Robert</strong> [01:07:07]: And it&rsquo;s nice if you have some transaction, but often it&rsquo;s not.</p> <p><strong>Miłosz</strong> [01:07:15]: And if you don&rsquo;t have the separation of any kind, it&rsquo;s also very easy to lock yourself in with some framework or library because, you know, you just scatter the library method calls all over your domain code. And if you want to migrate later you have all those places you need to change which is much more complex.</p> <p><strong>Robert</strong> [01:07:39]: And there is one interesting comment about abstracting the database layer, that it makes sense when you need to support different databases. So you need to choose what to deploy. And it&rsquo;s a very good point. And once in our life, we actually did something one level higher. Long story short, we worked, with the product that needed to be deployed in two ways, as microservices and as monolith. And it wasn&rsquo;t some it wasn&rsquo;t some crazy architectural idea. It was a pragmatic thing. So it would have, two deployments. So one in physical, servers and physical device and one in the cloud with the kind of simulated device. And because of that, it needed to be deployed in this way. And architecture was quite cool because of that because it was a matter of having two set of ports and adapters. One for the cloud deployment and one for the more bare metal deployment. And it worked Mhmm. Pretty nicely. And it thanks to that, it was manageable to do this.</p> <p><strong>Miłosz</strong> [01:08:39]: Yeah. That&rsquo;s quite quite a specific approach, but that&rsquo;s a good fit for for clean architecture in general.</p> <p><strong>Robert</strong> [01:08:46]: Don&rsquo;t do that. You probably don&rsquo;t need to do that, but, yeah, sometimes it&rsquo;s useful. Alright, so the first thing was the cognitive overload. But the second thing is that help is much easier with clean architecture is testing. And like cognitive overload which is important for a more complex project with bigger team, etcetera etcetera. The same goes for testing because when your, project service or whatever is small, probably doesn&rsquo;t matter what you test. I mean, you can probably just test it end to end and you can do that. But if you are working in the application that have thousand of use thousands of use cases, a lot of complex interaction with your database, testing it end to end is impossible because probably most of the use cases you cannot trigger with the public interfaces and it will be super super slow. It&rsquo;s especially, true for applications where the domain layer is very complicated because you can have even thousands of use cases and test cases and running it with the database, it will be just super slow. So mocking the extra level is the must because in other, I mean, obviously, you can wait a week for running test suite and I know that such application exists but probably it&rsquo;s not something that you would like to do.</p> <p><strong>Miłosz</strong> [01:10:09]: Yeah, and often having parallel tests in the same database is not often trivial to do. You can have conflicts between the use cases and so on.</p> <p><strong>Robert</strong> [01:10:19]: At some point it will be slow. I mean, obviously you can scale it horizontally but Yeah.</p> <p><strong>Miłosz</strong> [01:10:24]: There&rsquo;s often this argument that you can you have Docker, right, or test containers. You can just spin it and run all on this infrastructure. And it&rsquo;s true. That tests are so critical, you know, path of local development and CI that you probably want them the fastest you can.</p> <p><strong>Robert</strong> [01:10:45]: Exactly. So for example, if you&rsquo;re changing some important functionality, you don&rsquo;t want to then wait ten minutes every time when you change and change that. You would like to wait at most one second and see. Okay. It works. Oh, no. And thousands of unit tests are running and checking that. But it&rsquo;s the thing that it&rsquo;s again important for bigger projects, not all projects. If your project can just have end to end tests, it&rsquo;s fine. I mean, we worked with some projects. The main project that we&rsquo;re working on now in Tridos Labs, the Academy, is at the at the beginning we just have end to end tests for a pretty long time. It was just enough. So</p> <p><strong>Miłosz</strong> [01:11:25]: Yeah. I mean, if we were if we had unit tests, it would be even better. But sometimes you have to optimize for delivery speed. Exactly. So that&rsquo;s what we did. Yeah. Speaking about tests, maybe we can just quickly talk about how we speed them. If you have multiple layers, you you probably don&rsquo;t want the same testing approach for for all of them. So, for example, as we mentioned, for the domain layer, unit tests are are great fit because it is purely logic. There is no usually little dependencies that you can mock out, and you can have many many test cases that run fast using just viewing tests. That&rsquo;s that&rsquo;s pretty nice. And on the other hand, the adapters usually are a good fit for integration tests. So this is where we run some Docker database, for example, and just test the repository or the client or whatever it is in in separation from the domain. Right. And for the entry points and the application layer, we usually have little tests here, if any, because if all it does is translates one layer to the other and orchestrates things, it&rsquo;s probably not no big benefit in testing it.</p> <p><strong>Robert</strong> [01:12:52]: And I think it&rsquo;s actually one common issue that we&rsquo;ve seen multiple times and I think we forgot about mentioning that. But, yeah, if you have domain layer and application layer, running a lot of unique tests for your application layer probably doesn&rsquo;t make sense, especially if you&rsquo;re mocking everything around because it&rsquo;s</p> <p><strong>Miłosz</strong> [01:13:10]: Yeah. There are all kinds of tests. Especially if you have those mocking libraries that let you assert whether a function has been called, then your your test becomes become basically an application of your code. And every time you change anything, you have to go to a test and adjust it and basically gives you zero value.</p> <p><strong>Robert</strong> [01:13:31]: Yeah. I think it&rsquo;s actually great symptom, let&rsquo;s say, of why you have the bad tests. So when you are copying your logic of your logic in application from application error, for example, literally to the test. It&rsquo;s it means that it&rsquo;s something wrong with your tests. Yeah. And especially if everything around is mocked. But I think there is one nice, type of tests that are solving this problem nicely.</p> <p><strong>Miłosz</strong> [01:14:01]: Which is?</p> <p><strong>Robert</strong> [01:14:02]: Components tests that I think at least from my experience not many people knows of but long story short, idea of component tests is that you&rsquo;re mocking everything from external word and usually you&rsquo;re using so you&rsquo;re running your application in for example, Docker, you&rsquo;re mocking all external words or external APIs, everything. You have just your database in the Docker and maybe and everything else is mocked. And you&rsquo;re calling public APIs of your application and that&rsquo;s it actually. So, it&rsquo;s different from end to end tests because you&rsquo;re not integrating entire application. You are just having actually stops for the external words and it&rsquo;s nice balance because you don&rsquo;t need to, have super big environment to integrate everything with the downside that obviously you may mock some wrongs in the wrong way. But this is this standard problem with mocks but it doesn&rsquo;t mean that you shouldn&rsquo;t have end to end tests that are integrating everything. But you can cover much more cases than in end to end tests because you can define behavior of, the external APIs plus you&rsquo;re also checking the integration of everything. So it&rsquo;s covering a bit.</p> <p><strong>Miłosz</strong> [01:15:18]: Yeah. Probably the biggest benefit, Maybe the benefit, but the characteristic is that you don&rsquo;t call other services in inside those tests. So you have to somehow stop them or use numerators or something like that. Because you you want to test this one service end to end basically basically not the entire infrastructure around it. So it&rsquo;s also quite fast thanks to this.</p> <p><strong>Robert</strong> [01:15:46]: Yes. And it&rsquo;s much easier to also run local because if you are running end to end tests with entire infrastructure, it&rsquo;s not possible to run it on your laptop. And we&rsquo;re big fans of being able to run as much as you can locally and test against that because it&rsquo;s much easier to and faster to iterate over that.</p> <p><strong>Miłosz</strong> [01:16:05]: But no one enjoys, like, another team service breaking the test URL locally. It&rsquo;s the worst experience. It&rsquo;s not productive. So if you want to learn more about, testing this architecture, we have also a blog post about it, and we will link it in the description. Maybe another, like, like, less technical benefit I like about clean arch is, how it changes your mental model over time. So it gives you this kind of consistency in the project, so it&rsquo;s easy to find what you look for. For example, if you if you want to add a new API entry point, API method, or message handler, you know, to which directory to, you know, to go and which files to edit.</p> <p><strong>Robert</strong> [01:17:10]: And this is actually the often the counterpoint of many people that&rsquo;s, yeah, I&rsquo;m looking on this clean architecture project and I have no idea where everything is. Come on, spend fifteen minutes to understand where it is.</p> <p><strong>Miłosz</strong> [01:17:23]: Yeah, if the project is big enough and you work with multiple people on it, and this investment in learning, you know, the structure probably pays off, especially if it&rsquo;s consistent with how you do other projects in the team. And I think it also gives you this if this mental model spreads among everyone in the team, it gives you this nice vocabulary to to use. So, you know, when you discuss how to implement on some feature and someone says, okay. We need SQL adapter. Then everyone knows what is this, and it&rsquo;s quite clear how to implement it. So it&rsquo;s it&rsquo;s very easy to, to discuss the the project.</p> <p><strong>Robert</strong> [01:18:14]: And then what&rsquo;s useful in this because I know that it may take some time what layer you can import to what layer and works forbidden. The good, the good news is that there are leaders that can do that usually. So, for Go, there is go clean arch linter I written a couple years ago and it seems that it&rsquo;s pretty useful for many people. I guess it have probably around thousand stars in GitHub so probably it&rsquo;s useful. Often you have also linters I think in Java like, ArchLint or something like that that you can define some, I think it&rsquo;s in Java and PHP. So you can define what are the modules interaction. So, yeah, it&rsquo;s recommended to check for that, and it&rsquo;s pretty useful to because you can ensure that the dependency between, layers is properly handled.</p> <p><strong>Miłosz</strong> [01:19:10]: Yeah. In in Go, it&rsquo;s especially useful to have this clear direction because you you can have import cycles. Your code won&rsquo;t compile otherwise. So if you have some, you know, some clear way to to import between packages, it becomes just easier to stick to one way and don&rsquo;t worry about it again. So that&rsquo;s that&rsquo;s quite nice. Maybe maybe the next one would be what I also like about cleanarch is how it becomes easy to extend the application over time. And it&rsquo;s a it&rsquo;s a bit similar to this idea of having, you know, great architecture from the start. But I think it&rsquo;s a bit different because you don&rsquo;t really need to have all the use cases. But thanks to this clear structure, for example, it&rsquo;s easy to to add new SQL implementation or or or, for example, to to change one database query with another. That&rsquo;s that&rsquo;s faster. And the impact is very low because I mean, it&rsquo;s the the plus radius is small because you you don&rsquo;t touch multiple files and areas. You just change this one little part and it works. So you don&rsquo;t need to think of all use cases in the beginning, you just adapt over time with new adapters, with more application use cases, And this way you avoid side effects. In contrast, if you have this huge HTTP handler with everything in it and you make some changes to it, you don&rsquo;t really know what can happen. That&rsquo;s, you know, the worst scenario you can have when you work with a project you don&rsquo;t know well, and you see this huge method of thousands of lines, let&rsquo;s say, and you make some edits but, yeah, who knows what happens next. We don&rsquo;t really know this. It can affect many, many areas.</p> <p><strong>Robert</strong> [01:21:25]: And, yeah, the last upside that it&rsquo;s coming into my mind is the one that I think many people doesn&rsquo;t think about, but we used it pretty heavy in previous teams. So it&rsquo;s how how it helps you to split work.</p> <p><strong>Miłosz</strong> [01:21:41]: Oh, yeah.</p> <p><strong>Robert</strong> [01:21:42]: Basically, the idea is that you can plan some work so you have some feature to implement. You know that you need to implement it pretty quickly as usual. It will be great to parallelize your work as much as possible. And how we&rsquo;re usually approaching that was, agreeing on the interfaces between layers and just splitting the work. So, for example, we had some feature to implement. So we agreed that, okay, for example, we need such interface to integrate interact with database. This is more or less what we need to have in the domain layer and we could split this work for four people, for example, and each person was responsible of implementing one layer and it was quite simple after they agreed on one interface. And thanks to that, implementing one feature instead of taking, I don&rsquo;t know, three weeks, it was possible to finish it within one week including deploying that. And it was most important for features that we needed to just implement quickly. And this created some nice separation for each person and they were sure that we&rsquo;re not, not conflicting that much. And, obviously, sometimes there were some changes into the interfaces because, well, you will not be always sure always right from the beginning. But assume that you are talking to each other.</p> <p><strong>Miłosz</strong> [01:22:58]: Yeah. But I can just it gives you this nice vocabulary to use. So, for example, when you when you split, the feature into smaller tasks, someone can say, okay, I will take care of the HTTP layer. And it&rsquo;s super easy for them to, you know, create this API definition that will just do nothing in the beginning but will be merged, can even be deployed, and everyone else can integrate with it.</p> <p><strong>Robert</strong> [01:23:24]: And you can also split by complexity often. So, for example, you have some junior person, some principal person, and some mid developer. And more or less you know how much complexity will be in each layer. So usually, for example, your HTTP layer shouldn&rsquo;t be that complex usually because sometimes you have. Sometimes your domain logic will be very complex. Sometimes it will be quite simple. But for example, adapters will be complex or vice versa. And it&rsquo;s also very helpful to kind of assign work properly.</p> <p><strong>Miłosz</strong> [01:23:56]: Yeah. And it helps helps you create smaller tasks and PRs which is also a great thing. If you have, you know, a one file change in SQL adapter, I can review it in two minutes probably or something like that versus this whole feature where you need to focus on everything else. I think it&rsquo;s quite unique overall, this approach. Mhmm. Because I think in most teams, you would expect to see something like, Okay, Robert, you&rsquo;re supposed to work on this feature now. So see you in two weeks and you will show us what you have. And then you will come up with a PR that&rsquo;s, like, I don&rsquo;t know, 4,000 lines long. And then everyone spends reviewing it two days.</p> <p><strong>Robert</strong> [01:24:47]: And you have 100 comments like why you did like that or I don&rsquo;t like this. Exactly. I think it&rsquo;s even a good situation because often you will also struggle to find somebody who will do your reviews because this is the problem often when you are not working as a team but a set of individual stuff. Yeah. Your, goal is to your goal is to implement feature. It&rsquo;s not a team goal. And if you are working at it as a team of four, for example, or five, you have always people to review that and work is just going much, much, much faster. Yeah. But important disclaimer again, it&rsquo;s for, as we mentioned earlier, it&rsquo;s for more complex projects with bigger team. Again, for a full team, it&rsquo;s not a big team but it&rsquo;s not two or one person team, obviously. And we assume that it the features are complex enough to take one week of four people. So Yeah. Also important to keep in mind because, you know, sometimes there are features that one person can implement within one week. And using four people to implement that with green architecture, it will be just over engineering probably, and it will be just waste of time.</p> <p><strong>Miłosz</strong> [01:25:55]: Yeah. But it&rsquo;s good to keep in mind it&rsquo;s possible to to speed work, actually. I think it&rsquo;s not the the default for sure in the industry and somehow we accept that, you know, it&rsquo;s normal to just have someone sit on a picture for two weeks and then review what they did. Or other two weeks? Yeah. It&rsquo;s really not that productive. And sometimes if you have the layers especially, you can have three people or four or two work on the same thing and you know, do it in parallel on different areas. It&rsquo;s super effective because you have those small small PRs, smaller, bus radios of of the changes. You can deploy early often and all those good things.</p> <p><strong>Robert</strong> [01:26:42]: Yeah. And I&rsquo;m not sure if you remember but I remember this feeling like just work flowing.</p> <p><strong>Miłosz</strong> [01:26:48]: Yeah.</p> <p><strong>Robert</strong> [01:26:50]: Yeah. It was just pleasure because it was super effective and even very complex features were able to, you know, deploy it within one week. So it was cool.</p> <p><strong>Miłosz</strong> [01:27:01]: Another benefit that&rsquo;s kind of not relevant to Clean Arch, but you have those small subtasks in each feature and you can see how it moves on the board every day, you don&rsquo;t really need stuff like estimations maybe because, you know, whoever is responsible for the product or any stakeholder can just take a look at the board and see, okay, five out of eight subtasks are done in two days. So probably in two days they will be done with this. Okay.</p> <p><strong>Robert</strong> [01:27:33]: It requires the team to have a pretty big, let&rsquo;s say, engineering culture to achieve that.</p> <p><strong>Miłosz</strong> [01:27:39]: Yeah.</p> <p><strong>Robert</strong> [01:27:41]: It&rsquo;s very highly recommended to at some day work in such a team.</p> <p><strong>Miłosz</strong> [01:27:46]: Yeah. I think that goes a bit beyond clean arch. But I think clean arch is what really enables it. If you don&rsquo;t have this clean separation in the code of different layers, then it&rsquo;s very hard to split work because you will just keep getting, you know, in like, merge conflicts are the probably the least issue, but just conflict of who who works and what and how it impacts each other&rsquo;s work.</p> <p><strong>Robert</strong> [01:28:19]: Yeah. Yeah. Probably it&rsquo;s a topic for a separate episode how to do this splitting because it&rsquo;s also not obvious how to agree on this, let&rsquo;s say, one week work and everything like that. But definitely we&rsquo;ll touch on it at some point.</p> <p><strong>Miłosz</strong> [01:28:34]: So we talked about the benefits, talk about talk about the issues. The the question that&rsquo;s asked very often when someone asks about architecture is Can we just do it simple?</p> <p><strong>Robert</strong> [01:28:49]: And I think it was even mentioned on the chat.</p> <p><strong>Miłosz</strong> [01:28:51]: Yeah. Especially on on the chat, I think it was related to Go, which is a very good point because Go is seen as this simple language, you know, in contrast to Java, I guess, or C++ or whatever you want. And this is true. But does it mean that clean architecture has no place in Go? So, I think, you know, believing that if you keep things simple, it will save you is kind of lazy thinking, and it can work well if the problems you solve are simple. But if you start modeling something complex, it can it can be worse than having some more complex architecture in place.</p> <p><strong>Robert</strong> [01:29:47]: Yeah, and I think we&rsquo;re lucky enough to work in different kind of projects and I think it was nice to try different approaches. Try to do as simple as possible as over complicated as we can to try some techniques and, yeah, I think it&rsquo;s nice to showing that this balance is not simple, but it&rsquo;s not like you can always do simple way. It would be great always to go with the simple way. But, well, unfortunately, most of the applications are okay. Maybe not most, but many applications are not that easy. To make it even worse, I think probably we may end up in the situation when those simplest applications can be handled by AI because it&rsquo;s not complex. I mean, it it can already handle some simple scenarios, but the deeper problems, well, it&rsquo;s a bit hard to reason let&rsquo;s say reason AI, but you know what I mean.</p> <p><strong>Miłosz</strong> [01:30:47]: And it&rsquo;s hard to define what simple code is too. And, of course, everyone wants to create simple code. I I don&rsquo;t think anyone will tell you that they don&rsquo;t want to do it. But, for example, in Go, in the beginning especially, it was, I think, a seamless Flat structure of a project was considered simple. So probably you don&rsquo;t want to create any packages at all or just a few and then use these files in them. Works well for a standard library. That&rsquo;s a good idea if you write a standard library. But good luck managing this project with 10 people. When everyone creates files they want, I don&rsquo;t know. How do you create some consistency then? That&rsquo;s the hard part.</p> <p><strong>Robert</strong> [01:31:38]: How do you manage conflicts? So when you have more people working on one service and you have everything in one package, it&rsquo;s a much bigger chance that you will conflict on something.</p> <p><strong>Miłosz</strong> [01:31:48]: Yeah. So it&rsquo;s very, very hard to not be naive about simplicity. But if your team is two people, we are just alone, maybe three people, manageable. Or if you can just fit the entire code in your head.</p> <p><strong>Robert</strong> [01:32:05]: Or you have 10 times more microservices than people in your team. You laugh but probably you remember some projects where it was the case.</p> <p><strong>Miłosz</strong> [01:32:16]: I see the solution. You just need to have one file microservices and then it&rsquo;s not an issue anymore.</p> <p><strong>Robert</strong> [01:32:23]: Very recommend checking but, yeah, long story short, unfortunately, it&rsquo;s it can give you some impression that it&rsquo;s simplifying things. But again, implementing one change in 10 services, it&rsquo;s no way that you will be able to work efficiently.</p> <p><strong>Miłosz</strong> [01:32:42]: So basically, complex projects needs need proper modeling and can be approached with this primitive structure. But of course if you use some very simple domain then it makes sense to keep it super simple.</p> <p><strong>Robert</strong> [01:32:59]: Yeah, and I also noticed that many people think about clean architecture in terms of product structure like you have the structure, multiple directories, but I think it&rsquo;s not the most important part about clean architecture. So it&rsquo;s more about the mental model. So I would say that interior you can probably have clean architecture with one package because it&rsquo;s more it&rsquo;s harder to manage obviously because you probably will have more conflicts. It&rsquo;s harder to ensure that you are not interacting between the layers. But, well, in theory, probably, you can do that. But I don&rsquo;t see a really point in doing that. But, again, it&rsquo;s not about the structure of the product project. It&rsquo;s more about how you&rsquo;re splitting, functionalities in your application. And I think it&rsquo;s also pretty consistent with the Go philosophy where you should have one package with one responsibility. And in some way, it&rsquo;s compatible because you can have HTTP package that is responsible for handling HTTP. You have domain layer that is responsible for the domain layer. You have adapters that are interacting with the outside world. So I would say that it&rsquo;s very compatible with the approach of the language.</p> <p><strong>Miłosz</strong> [01:34:15]: eah. Yeah. If you put it like that, it&rsquo;s clearly this. Maybe it&rsquo;s the issue of being dogmatic but the other way around. So if you are too dogmatic about being simple, it can also hurt you.</p> <p><strong>Robert</strong> [01:34:33]: It&rsquo;s quite ironic. I mean, both sides of the barricade are doing the same issue. So they&rsquo;re dogmatic but just in the opposite direction. But they&rsquo;re making exactly the same issue.</p> <p><strong>Miłosz</strong> [01:34:50]: So it&rsquo;s probably good to remember the the basics and to understand why you do it in the first place. So don&rsquo;t don&rsquo;t do it because you want your code to be clean, but to, you know, better organize work in in a complex project that will grow over time will be there in a few months or years, and you have many people working on it. That&rsquo;s probably good good idea to use in architecture or some flavor.</p> <p><strong>Robert</strong> [01:35:21]: Yeah. But if your team are two people, it&rsquo;s easy to go in the wrong direction of the spectrum and over original things and make it just too complicated. So not see any benefits of clean architecture and just pay the price. That is usually not that big if you will not over engineer that because, again, it&rsquo;s not adding that much overhead if you know what you are doing, but still worth worth keeping in mind.</p> <p><strong>Miłosz</strong> [01:35:52]: Similar to the, question of should I write high quality or low quality code that we discussed in the first episode. Right? So if you if you have two person team, not, you know, developers who work together for a long time, and they trust each other, know how they work, and so on, you probably don&rsquo;t need that many roles in place. But if you have a fresh team that just started working together of 10 people, you probably want more more guidelines. So there is some consistency in the project, so they can just work better together. So pick also depending on the team and how how much mature it is. What about some alternatives to clean architecture?</p> <p><strong>Robert</strong> [01:36:45]: Well, so we already touched a bit on microservices. So as we talked earlier, clean architecture is useful with the handling complexity, making testing things easier. So it sounds like what microservices are promising you. But please keep in mind one thing that we touched multiple times because this is very often the issue that we have seen that people just went too far with microservices and you have more microservices than, people in your team much more.</p> <p><strong>Miłosz</strong> [01:37:19]: And it&rsquo;s usually Pretty common.</p> <p><strong>Robert</strong> [01:37:21]: Yeah. Yeah. And usually, it&rsquo;s ending up in the situation when implementing one functionality takes changes in 10 microservices. So I&rsquo;m not saying that microservices are bad, but they are bad if you are splitting them too granularity. And I think it&rsquo;s one of the sign that it&rsquo;s done with how many services you need to touch to implement our functionality or, for example, how many services you need to test one entire functionality. So, for example, if testing one use case that is handled requires 10 microservices, it probably may also mean that, there are two granular.</p> <p><strong>Miłosz</strong> [01:37:59]: Mhmm. Yeah. And there&rsquo;s also the vertical slices architecture, but we didn&rsquo;t use it as much yet. We probably have nothing smart to say about it. Yeah. So maybe next time, the next episodes.</p> <p><strong>Robert</strong> [01:38:15]: Or maybe somebody has some experience with that, so you can write in the chat.</p> <p><strong>Miłosz</strong> [01:38:26]: Okay, since we work with Go most of the time maybe we can just give some hints about Go specifically using clean arch. I think my favorite part is the implicit interfaces in Go. If you don&rsquo;t use Go, in contrast to languages like Java, for example, you don&rsquo;t need to specify that your your structure implements an interface. You just need to define the methods with the same signature as the interface, has. And while some approaches like ports and adapters, such as these separate ports layers for interfaces because you have to keep them somewhere to implement them, in Go you can implement them just close to where they are used, which is, I think, super convenient and also in line with the idea. So you can just define an interface next to the constructor with the dependency it needs. And basically, it&rsquo;s the only place that knows about the interface in your entire project.</p> <p><strong>Robert</strong> [01:39:42]: And I think it&rsquo;s also nice that you can make it work with interface segregation principles. So you can just use some subset of the interface that you&rsquo;re implementing in the consumer, let&rsquo;s say. So you don&rsquo;t need to always use entire interface everywhere. You can just use some subsets of the metal. And it&rsquo;s useful when you are doing some refactoring and, for example, changing some external adapter and you it&rsquo;s nicely visible where it&rsquo;s needed.</p> <p><strong>Miłosz</strong> [01:40:12]: I think someone mentioned it in the in the chat before. But this anti-pattern of creating, interface with exactly the same methods as the implementation. So you often see something like, you know, interface and then an implementation called the name of interface implementation, something like that. It makes no sense. And on the other hand, if you have the smaller interfaces, many things become easier like testing because your mock just becomes one method instead of 10. That&rsquo;s a super nice feature of Go that plays well with clean arch.</p> <p><strong>Robert</strong> [01:40:51]: The second problem that is pretty high a big problem is the fact that many sources are a missing point of clean architecture. It probably doesn&rsquo;t help that it&rsquo;s not defined that strictly, let&rsquo;s say. So it&rsquo;s adding a big, big, interpretation area. Let&rsquo;s see. So, yeah, look for good sources. So if you&rsquo;re writing in Go or you would like to write in Go, we recommend checking our blog obviously because we covered it already a couple years ago and it&rsquo;s still totally relevant. I think even if you&rsquo;re writing in a different language, it&rsquo;s the many ideas are common so it might be worth checking, what we&rsquo;re writing there. But remember that different languages have a bit different, let&rsquo;s say, idioms and some things are done a bit differently. So we know it from Go because many people try to port, clean architecture or other techniques directly to go from Java, for example, and it was terrible idea because of what you mentioned a bit earlier. Not, for example, in Go, we have duck typing. And because of that, it&rsquo;s implemented a bit differently. And it&rsquo;s worth keeping it in mind.</p> <p><strong>Miłosz</strong> [01:42:11]: Yeah. And probably the biggest anti pattern you can see in the examples are the single single model used for everything. So So you can have a structure that&rsquo;s used for, you know, the domain logic, but also it it has database and JSON tags on them, which probably means, you know, it&rsquo;s also used for storage and for API responses.</p> <p><strong>Robert</strong> [01:42:35]: And it probably means that we&rsquo;re getting into this problem that implementing an architecture in such a way will lead to over engineering. Because if you are able to use the same model everywhere, it means that probably you don&rsquo;t need such architecture and you could just use it everywhere and you don&rsquo;t need to have all this strange mapping, etcetera. You can just simplify it a lot. But if so if you don&rsquo;t see benefit so you&rsquo;ll not see benefit from using this approach. But if you have more complex models and your domain model is not exactly what you&rsquo;re returning from the API that works in multiple domains like, for example, financial, domain. The benefit of having different models in different places is is there.</p> <p><strong>Miłosz</strong> [01:43:29]: I think it&rsquo;s time to summarize and Yeah. Go to the Q&amp;A. So remember about the basics. Right? You want to separate domain logic, whatever it means for your app, from implementation details. That&rsquo;s the that&rsquo;s the primary idea of of clean arch or and similar patterns. And try to focus on the on this idea, on the why behind it, not on the how or or directory structure for for starters.</p> <p><strong>Robert</strong> [01:44:10]: Yeah. And there are a couple things that the architecture should give you and if those things are not met, probably it may mean that it&rsquo;s not maybe you don&rsquo;t need to use Clear Architecture like managing complexity with the bigger project, easier to test and write faster tests, you can parallelize your development, you can insulate your changes, and improve navigation. If you don&rsquo;t have those problems, maybe you don&rsquo;t need clean architecture, maybe your application is not that complex. Maybe, you know, just having everything in your HTTP controller it&rsquo;s fine. It&rsquo;s totally fine. We&rsquo;re doing it in some cases when because even within one application you can have Twin architecture in some places and doesn&rsquo;t have it in other places and it&rsquo;s totally, totally fine. And No use approach. Will save you a lot of time.</p> <p><strong>Miłosz</strong> [01:45:07]: Yeah. On the other hand, there are some pitfalls. So it might be confusing at first for people who don&rsquo;t know it yet. So take time to explain it to your colleagues so that you are on the same page. And watch out for common issues like using too many interfaces or too many layers or just over engineering the the basic idea, and watch out for using it everywhere, as Robert just said. So if you if you use modular monolith, I hope you do, not all not all modules need to follow the same architectural patterns. You can use something different in different modules. It&rsquo;s fine.</p> <p><strong>Robert</strong> [01:45:52]: And, yeah, to analyze your benefits and pitfalls of your application, it&rsquo;s worth putting to this equation the team size, your experience, and, well, watching out to not be too dogmatic when you&rsquo;re doing that.</p> <p><strong>Miłosz</strong> [01:46:09]: A good tip for whatever you do. So we&rsquo;ll link some materials in the description. Check out our blog post on clean architecture, on testing strategies as well. And there&rsquo;s one about integrating clean architecture with DDD and CQRS. In Go. And also the one about the repository pattern is probably quite relevant to clean architecture in general. And all those posts are just a series that describe the white workers repository, the example project. So you can check it out too.</p> <p><strong>Robert</strong> [01:46:51]: Let&rsquo;s take a look on comments. So we already covered some of them but let&rsquo;s check what we have missed. Where are comments? Oh here, okay, so let&rsquo;s start from the beginning. Yeah, so again a couple comments that people are missing working with me, Osh. Good sign. So usually we&rsquo;re using clean architecture so, you</p> <p><strong>Miłosz</strong> [01:47:28]: Know. Yeah. I think it&rsquo;s the only pattern that I&rsquo;ve heard of people using beyond, the teams we worked on. So, you know, we met one year later and they told us, oh, you know, I spread it in my team and now we also follow Clean Architecture&rsquo;.</p> <p><strong>Robert</strong> [01:47:47]: Or I missed the project that we worked together. The current one is so.</p> <p><strong>Miłosz</strong> [01:47:53]: But I mean, the the architecture is a pattern I&rsquo;ve heard mentioned multiple times, but it&rsquo;s probably also the the only one mentioned so often. So that must be a sign. Right? Looking at the bottom, there&rsquo;s a question about how clean architecture differs from hexagonal. And I would boil it down to naming with a different approach. I would say we use a combination of both. So it&rsquo;s not important. I would say it&rsquo;s implementation details.</p> <p><strong>Robert</strong> [01:48:36]: But, yeah, I think it&rsquo;s all about you have some inner layers and outside layers and inner layers doesn&rsquo;t know how outside layers are implemented.</p> <p><strong>Miłosz</strong> [01:48:47]: Yeah. And all those variants call them a bit differently but the core core idea is the same.</p> <p><strong>Robert</strong> [01:48:56]: Yeah. And, you know, my theory about this is that, you know, the multiple techniques are reinvented every 15 years because, you know, there is a hype cycle of every technique that at the beginning it&rsquo;s, hype time. Oh, yeah. It&rsquo;s solving all of our problems. So everybody is going into that without understanding how to do it. Multiple teams are implementing that. Some people in very dogmatic way. So people are starting to hate this technique and the word is spreading that, this is a bad technique. Don&rsquo;t use it ever. It&rsquo;s super bad. It&rsquo;s overcomplicating stuff, etcetera. And less and less people are using that. And after some time, somebody is thinking, oh, okay. It&rsquo;s so nice idea but let&rsquo;s it&rsquo;s the name is already so spoiled that we need and we need to name it a bit differently and so people will be ederabad about that. And, you know, it sounds absurd, but it works.</p> <p><strong>Miłosz</strong> [01:49:58]: So are are you suggesting we need a new name? Yes. And your standards too?</p> <p><strong>Robert</strong> [01:50:04]: Yeah. So if you have any idea how to name next, iteration of clean architecture, listed on the chat.</p> <p><strong>Miłosz</strong> [01:50:16]: There&rsquo;s a question about tests. We kind of mentioned this before. So do you ever add tests on the application layer and how do you go about it? Mocks or fakes in memory by implementing the interfaces? So it depends how many logic do we have there. If we have a separate domain layer, we usually have almost no test in the application layer because it becomes mostly orchestration of things. Yeah.</p> <p><strong>Robert</strong> [01:50:45]: And it should be covered by the component tests, basically. So you are calling the component tests and it&rsquo;s going over all layers and checking that everything is integrated with that problem.</p> <p><strong>Miłosz</strong> [01:50:55]: But sometimes it makes sense if there&rsquo;s some some logic you can test.</p> <p><strong>Robert</strong> [01:50:59]: Especially when you don&rsquo;t have domain layer, for example, because sometimes you may not have enough logic to have a separate domain layer. So you may have more logic in application layer. But in this case, what will for example, what you can do can you can have functions that you can just, test independently without entire words.</p> <p><strong>Miłosz</strong> [01:51:19]: Yeah. And in this case, in memory fakes work well. And this this is where having small interfaces is super important.</p> <p><strong>Robert</strong> [01:51:27]: Yeah, and what&rsquo;s important there is ensuring that you are mocking this IO stuff that I mentioned a bit earlier. So don&rsquo;t mock your, I don&rsquo;t know, logic, let&rsquo;s say, because it your test will be just mocking some subset of logic and they will not give you a lot. So, you know, mock your database, mock your mock your external dependencies, don&rsquo;t mock your logic.</p> <p><strong>Miłosz</strong> [01:51:52]: And we are big fans of writing the fakes, manually. I know many people use mocking libraries but we find them usually not that helpful.</p> <p><strong>Robert</strong> [01:52:04]: At least in Go. I think it may be, let&rsquo;s say, problem of Go because of its compiled language and often those libraries are using a lot of reflection. For example, I remember that in PHP there were some nice libraries for testing but in PHP it&rsquo;s a bit easier due to not being compiled language but, well, upsides and downsides.</p> <p><strong>Miłosz</strong> [01:52:29]: And sometimes it also creates, like, brittle tests. If you you start testing the function calls, for example, then you often need to change them if you change anything in the code.</p> <p><strong>Robert</strong> [01:52:46]: And, yeah, the problem with mocking libraries so it&rsquo;s another thing in the bucket of things opening the gate of hell so it&rsquo;s making easier to mock too much. In other words, it should be painful to mock things because you can mock too much.</p> <p><strong>Miłosz</strong> [01:53:03]: Yeah. That&rsquo;s what Todd in the comments mentioned before that if you need a mocking library, you are mocking too much.</p> <p><strong>Robert</strong> [01:53:09]: Exactly.</p> <p><strong>Miłosz</strong> [01:53:10]: Yeah. That&rsquo;s kind of the idea. I think that Yeah. And Firmino had some experiences with architect and his Gomock lib. I don&rsquo;t remember which one gomock is. Is it the one using reflection or the one using generation?</p> <p><strong>Robert</strong> [01:53:29]: I guess so. I already forgot it intentionally.</p> <p><strong>Miłosz</strong> [01:53:36]: Yeah. I just found another one from Todd. The first step I use when trying to introduce layers to people who don&rsquo;t use them is to explain that the application shouldn&rsquo;t be tightly coupled to the API. So, yeah, it&rsquo;s a great approach, and also shouldn&rsquo;t be coupled to the adapters, the database, for example. I guess the hard part of explaining it is explaining someone why it is good to decouple them.</p> <p><strong>Robert</strong> [01:54:05]: And if you don&rsquo;t see any reason again maybe you don&rsquo;t need the architecture because if you have a CRUD application and you are mapping your CRUD structure before your at least at least four layers, in worst case six layers or how many, it&rsquo;s just a waste of time.</p> <p><strong>Miłosz</strong> [01:54:28]: Yeah. And sometimes it&rsquo;s also not that trivial to explain the benefits because you can use the typical reasons like if you ever want to change the database it will be easy. But it&rsquo;s easy to counter this with. We won&rsquo;t. Which is also true.</p> <p><strong>Robert</strong> [01:54:54]: Don&rsquo;t use this argument.</p> <p><strong>Miłosz</strong> [01:54:57]: Yeah, I mean, it&rsquo;s it&rsquo;s hard to have a discussion that that it doesn&rsquo;t start to be about this dogmatic approach. Our code needs to be clean. Our code needs to be simple. So watch out for that. Yeah, but this is definitely a nice idea.</p> <p><strong>Robert</strong> [01:55:17]: It&rsquo;s also probably worth mentioning that, you know, you won&rsquo;t be able to always work in the teams following this approach, which is fine, but also don&rsquo;t be a rebel that is just using clear architecture in your small project. I mean, if you&rsquo;re working the team that, okay, you&rsquo;re not following this, don&rsquo;t be the rebel. I mean, sometimes disagree and commit, it&rsquo;s sad, but well, maybe it&rsquo;s not sad. It&rsquo;s something that you need to use in you should work in a team, not alone.</p> <p><strong>Miłosz</strong> [01:55:48]: It, I guess, depends on how well the project is doing. You know, if I know it&rsquo;s hard to, you know, disagree and commit if you see all the issues that following some pattern could solve. That&rsquo;s that&rsquo;s the hard part, you know.</p> <p><strong>Robert</strong> [01:56:05]: But you will be not always able to just convince everybody. It&rsquo;s different.</p> <p><strong>Miłosz</strong> [01:56:10]: Yeah. Just you need to know why you you want to use some pattern. Right? We discussed this in the first episode that we had this one project where everyone was super happy to work with because it it used, many cool patterns, and we kept talking about it years after it. And the project was shut down because it had no paying users.</p> <p><strong>Robert</strong> [01:56:40]: Well, at least it was great.</p> <p><strong>Miłosz</strong> [01:56:42]: So that&rsquo;s the hard part. You need to be productive and avoid getting into this big ball of mud issue. But sometimes it doesn&rsquo;t matter that much as well.</p> <p><strong>Robert</strong> [01:57:00]: Yeah. So at the beginning, we have a couple of comments that somebody is missing working with you. We have also one comment that please come to my team and make team follow architecture and solid. I use elixir, and I don&rsquo;t know if it&rsquo;s the language or the community, but, oh my, everything is coupled.</p> <p><strong>Miłosz</strong> [01:57:21]: I don&rsquo;t think it&rsquo;s the language. I think it&rsquo;s just it is what it is.</p> <p><strong>Robert</strong> [01:57:27]: I think it&rsquo;s also something connected. I mean, the mindset and language. I will not mention language names started with r or</p> <p><strong>Miłosz</strong> [01:57:38]: But Yeah. But, you know, even if in Go, which is supposed to be a simple language and, I don&rsquo;t know, somehow make it magically better or your applications easier to develop, We know the reality is it&rsquo;s just as easy to create something coupled in Go than in other languages. Maybe with less magic. That&rsquo;s the good part.</p> <p><strong>Robert</strong> [01:58:01]: That&rsquo;s true.</p> <p><strong>Miłosz</strong> [01:58:02]: But, for example, you know, the struct tags in Go make it super easy to couple layers and it&rsquo;s not that easy to see if you don&rsquo;t know what you&rsquo;re doing.</p> <p><strong>Robert</strong> [01:58:12]: But I think it&rsquo;s something in that, you know, a lot of Go people are very open minded because it&rsquo;s, you know, fresher language and in general, you you know, if somebody was eager to learn some new language and try something different, it&rsquo;s at least from my experience, it many cases, it was real open minded person and it was somewhere there. But, yeah, about about the team, you know, if you are not able to change the team, change the team. It&rsquo;s sometimes like it and it&rsquo;s just sometimes not possible to change what you&rsquo;re doing in the team. So, you know, it might be worth, you know, leveling you up in your skill set and trying to find a better place. Unfortunately, there are a lot of great places and hard bad places to work in. It&rsquo;s also a bit unfortunate that we know that it&rsquo;s hard to join good teams that are, you know, following some good practices. But from other side, you know, also grass in your neighbor&rsquo;s garden is always, more greener than yours. But yeah. It you can always find a better team. But for that, you often need to</p> <p><strong>Miłosz</strong> [01:59:20]: There&rsquo;s also there&rsquo;s a risk there that you start thinking that the team using clean architecture is better than yours. Yeah. Who knows? Maybe they over engineer it so much you would hate it even more.</p> <p><strong>Robert</strong> [01:59:34]: Yes. Yes. But yeah, about the quality again, I recommend checking the episode about how to write bad code qualities. We&rsquo;re also touching a bit on how to set expectations because well, none of the team is perfect. There are always problems and but, yeah, there are better and worse teams.</p> <p><strong>Miłosz</strong> [01:59:57]: Okay. Are we out of questions?</p> <p><strong>Robert</strong> [02:00:01]: I think so.</p> <p><strong>Miłosz</strong> [02:00:04]: I think we are just on the two hour mark.</p> <p><strong>Robert</strong> [02:00:08]: Perfect.</p> <p><strong>Miłosz</strong> [02:00:14]: Cool. So thank you everyone for joining us. If you want to not miss the next episode, you can subscribe to our newsletter on our blog, and we will let you know. You can, of course, subscribe to YouTube, our YouTube channel, but we&rsquo;re not sure if they will notify you, so our newsletter is the best one, the best place to get notified. And you will also not miss posts, our notifications of our our our posts, so that&rsquo;s another reason. Yeah. Leave us any comments. Tell us if you liked it and what you can improve. If you are listening to this after the live happened, you can just comment under the video. And if you listen to this on any podcast application, give us five star rating.</p> <p><strong>Robert</strong> [02:01:17]: Yeah. Unfortunately, we&rsquo;re ruled by the algorithm so your, thumb up or flight style review can help us a lot and I think this is the best way how you can support us and well, help us to record more episodes.</p> <p><strong>Miłosz</strong> [02:01:37]: Which, will happen in two weeks.</p> <p><strong>Robert</strong> [02:01:39]: Yeah. About what we are talking in two weeks? I don&rsquo;t remember.</p> <p><strong>Miłosz</strong> [02:01:42]: I think how to learn quickly.</p> <p><strong>Robert</strong> [02:01:45]: So I think it will be another episode that is nicely connected to what we were talking earlier. And, yeah, if, Nicholas, you were mentioning that you would like to join your team, We are not looking for a job, but what can help is, well, learn more and being able to join better teams. So this is the tactic that we definitely recommend.</p> <p><strong>Miłosz</strong> [02:02:09]: Or spread some ideas within your your team.</p> <p><strong>Robert</strong> [02:02:13]: Yep. That&rsquo;s true. So, yeah, the next episode is about that. Hope you see.</p> <p><strong>Miłosz</strong> [02:02:19]: Thank you everyone for joining us today. Thank you, Robert.</p> <p><strong>Robert</strong> [02:02:22]: Thank you, Miłosz. And see you in two weeks. Bye bye.</p> <p><strong>Miłosz</strong> [02:02:25]: Bye bye.</p>When you shouldn’t use frameworks in Gohttps://threedots.tech/episode/when-you-should-not-use-frameworks/Wed, 19 Mar 2025 17:00:00 +0000https://threedots.tech/episode/when-you-should-not-use-frameworks/<h2 id="quick-takeaways">Quick takeaways</h2> <ul> <li><strong>Frameworks promise productivity but often lead to issues</strong> as projects get larger and more complex.</li> <li><strong>The Go community prefers small, focused libraries over frameworks</strong> due to Go&rsquo;s design philosophy influenced by Unix principles.</li> <li><strong>Watch out for risks using frameworks</strong> like vendor lock-in, deprecation, and costly migrations that can take months.</li> <li><strong>Explicit code is more maintainable</strong> than magic framework abstractions.</li> <li><strong>Choose your approach based on project size and maturity</strong> - frameworks might work for prototypes, while modular libraries are better for long-term projects.</li> </ul> <h2 id="introduction">Introduction</h2> <p>In this episode of the No Silver Bullet podcast, we discuss <strong>frameworks in Go and when they&rsquo;re useful or problematic</strong>. We talk about why the Go community generally avoids frameworks compared to other languages, and how small, modular libraries are often preferred in Go development.</p> <p>We share our experiences with frameworks across different projects, including tradeoffs between productivity and long-term maintenance.</p> <h2 id="notes">Notes</h2> <ul> <li><strong>Model-View-Controller (MVC)</strong>: Pattern first described in the 1970s for Smalltalk, still widely used today.</li> <li><a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FUnix_philosophy" target="_blank"><strong>Unix Philosophy</strong></a>: design concept created by Ken Thompson (also a Go creator) promoting small programs that do one thing well and work together.</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthings-to-know-about-dry%2F" target="_blank"><strong>When to avoid DRY in Go</strong></a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank"><strong>Watermill</strong></a>: Our event-driven library for Go designed to not be a framework.</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F" target="_blank"><strong>Repository Pattern</strong></a>: Our blog post that is still relevant and frequently referenced.</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fcli" target="_blank"><strong>tdl</strong></a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2Ftools%2Fpq" target="_blank"><strong>pq</strong></a> - the CLI tools we mentioned.</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F" target="_blank"><strong>Clean Architecture</strong></a>: The topic of our next podcast episode, a design approach that helps maintain separation of concerns.</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank"><strong>Wild Workouts</strong></a> : Our example Go codebase demonstrating clean architecture.</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbest-go-framework%2F" target="_blank"><strong>The Best Go framework: no framework?</strong></a></li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>The happy path is easy enough, but <strong>the happy path is usually not the hard part of software</strong>. We often overvalue how much effort the boilerplate requires.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p><strong>Framework knowledge tends to become out of date.</strong> You can spend days or weeks learning something about a framework, but it can be outdated. And if you switch to another programming language or company, a lot of effort that you spent to learn stuff will be just wasted.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>It&rsquo;s more important to learn even-driven architecture because you learn the theory behind it and how it works in general - it transfers better to whatever you will do later. <strong>Focus on timeless skills like how to split modules in your application, how to make it decoupled, how to write business logic</strong> so it&rsquo;s easy to read and modify.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>The Go language is heavily influenced by Unix philosophy - write programs that do one thing and do it well, write programs that work together. It&rsquo;s visible in Go&rsquo;s standard library. This is why <strong>Go promotes building independent components that you can connect together</strong>.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p><strong>You need to be careful not to go too far with foundations.</strong> It&rsquo;s better to start with some modular libraries, have some reasonable setup in place, but don&rsquo;t go too crazy with it. Most of the time you&rsquo;ll need to refactor the project anyway, whatever you do, because it can change drastically.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>One big decision at the beginning may cost you six months of work later. Understanding if something is tightly coupled to your application is simple - just <strong>think about how easy it would be to remove it</strong>.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D0s">00:00:00 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D74s">00:01:14 - What is a framework?</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D361s">00:06:01 - The hidden costs of frameworks</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D643s">00:10:43 - Go&#39;s approach to frameworks</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D765s">00:12:45 - Unix Philosophy and Go</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D953s">00:15:53 - Framework risks</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D1130s">00:18:50 - Early experiences with frameworks</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D1499s">00:24:59 - Framework limits</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D2060s">00:34:20 - Migrating out of a framework</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D2278s">00:37:58 - Should you use a framework?</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D2558s">00:42:38 - Simple vs. complex applications</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D3083s">00:51:23 - Alternatives</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D3632s">01:00:32 - Framework developers vs. universal knowledge</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D3973s">01:06:13 - Summary</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D99eNvrtz1ss%26amp%3Bt%3D4229s">01:10:29 - Q&amp;A session</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Robert</strong> [00:00]: What if framework you choose today costs your team months extra work next year? Choosing a framework at the beginning of the project can greatly impact the project in the future. We have been in projects where relatively small decisions in the beginning led to many months of work a couple works later. Have you ever considered why the Go community does not favor using frameworks in go? Does it mean that using frameworks is always bad in Go? I&rsquo;m Robert.</p> <p><strong>Miłosz</strong> [00:31]: And I&rsquo;m Miłosz And this is No Silver Bullet podcast where we discuss mindful backend engineering. We spent almost 20 years working together in different projects and teams, and during that time we&rsquo;ve seen that following advice like &ldquo;always do X&rdquo; or &ldquo;never do Y&rdquo; rarely works and can limit your growth. And in this show, we share multiple perspectives that will help you make smart choices and level up into the principal engineer level.</p> <p><strong>Robert</strong> [01:01]: If you have any follow up questions, leave them on the chat. So we will try to answer them at the end or maybe even during discussion if we will have enough time.</p> <p><strong>Miłosz</strong> [01:14]: So we talk about frameworks today in Go, but also in general, maybe let&rsquo;s start with what we consider a framework. So we are all on the same level. First of all we focus on backend frameworks. So we don&rsquo;t talk about the frontend world today. Just the backend side of things. And in this episode we specifically focus on the Model View Controller framework or MVC, which is usually what most people consider a framework during the discussion of &ldquo;should I use a framework at all?&rdquo;, or &ldquo;which framework do I choose?&rdquo; So maybe, Robert, could you give us a TL;DR what MVC is?</p> <p><strong>Robert</strong> [02:02]: So MVC. So it&rsquo;s an interesting pattern that it&rsquo;s already pretty old, but well, I&rsquo;m kind of fan of some old patterns because I think often they&rsquo;re still pretty relevant. And the same thing is with MVC. I think for the first time MVC was let&rsquo;s say invented or written down in 1970s. So yeah, it&rsquo;s more than 50 years ago. So pretty long time, but it&rsquo;s still pretty valid.</p> <p><strong>Miłosz</strong> [02:29]: Like SQL.</p> <p><strong>Robert</strong> [02:31]: Yep, yep. And I think it was for the first time in Smalltalk language. That was also pretty interesting one. But we&rsquo;ll not go today about talk about Smalltalk. But yeah, and MVC. So it&rsquo;s acronym for Model View Controller. And the interesting thing is that the chance that you are using MVC is pretty high. If you are building an application that has a database and a user interface, and you are allowing to modify this data. So it&rsquo;s pretty high chance that you are using MVC. And it&rsquo;s because it&rsquo;s the industry standard on how you are building applications now. And very often it&rsquo;s actually MVC pattern. And the idea here in MVC is to split your model, view and controller. So model, this is your domain logic. Your entities that are handling data. View is we can say the user interface often and controller is responsible for orchestrating everything between this. I&rsquo;m not sure if you agree Miłosz, but probably it may be obvious for some single page applications, but I guess we can also say that even if you are building some react application, it&rsquo;s some form of MVC also, even if it&rsquo;s a bit different than how it was built.</p> <p><strong>Miłosz</strong> [03:56]: On a different level, but a similar idea. With the one model that is the source of truth of data, and then some parts of the application that react to it in some way. So that&rsquo;s similar concept I guess.</p> <p><strong>Robert</strong> [04:13]: And yeah, today we&rsquo;ll discuss MVC frameworks. So if you think about frameworks like Ruby on Rails, Django, Laravel, Symfony, Spring, all of them are MVC and are MVC frameworks because they have building blocks for building MVC parts. So you have some building blocks for models. Usually you have some building block for controller and you have some building block for view. So the idea is that they should simplify your life when you are building such kind of application.</p> <p><strong>Miłosz</strong> [04:45]: And there are some other libraries that we probably shouldn&rsquo;t consider MVC frameworks for this discussion. Like router libraries and in Go there is many of them, but you can also think of something like maybe flask in Python or I guess chi or echo in Go. So they give you the tools for working with HTTP messages and you get the views, but there is no model. Or you could do it yourself with some ORM framework maybe, but it&rsquo;s yeah, it&rsquo;s not out of the box compared to, for example, Django that gives you all of it.</p> <p><strong>Robert</strong> [05:31]: Django or anything what I mentioned earlier, like Ruby on Rails or Laravel, Symfony, etc. etc.</p> <p><strong>Miłosz</strong> [05:38]: From what you said so far, frameworks seem like a good idea, like the separation between model, view and controller seems like a separation of concerns. What we want most of the time and they do many things for us, they come with batteries included. So usually if something sounds so great, there must be some hidden cost, right?</p> <p><strong>Robert</strong> [06:09]: Yep. And frameworks are no different. So I went over websites of couple of frameworks to figure out actually what they are promising. And I have a list actually about things that are usually mentioned there like authentication and authorization, logging, observability, queues, database access or some sort of ORMs, dependency injection, sending emails, payment provider integration. And the idea is that it&rsquo;s all integrated. So for example, if you are using any component, it should be integrated with observability with queues. And the idea is that it should work together out of the box. I also checked some promo materials for those frameworks and some sound pretty nice. Like a &ldquo;complete ecosystem for web artisans&rdquo; or &ldquo;build and ship software with tools crafted for productivity&rdquo;. Sounds good. This one I like pretty much &ldquo;compress the complexity of modern web applications&rdquo;, or the last one &ldquo;optimized for happiness&rdquo;. It sounds a bit like some religious cult, you can say, but to be fair, well, spoiler alert it&rsquo;s not false. I mean, in some contexts it&rsquo;s meeting those requirements moments. Maybe happiness sometimes.</p> <p><strong>Miłosz</strong> [07:34]: It sounds good on paper. I think especially if you just start, if you bootstrap a project, especially before we had AI tools to bootstrap projects with. It could be quite cool to just call a couple of functions and have a website ready.</p> <p><strong>Robert</strong> [07:57]: And it&rsquo;s kind of &ldquo;wow&rdquo; effect. So you just spend five minutes on something and it&rsquo;s working. You can show it to your mom, &ldquo;Mom, look what I built, oh&rdquo;.</p> <p><strong>Miłosz</strong> [08:08]: Especially if you are a beginner, right?</p> <p><strong>Robert</strong> [08:12]: Yeah. So you don&rsquo;t need to have all this knowledge about how to connect stuff. And you can focus on things that matter at the end and are the most important. So building the useful things. And you are productive when you are doing that. So that&rsquo;s cool.</p> <p><strong>Miłosz</strong> [08:29]: And what I also like about it is that you can focus on writing the core domain parts of your software because for many web apps, there are probably, I don&rsquo;t know, 80% of the code that is written is very generic, right? It&rsquo;s what all web applications use, like database handling or HTTP routing and stuff like that. So it makes a lot of sense to focus on what&rsquo;s different in your software and not waste time on writing what someone solved already some time ago. But I think there&rsquo;s also this trap here that I&rsquo;ve seen many companies and projects have this dream of, you know, we will build all the foundations first. So we have this super custom framework or platform or whatever this means, and then we can just create new applications or microservices or whatever else, like a cookie cutter. So it virtually should take no effort in theory. And it sounds nice, but I think it&rsquo;s also a trap because it ignores the fact that most of the complexity in software is in the details, in the weird edge cases, which is, by the way, also probably the reason why AI tools are not that great yet. I mean, they are very, very nice for the generic cases, maybe for similar stuff for what frameworks are nice. But when it comes to those weird edge cases you need to handle in the real world, yeah, that&rsquo;s a bit harder.</p> <p><strong>Robert</strong> [10:18]: It&rsquo;s actually quite interesting that we can say in some way that AI is competition for frameworks, because, yeah, that&rsquo;s true.</p> <p><strong>Miłosz</strong> [10:26]: They all fall into this category of this dream of &ldquo;code with no code&rdquo; or &ldquo;software, with no code&rdquo; to create applications with no code like UML was supposed to be at some point maybe.</p> <p><strong>Robert</strong> [10:42]: But let&rsquo;s go back for a second to Go, because the title of this live podcast is &ldquo;When you shouldn&rsquo;t use frameworks in Go&rdquo;. So let&rsquo;s go for a second for Go, because now we covered a bit how it looks like in the different technologies. But it&rsquo;s worth mentioning that in Go it&rsquo;s quite different situation. So like in different languages that we mentioned you have some kind of well-established frameworks, Go is different. So there is no leading framework. And generally the community is against using frameworks. And this is interesting situation and I think one of the reasons for this situation is that well, Go is pretty new technology compared to other technologies. And well, building one big framework, it requires much more effort. If you compare it to just building small libraries that do one thing and yeah, it&rsquo;s just faster. So it may be probably one reason for that.</p> <p><strong>Miłosz</strong> [11:46]: And maybe also the, the libraries that we have now evolved over years. I remember like when we started eight years ago, there weren&rsquo;t that many libraries for anything but they started appearing over time and they grew and became more polished. But there was nothing that connected them all. Well, there were some attempts for sure, but I think there is no one standard framework right now that everyone uses. Maybe it&rsquo;s because also of how the standard library looks like and the Go philosophy of small packages that do one thing well. That kind of resonates if you think about the current ecosystem of open source libraries in Go. There are many smaller libraries that focus on doing just one thing.</p> <p><strong>Robert</strong> [12:44]: I think a good reason for that may be also the design of the language, so how the language was designed. So I think, I&rsquo;m not sure if anybody mentioned it explicitly, but I think it&rsquo;s visible that it&rsquo;s heavily influenced by Unix philosophy. So I suspect that for at least one reason that one of the creators of Go language is Ken Thompson, who is also author of Unix Philosophy and Unix Philosophy tells us to write programs that do one thing and do it well. To write programs that work together. And it&rsquo;s visible in Go standard library. If you look on how, for example, HTTP package is built, how io.Reader or io.Writer are built. So, you know, io.Reader or io.Writer is an excellent example of a small program, let&rsquo;s say, or interface doing one thing and you don&rsquo;t really care about how it&rsquo;s working under the hood. It&rsquo;s pretty similar. So if I would like to explain Unix philosophy for you, probably the best way will be mentioning how your terminal works. So you have things like cat, you have things like grep and you can connect it together and pipe that. And this is similar. This is actually this idea. And this is moved to Go heavily. And yeah, I think it may be also the reason why Go is kind of promoting the building independent components that you can connect together. And it&rsquo;s nice because for example, if you have http package, you can have multiple libraries that can understand HTTP request or HTTP response writer and interact with that. So it&rsquo;s promoting this, creating independent libraries. And for example you can have a library that is just giving you one HTTP middleware, and you can integrate it with the rest of the system.</p> <p><strong>Miłosz</strong> [14:38]: It&rsquo;s a great API design rule overall, no matter the programming language really. And it&rsquo;s nice that Go gives you those interfaces out of the box. So most libraries try to stick to them, which makes it easy to connect them together.</p> <p><strong>Robert</strong> [14:53]: And it&rsquo;s not something that I&rsquo;ve seen in different languages. So usually in different languages you have library that is doing HTTP, and standard library is not giving you that much. Or sometimes those frameworks or libraries are doing something totally incompatible. So it&rsquo;s just hard to have multiple libraries that are working together. And in Go giving these interfaces, it makes this much easier.</p> <p><strong>Miłosz</strong> [15:15]: That&rsquo;s actually a good point. I think Go is the first language I&rsquo;ve used that gives you this really powerful HTTP server. That&rsquo;s yeah, it wasn&rsquo;t ideal from the start, now it&rsquo;s a bit better. We usually use routing libraries anyway, but it&rsquo;s quite solid if you want to start with anything. I think most other languages, at least at the time Go was released, weren&rsquo;t that great. You had to just, you know, include some custom library. So why not use a framework if you have to do it anyway? And there were some attempts at creating Go frameworks, I think. And one risk of using them is that if there is no one big default framework like let&rsquo;s say Spring in Java, you have this risk of this framework becoming deprecated, and then you are stuck with some project that uses a framework that&rsquo;s hard to migrate out of. And I think there was go-kit. We&rsquo;ve seen used some projects.</p> <p><strong>Robert</strong> [16:28]: No, it was go-micro.</p> <p><strong>Miłosz</strong> [16:29]: Oh, true. Yeah. And I think there was also a very popular Gorilla router library that also became deprecated over time. So you can still use this of course, but it doesn&rsquo;t receive updates. So there is some risk that you might need to migrate out of it in the future. And you know, migrations are fun.</p> <p><strong>Robert</strong> [16:55]: And you know, if you are using frameworks, you have this framework that is integrating all MVC layers. So model, view and controller, it&rsquo;s much harder to migrate out because if you just have a library that is handling routing for you, it&rsquo;s kind of simple to get rid of this library. But if it&rsquo;s integrating everything more, I think model is probably the worst part, because if your model is integrated with views and controllers, it&rsquo;s very hard to get it out of your application. And yeah, and okay, somebody may say, okay, it&rsquo;s not happening, that libraries or frameworks are deprecated. You can check history of them, that some of them are quite dead, we can say. And there is also one example of licensing change. So go-micro from what I checked actually changed license and is no longer open-source license. So it&rsquo;s business source license. So the same license as Terraform is using and it&rsquo;s not open source. So you can read the code, but you can cannot use it like open source thing.</p> <p><strong>Miłosz</strong> [18:02]: Time to fork.</p> <p><strong>Robert</strong> [18:04]: Yeah. But remember fork the tag before changing license. So the thing what happened with with Terraform. But it&rsquo;s risky obviously. And in some cases it may just cost you a costly migration. And this is just one example.</p> <p><strong>Miłosz</strong> [18:20]: So someone could say it can happen with any library. Right. So why bother? But I think the difference is the smaller libraries are easier to to replace if it impacts just one area of your code. Using a different library here is quite easy, probably. Depends on how you do it, but hopefully. But if you do it as a core of your project, like a framework, then it becomes much, much harder to do it.</p> <p><strong>Robert</strong> [18:47]: Definitely. And if you go back for a while to discuss the promises of frameworks because, you know, we&rsquo;ve been there also a couple of years ago when we were starting programming and, and yeah, at this time we&rsquo;re working, to give you some context. So it was, I think almost 20 years ago. And at this point, the landscape of programming looked totally different. So, for example, if you know Laravel framework from PHP, it didn&rsquo;t exist yet. jQuery, that already sounds like super old technology, it was almost not adopted yet. And if you are talking about other PHP frameworks. So it was Symfony, but it was also not widely adopted. It was something pretty new. And when I remember that we use Symfony for the first time, it was actually quite great at the beginning because it made a lot of things that we were doing earlier by hand, and it just allowed us to save a lot of time. So I already mentioned authorization, logging, nice observability. It just worked of the box, and it was great. It also generated for us database models. So again, we were able to deliver stuff faster. But again we were also a bit less experienced. So for us it was like magic at that point. And I remember that before that we were just crafting some SQL queries by hand and it was super dirty.</p> <p><strong>Miłosz</strong> [20:22]: But this magic was also a bit unexpected for me when I first started using it. So I remember you tried to onboard me to Symfony. And you know, I was looking through the project and I couldn&rsquo;t find a SQL migration files and I asked you, where are the SQL files? You told me what? You don&rsquo;t use it. You just you just use models in PHP. And that felt weird for me at the time because you had to change the mindset. You have to just trust that framework, that it will do its job well. You don&rsquo;t really. I mean, you can dump the schema, of course, and inspect it, but usually just accept that there&rsquo;s some annotation you have to do and that&rsquo;s it. So if you would like to know how it works, the internals, that&rsquo;s a bit of a mindset change you have to do. And also, on the other hand, if you have someone who is used to working with Spring, let&rsquo;s say, we had developers who were Java developers and joined us to write in Go, and I remember they were confused about how we don&rsquo;t have a dependency injection library, and they were just mind blown similarity of, you know, how why do we do it manually? Why not just put a single annotation and it works? So it&rsquo;s a bit different concepts. You have to adjust.</p> <p><strong>Robert</strong> [22:07]: But I think it&rsquo;s also one downside that, you know, it&rsquo;s hiding this complexity from you. And sometimes those people were coming from Spring. They struggle because, okay, they need to write some query by hand. And it&rsquo;s like writing a SQL query is so hard. I didn&rsquo;t write any SQL query in my life earlier.</p> <p><strong>Miłosz</strong> [22:29]: That&rsquo;s maybe why, you know, &ldquo;no silver bullet&rdquo; is so difficult because we tend to adopt to one way of doing things when we started.</p> <p><strong>Robert</strong> [22:42]: But I think it&rsquo;s also showing that it&rsquo;s, you know, often frameworks are showing you that, okay, you&rsquo;re doing it in one way and people are not really thinking about doing some exceptions because for many cases, it&rsquo;s just enough to have generated model that does queries for you and ORM is doing for you, but probably 10, 20% of queries can be written by hand and it can be much more optimal. But people are often going in the direction that, okay, we need to do it with ORM and are going with some crazy stuff and also to ensure that it&rsquo;s properly shown somewhere with the view layer.</p> <p><strong>Miłosz</strong> [23:18]: Yeah, we like consistency. What can I tell you? I had the same experience as I just just mentioned. It&rsquo;s nice to work in a way you already know. And if you have to change it&rsquo;s problematic.</p> <p><strong>Robert</strong> [23:34]: But probably we can recap that as okay, frameworks obviously they have some downsides but well they are giving us a lot. So you may ask us okay why we are no longer PHP developers writing in Symfony. So it may be some reason that, well, it wasn&rsquo;t the best technology in our life and now we are a Go developers instead. So it&rsquo;s a question what went wrong. So I already mentioned that, you know, 20, 10 years ago the landscape was totally different. So it was much less open source tooling like for observability, logging, tracing and routing so you didn&rsquo;t have much choice because you could use Symfony or some magic PHP frameworks, or Spring maybe, or maybe some Python tools. So actually frameworks were giving this a lot for free, but now it&rsquo;s quite different because you have much more libraries, especially in Go that are open source. And they are using some open standards.</p> <p><strong>Miłosz</strong> [24:38]: Or even standard library like with HTTP in Go, you don&rsquo;t need a separate library for this and many other similar for stuff like, you know, it wasn&rsquo;t possible back in PHP. You know the standard library there was quite poor at the time.</p> <p><strong>Robert</strong> [24:55]: Oh yeah, especially compared to the Go one.</p> <p><strong>Miłosz</strong> [24:57]: So basically we learned that there is no free lunch. There is no silver bullet, you have to consider what you need to give up to use a framework. And I think it&rsquo;s more relevant if you think about the project in the long term. Because if you just start the project, as we said, you know, we just throw in a couple of function calls and you have a working website that&rsquo;s pretty cool. It feels like magic initially. But over time it becomes less cool because you start getting into those edge cases that the framework doesn&rsquo;t handle for some reason. And maybe what you mentioned about SQL queries is a good example of this. So often it&rsquo;s it just makes more sense to use a custom query to get some data you want, some complex query, instead of trying to use the models. I especially like how you know in Go the ORMs often have a separate language in the struct tags. So you have to learn a separate syntax that is not validated by the compiler. And I started to wonder, you know, why is it really that that helpful to use it.</p> <p><strong>Robert</strong> [26:34]: And you need to also do a PhD from how those magical notation works and it&rsquo;s not great.</p> <p><strong>Miłosz</strong> [26:42]: Yeah, exactly. So at some point it just becomes less flexible. It&rsquo;s easy to start. But then you need to add some extensions, maybe, you know, even in several components that use the framework and it&rsquo;s starts to get a bit tricky.</p> <p><strong>Robert</strong> [27:01]: Model is one thing, the flexibility of models. And if you are talking about MVC frameworks because, as we said at the beginning. So in most cases when we are talking about frameworks, it&rsquo;s MVC frameworks. So if it&rsquo;s MVC, it needs to support not only model and controller, but also view. And from my experience, very often the biggest problem is in the view because okay, if you are starting to build your simple application, just having a form and a form with some fields there with simple inputs, it&rsquo;s enough. But probably very soon you will find out that what frameworks are giving you out of the box, it&rsquo;s not really allowing you to express, let&rsquo;s say, every UX and requirement that you are receiving earlier. So yeah, it&rsquo;s also obviously often you can do workarounds or for example, not use the view part from framework. Just return your models from Rest API or from other site, you are losing a lot of advantages that the frameworks is giving you.</p> <p><strong>Miłosz</strong> [28:10]: You need to find this balance. Because if you find yourself spending more time on extending this, those missing edge cases and writing those workarounds, it&rsquo;s probably a good idea to do something else. Those queries for example. Right. So if you start fighting with those with this weird syntax of an ORM or a library, and maybe just give up and use a standard SQL query and should be good enough.</p> <p><strong>Robert</strong> [28:44]: And I think it&rsquo;s especially a problem when your project is becoming more successful and team is bigger, because if you are two people, team whatever, and your product is not that successful or widely used, it probably doesn&rsquo;t matter. Actually, you can take shortcuts and it will be everything fine. But well, if your product is becoming successful, all the problems are starting to pop up there.</p> <p><strong>Miłosz</strong> [29:08]: As usual, the happy path is usually easy enough, but the happy path is usually not the hard part of software. And we often overvalue how much effort the boilerplate requires. Like, it often it just seems boring to to write all this, you know, routing and database database queries and all this stuff that you do in every application. It&rsquo;s not really different. It doesn&rsquo;t differentiate you from other applications, but it&rsquo;s not really a big effort. Right? It&rsquo;s just boring. And now LLMs will happily do it for you. So it&rsquo;s even better.</p> <p><strong>Robert</strong> [30:02]: But from one side, writing it by hand is boring. But I would not say that it&rsquo;s super interesting to do workarounds for that. And I would say that it&rsquo;s much, much worse than.</p> <p><strong>Miłosz</strong> [30:14]: Oh, but sometimes, sometimes it&rsquo;s actually fun to do it. I have a story maybe I can share. So you remember in one project we had this entities stored in a database in, and we used one model for them in Go. And yeah, we had this issue that we didn&rsquo;t want to return the exact values we stored in the database to the front end side. So we had one model and we wanted something different in the views than just the database model. And we could just create a different model and map those fields between them. But that would be the boring part, right? You don&rsquo;t want to write this mapping.</p> <p><strong>Robert</strong> [31:11]: We are smarter.</p> <p><strong>Miłosz</strong> [31:13]: Yeah. So it&rsquo;s the usual question we get asked, right? Like, okay, I did clean architecture and I separated my models between database and HTTP, let&rsquo;s say. But what now? How do I automatically map those structures. Yeah. You don&rsquo;t. Like in this project. We exactly thought we are too smart to do it by hand. So we wrote a library that could do it for us using reflect and custom struct tags. And please don&rsquo;t do it.</p> <p><strong>Robert</strong> [31:13]: It was great.</p> <p><strong>Miłosz</strong> [31:52]: And since then, my rule is if you use a statically typed language and use reflection think twice. Is this really what you want to be doing?</p> <p><strong>Robert</strong> [32:05]: Yeah, probably second outcome is that don&rsquo;t be smart. Usually being smart as programmer, it&rsquo;s not the best way because you may become too smart. And your solution may be that smart that nobody will understand that. And it will take a lot of time to maintain it.</p> <p><strong>Miłosz</strong> [32:26]: This is actually my favorite part of this story is the why behind it. We just thought we are smarter than writing boring code and that led to more trouble. So you have to be careful around this.</p> <p><strong>Robert</strong> [32:48]: All right. One important thing. So I think in general, the discussion about frameworks that we have today, it&rsquo;s nicely connected to the previous episode that we had two weeks ago. So if you didn&rsquo;t have a chance to listen to that, I recommend it because in general it&rsquo;s often well, choosing framework is big architectural decision. So often if you do it at the beginning, it&rsquo;s quite hard to remove it later because as we said earlier. So it&rsquo;s a matter of coupling your models to rest of the application. And it&rsquo;s not like replacing your HTTP router or something like that. It&rsquo;s usually very tightly coupled, and it&rsquo;s just hard to remove that. And yeah, in previous episode we were discussing pretty deeply that it depends a lot in which stage of project you are or if you think this project will be successful in long term, because probably if you are doing some pet project or you have some small team and it&rsquo;s proof of concept, probably doesn&rsquo;t matter that much. But if you see that your project is taking traction and team will be bigger, it may be problematic to go out of this decision.</p> <p><strong>Miłosz</strong> [34:20]: So this assumes someone would need to remove a framework, right. Which usually doesn&rsquo;t happen if it&rsquo;s a big and proven framework that many people use. But with smaller frameworks it&rsquo;s a bit more risky. So some time ago we worked with this go-micro framework. You remember, for microservices.</p> <p><strong>Robert</strong> [34:46]: I remember.</p> <p><strong>Miłosz</strong> [34:47]: Across many teams, tens of microservices, and there was some issue with the protobuf version there. There was something custom.</p> <p><strong>Robert</strong> [34:57]: Yes, I think it was some custom implementation of protobuf that supported something extra, but at some point it was no longer maintained.</p> <p><strong>Miłosz</strong> [35:06]: Was it related to gogo/proto or not? Doesn&rsquo;t matter. But somehow there was a decision to remove it. And that&rsquo;s a long story short.</p> <p><strong>Robert</strong> [35:18]: Yeah, I think so. It wasn&rsquo;t like, okay, we need to remove that. I think it was leftover. Somebody decided to use it a couple years earlier. A year and couple years later we were in the project and it was some leftovers from that. And I think it was not maintained and it didn&rsquo;t support something. And it also have some weird edge cases. At some point we needed to get rid of it because it blocked something, something else.</p> <p><strong>Miłosz</strong> [35:44]: Possibly also back then Go modules weren&rsquo;t as great as is it now. And I think there were also some issues with this, but basically there was decision to get it removed and it went exactly how you would expect a framework removal to go.</p> <p><strong>Robert</strong> [36:04]: It to be exact. So it wasn&rsquo;t removing entire framework, it was just leftover from something that was there since a couple of years.</p> <p><strong>Miłosz</strong> [36:13]: I think it took man-months of, I don&rsquo;t know, half a year, probably in total effort from people from different teams because this spanned across teams, because it was related to communication between services. So obviously it&rsquo;s what connects the project together. Yeah. I remember when we finally got rid of it, there was a big maybe not event, but I remember celebration where everyone was super happy. I mean, I were too, but then you start to think about how dumb effort this was, you know, spending time on removing some obscure library that caused some issues, but, you know, shouldn&rsquo;t be there in the first place probably.</p> <p><strong>Robert</strong> [37:07]: I think it&rsquo;s an excellent example that some decision that happened a couple years earlier that have that big impact later. And yeah, it&rsquo;s not the only time when we&rsquo;ve seen that. So obviously it&rsquo;s hard to do all the decisions fine all the time. But yeah, the thing that I mentioned. So it&rsquo;s worth thinking like, okay, I&rsquo;m using this framework library or something like that, just in case if it will be deprecated, how I can remove that. And if the answer is okay, It will be super hard. Maybe it&rsquo;s worth thinking. Okay, maybe we should choose something different or how we can mitigate that. Or again, maybe again. It&rsquo;s a bit more in the previous episode about that, but maybe it&rsquo;s fine because maybe it&rsquo;s proof of concept.</p> <p><strong>Miłosz</strong> [37:58]: Probably hard to decide what is a good, you know, or maybe maybe a different way. What is what are some red flags that tell you stay away of this framework? Because I guess if someone uses a proven framework like Spring or Ruby on Rails or Symfony or whatever&hellip;</p> <p><strong>Robert</strong> [38:20]: Probably it would be not deprecated.</p> <p><strong>Miłosz</strong> [38:22]: Probably. Yeah. Because the community is big enough. That&rsquo;s one thing. If the company behind it would drop it, I guess you could expect the community to just fork it and maintain it over time.</p> <p><strong>Robert</strong> [38:36]: Or maybe not, because it&rsquo;s just not adopted enough and because I think it&rsquo;s the thing in Go. So in Go we have pretty, it&rsquo;s pretty big segregation of frameworks and there is no one leading framework. And maybe some people will do some updates, for example in the fork. But I would not expect that it will be super big updates. So it&rsquo;s risky in Go, I would say.</p> <p><strong>Miłosz</strong> [39:04]: Especially if the framework is created by one person or a small team.</p> <p><strong>Robert</strong> [39:09]: Or for example, some other risk that people are often not thinking. So if it&rsquo;s created by some VC backed company. So, you know VC capital is not free, it&rsquo;s not charity. So probably they will need to make money at some point. And we&rsquo;ve seen recently when it was big shift in trying to get money from multiple products and some licenses changed, like with Terraform, for example, and it&rsquo;s definitely a risk. Again, especially with things like framework that it&rsquo;s just coupling everything and it&rsquo;s hard to remove. So I can imagine that in many cases it&rsquo;s like, okay, let&rsquo;s better pay for that and forget.</p> <p><strong>Miłosz</strong> [39:58]: Or maybe let&rsquo;s just pay for the first year and then we&rsquo;ll migrate. And you know, this migration task sits at the bottom of your backlog, and one month before the next invoice, you realize, oh, we should migrate.</p> <p><strong>Robert</strong> [40:13]: Task: remove framework.</p> <p><strong>Miłosz</strong> [40:15]: Oh, let&rsquo;s do it next year. And then the next year, the company shuts down because they run out of funding. So yeah, there&rsquo;s a risk.</p> <p><strong>Robert</strong> [40:29]: If you are thinking about framework, it&rsquo;s also worth looking on the features list and assess if you need everything from this list. Because frameworks are usually providing many things out of the box, but very often you don&rsquo;t need to have everything out of that. For example, do you really need, I don&rsquo;t know, payment provider integration? Do you need ORM or many things from that? And it&rsquo;s a question if it&rsquo;s worth paying the cost of coupling everything. Or maybe it&rsquo;s just better to use something independent.</p> <p><strong>Miłosz</strong> [41:08]: And often frameworks have to be that, have to cover all those edge use cases because otherwise they are not useful because people use a framework and they have all kinds of requirements. I think it&rsquo;s even for authentication is a good example. I think there is this authboss in Go for authentication kind of framework. I don&rsquo;t know if maybe it&rsquo;s more a library, but I think it nicely shows how we can have so many approaches to authentication. If you want to create a library that&rsquo;s popular, you have to support all of them. Otherwise people will just skip it because it is not supported. But as a user, in contrast, you only probably want 10% of what it offers.</p> <p><strong>Robert</strong> [42:02]: And I think it&rsquo;s also making the complexity rising exponentially, because when you have multiple things like database models, logging, observability, payments, view controller and you need to have everything integrated. Adding one case, it requires you to add it very often everywhere. So obviously it&rsquo;s much harder to make it flexible because you are often focused on providing one path that works. But if you need some flexibility around it, just maybe not compatible with the rest of the components.</p> <p><strong>Miłosz</strong> [42:39]: And this one is often very, very useful. If you have a proof of concept app or just a trivial CRUD domain. Then having this happy path scenario and you know, one model for everything that&rsquo;s actually quite useful because you don&rsquo;t waste time on this mapping and so on.</p> <p><strong>Robert</strong> [42:57]: That&rsquo;s true. But from other sides. Well, experience tells me that often applications are not CRUD. I mean, I like to think about that. Okay. If your application is simple CRUD, maybe spreadsheet is enough.</p> <p><strong>Miłosz</strong> [43:13]: Exactly. That&rsquo;s that&rsquo;s the usual case, right? Like, why would you build custom software if it&rsquo;s that trivial? For stuff like this, this no-code solutions should be ideal.</p> <p><strong>Robert</strong> [43:27]: Or spreadsheets. I love spreadsheets.</p> <p><strong>Miłosz</strong> [43:29]: Or some of the more, more complex spreadsheets that are now quite popular.</p> <p><strong>Robert</strong> [43:36]: Oh, come on, you&rsquo;ve seen what you can do with Google spreadsheets and that entire startups can live on Google Spreadsheets, and it&rsquo;s fine for a while.</p> <p><strong>Miłosz</strong> [43:46]: Yeah, I think the biggest trap is if you think you have a CRUD domain, but you have not, and you start with this single model for everything, and then you realize it when it&rsquo;s too late. And we have a lot of articles about this on our blog, so you can take a look. There&rsquo;s this one to watch out for the DRY, don&rsquo;t repeat yourself principle, which is about this issue. So we will link this in the summary later. So you can take a look. And it&rsquo;s also about the mapping issue I mentioned before. And I see that Grzegorz in the chat asked about this mapping. So there is a mapping library in Java mapstruct and it is common to use it. So why can&rsquo;t we do it in Go. And how did we overcome the mapping problem. So the issue with the magic mapping we created was that it was just hard to maintain because there was a lot of reflect code, which is hard to grasp and test. And, you know, long term it will be just it would be easier to just map the structs manually. And that&rsquo;s what we did later in later projects. So that&rsquo;s how we overcame the problem. We didn&rsquo;t. We just kept separate structs and mapped one to the other when we had to. And I don&rsquo;t know, maybe it sounds boring or bad. It is quite boring, but there is a big plus of boring code. It&rsquo;s trivial to understand and test and change.</p> <p><strong>Robert</strong> [45:40]: And it&rsquo;s also compile time checked for that. So if you break something, you written it properly. Compiler will tell you that it&rsquo;s something wrong here.</p> <p><strong>Miłosz</strong> [45:51]: And today we have so many, you know, copilot tools that can generate it for you that it&rsquo;s even better than it was a few years ago. It&rsquo;s basically free. So that&rsquo;s you know, that&rsquo;s our approach right now. We prefer explicit to magic.</p> <p><strong>Robert</strong> [46:10]: And I think what I can recommend is just trying it because again, it may sound boring, but yeah, I would recommend trying that and just feel that it&rsquo;s not that scary. And actually it works quite nicely. And again, don&rsquo;t be afraid of boring code or boilerplate because again, we tried different languages also where it&rsquo;s more promoting some magic, some elegant code, but at the end later it&rsquo;s super hard to understand. Like Python is probably a great example that okay, it&rsquo;s in general allowing you to create nice interfaces that look really nice, like some decorators, but at the end you have no idea what&rsquo;s happening under the hood, and in Go, you just cannot do that. And it&rsquo;s very explicit. And you can navigate in the code up to system calls to understand what&rsquo;s happening. And it&rsquo;s very linear I would say. So there are just navigating and you see what&rsquo;s happening.</p> <p><strong>Miłosz</strong> [47:12]: But maybe there&rsquo;s also a good idea now to remind of the no silver bullet principle that now what we just said about this manual mapping and all of this is especially nice if you have if you work with a team in a bigger project and you know, we have you have to have some consistency and maintainability for the long term. And we talk about this in the previous episode. But again, if you have a proof of concept application or something you work on just yourself or want to try to build some MVP, then of course just doing this single model might be a better idea, maybe just simpler and easier in the short run.</p> <p><strong>Robert</strong> [48:02]: Yeah, I think it&rsquo;s a really good point. And usually where the problem is, it&rsquo;s when people are going into extremes, like always do it like this and it&rsquo;s it&rsquo;s usually the bad idea. And yes, you said so. It&rsquo;s also not like where you always using separate model for everything. No actually we often using one model for multiple things. But it&rsquo;s in case when the model is the same everywhere. So for example, or sometimes and sometimes we&rsquo;re also wrong. I mean, we&rsquo;re creating some two models for something. And we later see that okay, we&rsquo;re always mapping it 1 to 1. Maybe let&rsquo;s just make it one model and whatever. So it&rsquo;s important to be kind of mindful probably in that. And if you see that you&rsquo;re doing some stupid mapping and it&rsquo;s always the same. Because what we just said a couple seconds ago about this mapping manually, etc., it&rsquo;s good for the models that are different. But if you are always have the same fields in two models, maybe you don&rsquo;t need to have two models. Maybe it&rsquo;s fine to have one model. And it&rsquo;s also a matter of encapsulation, because sometimes you need to have DTO when you are doing an encapsulation like in some of our projects that. So for example in Wild Workouts project that we have. So in this case it&rsquo;s we are introducing some transport DTOs to have encapsulation in domain layer, but again, it&rsquo;s not always needed. If you sometimes have some pretty simple models encapsulation, whatever you have, small team, whatever, you have 100 people team, you know, it&rsquo;s totally different thing. And spoiler alert. So I think it&rsquo;s also in the topic of clean architecture that also a lot of people are saying like, oh, clean architecture is overengineering or whatever, but okay, how complex is the project that you are mentioning? How many people were there? Was the person that was using clean architecture aware how to do that? Maybe it was overengineered, but maybe it was not the proper implementation.</p> <p><strong>Miłosz</strong> [50:02]: We&rsquo;ll talk about it in two weeks. About clean architecture. I think what helps is knowing both extremes, because then you can just consciously choose one method. It&rsquo;s much worse if you don&rsquo;t know about it. Just stick to one and use it for everything. But what&rsquo;s also tricky is consistency, especially in a team. If you map structs for the entire project and then someone wants to skip it in one place, it usually rises long discussions about should we do it? Should we not do it? But I think if you work in a team, it helps to have more strict guidelines. So you just get rid of those discussions or you know, someone has to decide.</p> <p><strong>Robert</strong> [50:53]: All right. So time to go back to frameworks. So we already know where more or less we should avoid that. Let&rsquo;s name it vendor lock in. So we should watch out for frameworks that can become deprecated. Or there are some problems with flexibility. And especially in Go when we don&rsquo;t have the frameworks that are standard one. So we may be more afraid that something will be deprecated or maybe non-flexible. So what do you think are the alternatives in in this case.</p> <p><strong>Miłosz</strong> [51:30]: What worked very well for me before is just modular libraries, you know, just connecting libraries together. And it can also mean internal libraries in your company or in your project, the so-called common package. That&rsquo;s also considered bad practice sometime. But I think you need some consistency usually. Or you want some consistency across things like, you know API communication, messaging, maybe database as well across projects, especially in a team or across company, usually makes sense to have some standards around this. So you don&rsquo;t have just wild west, everyone doing whatever they like. But the difference is, instead of this one framework that you have to fit into the project, you just you use the libraries for where they make sense and sort of like building blocks or puzzles. You just pick what you need and leave the rest. And if something changes or breaks or you want to remove one thing, it&rsquo;s quite easy to do. And I think what&rsquo;s also very nice is you hide the complexity from like whoever uses the API. I had this experience of setting up a code base in a company in Go, and I took this approach. And for example, we used Watermill for messaging, and I hid it behind internal library. And there were all the bootstrapping, all the settings and so on, which can be quite complicated if you use some Pub/Subs. And the nice part is, you know, my colleagues didn&rsquo;t need to know exactly how it worked, although it was documented and quite easy to grasp. But as a user of a library, you usually want very small API, right? So for example, if you use an HTTP router, you don&rsquo;t want to, you know, work with the internals of the protocol. You just want to say /users GET and that&rsquo;s my function. And that&rsquo;s it. You don&rsquo;t want to configure it further unless you want to do something more specific. So then you can. But I think it&rsquo;s a nice design philosophy in general to have small APIs.</p> <p><strong>Robert</strong> [54:16]: Yeah, and I think in practice it&rsquo;s not that hard to usually do that. Again, we&rsquo;re mostly talking about Go now, but I think often it may sound like rocket science. And I think a lot of people are really promoting frameworks like it&rsquo;s helping me to create stuff so fast. And yeah, you can also if you go to the frameworks websites, you can also see some big logos using those frameworks. But you know, don&rsquo;t be fooled by that that much because I&rsquo;m not sure if you know how often getting some logos on your website work, but you know, if you have some colleague working at some big company, you can ask him to use your library or framework in this company, and later you can put this logo. So, you know, it&rsquo;s often more like that instead of, you know, I don&rsquo;t know, some Airbnb using some big framework, so you know, it can be just used in some back office application used by couple people in the company, and it&rsquo;s fine.</p> <p><strong>Miłosz</strong> [55:20]: Sometimes if you read those stories, it seems like the framework is the success behind the company or something. And when I read this I feel envy because, you know, I would like to be as a successful as they are. So maybe I will use this framework. But most of the time it just gives you this boring boilerplate. That&rsquo;s quite nice, but also not life changing usually.</p> <p><strong>Robert</strong> [55:51]: And sometimes even opposite from my experience. I mean, I&rsquo;ve seen some projects where again, it wasn&rsquo;t that it was couple months to remove the framework. It was rather impossible to do that even. I think I would find some projects that I built. I use a framework and sorry for everybody who is maintaining it now, but I was young and I was optimistic, but I&rsquo;m pretty sure that if they was able to get rid of that, that&rsquo;s great, but I&rsquo;m pretty sure that they didn&rsquo;t. And it&rsquo;s probably a mess to.</p> <p><strong>Miłosz</strong> [56:25]: What we didn&rsquo;t cover yet is in-house frameworks, which can also be a trap because then you have no one maintaining it if you don&rsquo;t do it.</p> <p><strong>Robert</strong> [56:38]: So I think it&rsquo;s probably some balance between some internal libraries and that are using something that is pre-built versus frameworks.</p> <p><strong>Miłosz</strong> [56:46]: I really like the internal libraries approach, and especially to hide some open source libraries behind it, behind some API. And one example might be observability. So I think for a logger, you know, it&rsquo;s I like to expose my own logger type as just a small interface and then use some implementation behind it. And I think you replaced logrus recently with slog in our project. And we used the same approach. And it was quite easy.</p> <p><strong>Robert</strong> [57:25]: Yeah definitely. And probably the same goes for metrics and for tracing. So usually metrics and tracing the interface is not very big. So most of the interface is matter of setup. And but the function to do trace to report metrics. It&rsquo;s pretty small. So you can and well it looks that those libraries are also changing pretty often. And if you look in the longer time span it it may change again.</p> <p><strong>Miłosz</strong> [57:55]: And the metrics and tracing calls will be scattered all over your your code base. So if you stick to the. You just import the library directly everywhere. You will need to redo it everywhere the same way. And maybe not a big deal. You know you can just do some search and replace or whatever, but it&rsquo;s much easier if you stick to your own interface and then just replace the implementation behind it. And that&rsquo;s quite a nice outcome. And you don&rsquo;t need a framework for this separate package.</p> <p><strong>Robert</strong> [58:31]: And what&rsquo;s also nice is that you can also hide a lot of flexibility that those libraries are often supporting. So you can expose a simple interface without a lot of complexity flexibility. And it can also standardize how, for example, you are doing metrics.</p> <p><strong>Miłosz</strong> [58:50]: And maybe one counterexample for frameworks or we can have from our own is Watermill. Right. So for people who don&rsquo;t know Watermill is our event driven library, and we especially had this idea in mind in the beginning to make it not frameworky at all. So the idea is that you should be able to replace it with anything else you want. And of course, it depends on how tight you couple your application with it. Hopefully you don&rsquo;t. But yeah, that was the idea from the beginning. And I think it it works pretty well right now in our projects we could just, you know, throw it away and use something custom.</p> <p><strong>Robert</strong> [59:42]: And yes, I think it&rsquo;s nicely answered the question that I mentioned earlier. So how that you can understand if something is coupled to your application by understanding how easy you can remove that. And for example, if Watermill. Well, you can do it pretty easily because you can still keep some application using Watermill and consume messages by any programming language practically. And it will be just compatible because under the hood you have messages on some message broker of your choice and it&rsquo;s compatible with everything and vice versa. For example, if you have some event driven system that is already there, you can implement Watermill into that because Watermill is not doing anything specific basically.</p> <p><strong>Miłosz</strong> [01:00:27]: Yeah.</p> <p><strong>Robert</strong> [01:00:32]: One more thing that is also interesting about if you think about frameworks and I think it&rsquo;s visible in the different languages, maybe you heard sometimes the let&rsquo;s say title like Spring developer, Symfony developer or whatever. And it&rsquo;s actually kind of makes sense because there are people that are let&rsquo;s say experts in all this framework works and how to use that.</p> <p><strong>Miłosz</strong> [01:01:01]: I remember like ten years ago, there was a big part of software houses were using Ruby on Rails, for example. I bet they hired experts. So it made sense. Or maybe still makes sense sometime. But&hellip;</p> <p><strong>Robert</strong> [01:01:21]: I think it&rsquo;s one big downside because you know, those framework knowledge how it tends to become out of date. So you know you can spend days weeks or learning something about framework, but it can be outdated. And if you switch to go to other programming language or to other company, a lot of effort that you spend to learn stuff, it will be just wasted. Because this is not, let&rsquo;s say, universal knowledge that you can put in other projects. So I think it&rsquo;s also worth mentioning that I would not really recommend to specialize in some frameworks, because you can use your time more wisely.</p> <p><strong>Miłosz</strong> [01:02:08]: And you can learn some patterns that are unique to the framework. And for example, if it supports messaging, you can you could learn how to send a message and receive it, but you don&rsquo;t learn how it works underneath. So maybe it&rsquo;s a better idea to learn even driven architecture, because you know the theory behind it and how it works in general, because it&rsquo;s better transfers to whatever you will do later. And maybe in general, some more timeless skills like how to split modules in your application, how to make it decoupled, how to write business logic so it&rsquo;s easy to read and modify.</p> <p><strong>Robert</strong> [01:02:55]: Yeah. And there are a lot of discussions now like will AI replace us or whatever. So I would say that if somebody will be replaced. So probably as framework developer, I would be afraid the most because this actually something in what AI is pretty good. So about having the knowledge about all the quirks about frameworks, how to use some strange use cases and how to connect everything. So it&rsquo;s an example. So I don&rsquo;t like to write front end, but AI is actually pretty great in that. So if I&rsquo;m for example, using react and there are some magical things that I have no idea that they existed, I knows that and can use it pretty nicely. And I can imagine that there are people that have all this knowledge how it works. But well, I would be a bit afraid that I may have less, let&rsquo;s say, job opportunities. And again, it&rsquo;s also the risk that it will be out of date and from other side. LLMs are not that great in things like design, modularization and you know, splitting bigger problems into smaller. It&rsquo;s still something that you can do better than it.</p> <p><strong>Miłosz</strong> [01:04:10]: And you need to have the knowledge to verify if hat the AI spit out is valid, which is also important. If you do it blindly, it might not end well. So, you know, there are some skills that are here to stay probably whatever you do, like those timeless architecture patterns and so on, which probably make more sense to learn than just one framework and be done with it.</p> <p><strong>Robert</strong> [01:04:39]: And I think it&rsquo;s also something what we promote, and we also see that it works for us, because I think one of the good examples is the repository An article about repository that we have on our blog. So I think we have written it five years ago, and I don&rsquo;t remember how many times it was on Reddit on /r/golang, but I think it was again there for one week ago with more than 100 upvotes. And I don&rsquo;t know, probably, I don&rsquo;t know, 20,000 visits again. So, you know, we written an article about some knowledge that is universal and it was valid five years ago, and I&rsquo;m pretty sure it will be valid in next five years.</p> <p><strong>Miłosz</strong> [01:05:21]: So yeah, even if you use AI heavily to, you know, to update your code, I guarantee if you use repository pattern, it will be much easier to work for the AI than to just have raw queries.</p> <p><strong>Robert</strong> [01:05:34]: You know what to suggest to it. Definitely. And so yeah, this this is the approach that we recommend and we see how it works for us. Because you know we&rsquo;ve been learning from PHP frameworks 15 years ago. And, you know, it&rsquo;s not helping us that much now. I mean, it was nice because we learned that, okay, things can be built in this way. Some things are possible. But we also from other side was focusing on the things that are universal. And we can just use for longer.</p> <p><strong>Robert</strong> [01:06:13]: All right. So time to summarize what we said today. So the thing that I already mentioned. So I think it&rsquo;s not like it&rsquo;s always bad to use frameworks. And especially if you are a beginner, it&rsquo;s pretty useful because it can make you more productive. And it can help you to focus on things that matters. And later you can try to reproduce that what you seen in frameworks and create your own simple libraries that are doing that.</p> <p><strong>Miłosz</strong> [01:06:47]: Yeah, but don&rsquo;t become a framework developer. Just focus on more timeless skills and universal knowledge because the framework specific stuff will probably get outdated soon. And if you learn the universal concepts, it&rsquo;s easier to join a team that needs something similar, but maybe not exactly. In your in your domain. It&rsquo;s easier to pick up new skills then.</p> <p><strong>Robert</strong> [01:07:15]: Also watch out because I think there is a lot of promotion, let&rsquo;s say on social media about frameworks. Maybe some is paid, maybe some not. But it&rsquo;s often focuses on the bright side that okay, it&rsquo;s making you productive. It&rsquo;s simplifying stuff. But I think it often misses the long term implications that you see. You will see when you use this for 2 or 3 years, and team will become bigger, and you may being the trouble. And it&rsquo;s even harder after a time, because you&rsquo;re spending more and more time on workarounds. And when you spend weeks or months on doing workarounds, it&rsquo;s pretty hard to just remove them because of skunk. False sunk cost fallacy.</p> <p><strong>Miłosz</strong> [01:08:00]: But it&rsquo;s useful to have some experience with frameworks. So at least you know how they look like, how they work. Especially with stuff like code organization or modules or observability. So you can maybe recreate it with modular libraries later. And you know, seeing a project suffer from framework use may also be useful because you see where the issues are. And, you know, you can often see it in in products, even if you don&rsquo;t work there. I think, you know, products where it seems like initially they iterate so fast and deliver to market, and then you log in one year later and it looks the same just because there were some some nice initial speed but then lost over time. And yeah, if you join a project like this as a developer it&rsquo;s a nice chance to learn some anti-patterns as well, which will help you.</p> <p><strong>Robert</strong> [01:09:08]: And again, if you join such company and in the first week, say, okay, we need to remove this framework and let&rsquo;s try to do that. Let&rsquo;s listen to the previous episode because it&rsquo;s also not if product is in the stage that okay, it&rsquo;s all coupled and you cannot maintain that. It also requires some proper approach on refactoring that. And yeah, remember that Go is also pretty specific in terms of frameworks, while in other languages there are some well-established frameworks in Go it&rsquo;s not. And I&rsquo;m pretty sure that the situation will not change. So in Go in general, we don&rsquo;t recommend using frameworks as long as maybe there is some proof of concept or maybe some your pet project. So it may be nice to see okay how some stuff can look like, but for bigger project with more people in general were not recommended in long term.</p> <p><strong>Miłosz</strong> [01:10:08]: I can recommend module libraries approach like internal libraries for your company. That usually works works well. You have some consistency. You can do stuff one way, but then easier to throw some things out if you need. You don&rsquo;t need to stick to it.</p> <p><strong>Robert</strong> [01:10:29]: All right, so is it time for Q&amp;A? So let&rsquo;s see what&rsquo;s happening in the chat. Where is chat? Hello. So Go is a great choice to build a LLM powered applications, but it is visible to use Golang for developing LLM models. Viable. Yes. So I think I can agree with that. Go is pretty nice language for building applications that are using LLMs because. So if I would for example compare it to Python. So for LLMs it&rsquo;s often nice to do some gathering of the data for example asynchronously. And good luck with Python.</p> <p><strong>Miłosz</strong> [01:11:29]: I actually work on this currently for our platform. And yeah, Go concurrency model works is great for this. Yeah, but for developing models, I don&rsquo;t think I can say anything. We don&rsquo;t have experience with this.</p> <p><strong>Robert</strong> [01:11:47]: Obviously, but I think, you know, most probably 99.99% people are not developing models. And it&rsquo;s fine. Like you are not building your own CPU so you can use something that other people did already. I think you can build almost anything in Go, mate. Yeah.</p> <p><strong>Miłosz</strong> [01:12:13]: You can, but as as the next comment says, as Konstantin says yeah. I think Python is the tool for now for most deep learning. So yeah, we don&rsquo;t have any experience with deep learning in Go. So we can&rsquo;t really answer this.</p> <p><strong>Robert</strong> [01:12:32]: But yeah probably Python will be the choice. But from some recent news it seems that you can build TypeScript compiler in Go.</p> <p><strong>Miłosz</strong> [01:12:40]: Oh yeah.</p> <p><strong>Robert</strong> [01:12:41]: So yeah, it seems that Rust is not the only option. Actually, it was pretty fun to see how many people were pissed because they didn&rsquo;t use Rust for that. I actually wondered if any of them used write something in Rust. Or maybe it&rsquo;s some cult. But let&rsquo;s leave it. Yeah. Daniel mentioned that I just signed your platform guys three days ago. I want to grasp skills in Go. Yeah, I hope you like that.</p> <p><strong>Miłosz</strong> [01:13:11]: Welcome on board.</p> <p><strong>Robert</strong> [01:13:12]: And yeah, we should also have some actually AI related stuff in our platform. So we should have AI mentor soon. So if you are starting to learn, Go. I think it will be also pretty helpful to with some more complex exercises that we have there. So stay tuned. Okay. The next message. When I think of frameworks in Go, the first one that comes to mind is GoFr, which is super useful for smart and concise microservices with all powers like observability, gRPC, HTTP and queues. I didn&rsquo;t hear about this one. I&rsquo;m not sure if you did.</p> <p><strong>Miłosz</strong> [01:14:01]: No, not really. Maybe we just stopped looking at new frameworks in Go? Yeah. I think like what we mentioned before applies. Basically it&rsquo;s I&rsquo;m sure it works well. Just be careful if you have a bigger project that will be there for a few years. Just keep in mind maybe you will want to migrate out of it.</p> <p><strong>Robert</strong> [01:14:27]: And updating five microservices is easy, but 50? It&rsquo;s just much, much, much harder.</p> <p><strong>Miłosz</strong> [01:14:35]: I&rsquo;m sure it works for many people, for many projects just, you know, keep in mind those cons of it and consider what would you do if you wanted to move out?</p> <p><strong>Robert</strong> [01:14:49]: So thought experiment. Can I remove that easily.</p> <p><strong>Miłosz</strong> [01:14:52]: If it&rsquo;s just, you know, gRPC, HTTP and queues then it sounds like just inputs and outputs of your application. Maybe that&rsquo;s if you keep it separate from the business logic. Maybe that&rsquo;s fine.</p> <p><strong>Robert</strong> [01:15:04]: Yeah. So in other words it&rsquo;s not MVC framework. So it&rsquo;s not coupling everything. So it should be easier.</p> <p><strong>Miłosz</strong> [01:15:14]: Okay. The next one how to how do you set up an API project. You expect to be big. Do you follow clean architecture? Do you use a package for dependency injection? Yeah. If we expect the project to be big, then clean arch or sure is what we will use and we will cover this in the next episode in two weeks.</p> <p><strong>Robert</strong> [01:15:37]: But watch out because I think everybody expects that project will be big. Reality is often different.</p> <p><strong>Miłosz</strong> [01:15:44]: So but this is also something we talked about a bit in the previous episode about when to write high quality code because this is similar similar question. Right. I expect this, this product to be to be big, and you have to be careful not to go too much too far with the foundations. So, you know, be wary of thinking like, I need to set up all this foundations now. So later I will just add features and it will be super quick. Most of the time it&rsquo;s not like this. You will need to refactor the project anyway, whatever you do, because it can change drastically. So it&rsquo;s better to start with some modular libraries, maybe have some reasonable setup in place, but don&rsquo;t go too crazy.</p> <p><strong>Robert</strong> [01:16:47]: Yeah, and remember 90% of startups fails. So if it&rsquo;s startup, you know it will be good to not fail because of you. Spend too much time on making your product future proof. So.</p> <p><strong>Miłosz</strong> [01:17:03]: Yeah, dependency injection. We used the wire from Google before it was sort of okay, but also had some issues. I think later we switched to just doing manually.</p> <p><strong>Robert</strong> [01:17:17]: Yeah, yeah. So wire was nice because it&rsquo;s code generation based. So I think it&rsquo;s the best what we used so far, but also it&rsquo;s kind of become very complex at the end. So we found out that it&rsquo;s just better to inject by hand because you see what&rsquo;s happening there. And at the point when you feel that, okay, it&rsquo;s super hard to inject everything. It may be not a problem of DI, it may be a problem of your code, let&rsquo;s say. I mean, maybe your dependency graph is too complicated, and maybe this is the problem because I think it&rsquo;s a good example. DI is a good example that it can surface other problems. And some people may think that, oh, okay, it&rsquo;s a problem with the DI framework, but in most cases it&rsquo;s not a problem with DI framework. It&rsquo;s a problem with your code and dependency graph that you just have too many dependencies, too many interfaces. So the most often we think that, you know, there were interfaces for things that have one implementation and it was injected and it was multiple layers of injection. And, you know, just having it one level in the AI and simplifying it, it just made things simpler.</p> <p><strong>Miłosz</strong> [01:18:35]: So yeah, I think we need to talk about it in the next episode as well. Combined with clean architecture because it&rsquo;s related. But it&rsquo;s also nice concept of because if you start on a project now, you expect to be big. I think dependency injection is something you shouldn&rsquo;t be thinking about. because that&rsquo;s this kind of overengineering maybe like just, you know, start with some something simple. Now start with manual dependency injection and see, I don&rsquo;t know, three months how it works. If you know if it&rsquo;s painful then consider some framework for dependency injection. Maybe the framework library for dependency injection. But before it happens, maybe you are overthinking it. Maybe just start.</p> <p><strong>Robert</strong> [01:19:26]: Start simple and remember that a lot of boilerplate doesn&rsquo;t mean that it&rsquo;s something wrong with that. I mean, it&rsquo;s just boilerplate. Yeah, but as long as it&rsquo;s manageable, it&rsquo;s fine. So this is pretty similar to what we mentioned a bit earlier about structs. So if you&rsquo;re passing values between structs and doing stupid mapping it&rsquo;s great. I mean it&rsquo;s not a problem when you have dumb code and boilerplate. It&rsquo;s actually good because you can understand. And you know, the alternative will be reflection and we will have no idea how it works. So having basic, you know, you can even have 1000 lines of code file that is doing injection. It&rsquo;s fine. Yeah, you can totally manage that. And it&rsquo;s easier to manage than you think. And for example, if you for example, you start to have dependency, like some things are starting to be dependent on something, you just often need to move big blocks of code from bottom to top. And you actually can see what dependence, what state. And if something doesn&rsquo;t work, compiler will tell you it&rsquo;s bad.</p> <p><strong>Miłosz</strong> [01:20:31]: And yeah, exactly. That&rsquo;s what my experience with, you know, recent my recent projects with main.go Being 2000 lines long with just project setup. It seems really dumb, but it works. Basically if you extend some constructor, you go to main.go and you press the shortcut in your IDE that says next error. You go there, fill the missing pieces and you&rsquo;re good to go.</p> <p><strong>Robert</strong> [01:21:02]: Yeah. And alternative with this dependency injection frameworks is often some magic that you don&rsquo;t understand. Highly recommended to try to do it by hand.</p> <p><strong>Miłosz</strong> [01:21:13]: So sometimes boring is nice.</p> <p><strong>Robert</strong> [01:21:19]: Okay, the next question or comment? We don&rsquo;t know yet. Actually, we&rsquo;re reading that without checking what&rsquo;s there. It&rsquo;s risky. We&rsquo;ve been pushing my team toward Cleanish architecture. We have modular monolith modules, use domain driven design patterns, but we break the rules where abstractions are too much overhead. That I think makes sense. So. And it&rsquo;s also totally fine that we also are doing it from time to time that we were thinking that it will be more complex, but it&rsquo;s not. And it&rsquo;s important to again be mindful and see that, okay, it&rsquo;s not serving anything. Let&rsquo;s maybe remove domain layer and put everything to application, remove encapsulation. And I know it&rsquo;s hard to do sometimes when you spend time on that. It&rsquo;s hard like removing unit tests I know, but it&rsquo;s sometimes something that you&rsquo;re doing and it&rsquo;s it&rsquo;s fine And it&rsquo;s nothing wrong in being trying to do, trying to or actually to overcomplicate stuff. Everybody does that. We are doing that. You can try to do your best and you will be doing that. It&rsquo;s fine. No worries.</p> <p><strong>Miłosz</strong> [01:22:31]: Yeah, we kind of mentioned this in the previous episode about breaking rules, so it&rsquo;s fine to do it, but it&rsquo;s best if you know why you do it and that you actually are breaking rules, because if you do it by accident, then it&rsquo;s much harder to decide when to change the approach or when to fix it. But if you do it on purpose, then it probably is fine because you probably know what you are doing. It&rsquo;s much better than being dogmatic about something and just, you know, announcing that, okay, today we do everything domain-driven design way and any other approach is is wrong. And that&rsquo;s asking for trouble because there&rsquo;s no one approach that works for everything. So yeah, that&rsquo;s sounds like a good, good approach. Wilson. Okay.</p> <p><strong>Robert</strong> [01:23:28]: There is another comment about LLMS. I think there is a difference between deep learning and LLMS or and also LLM-powered applications, the latter opening up to more languages. And since it is just just a bunch of API calls, Go is well suited. Yeah, I totally agree.</p> <p><strong>Miłosz</strong> [01:23:50]: Yeah, we currently work with with this like adding LLM features to our platform and using it in Go is really trivial. That&rsquo;s pretty cool.</p> <p><strong>Robert</strong> [01:24:01]: Yeah. And again I think I like to compare it to like CPUs or something like that. It&rsquo;s pretty similar situation that you are using something done in some totally different technology. That&rsquo;s totally fine. You don&rsquo;t need to fully understand. It&rsquo;s useful to understand how it works. But again, it&rsquo;s something under the API and it&rsquo;s encapsulated from you. And you can also kind of exchange that. What are your favorite frameworks and why. Oh that&rsquo;s interesting. So yeah probably as we said earlier. So we don&rsquo;t have really framework that we like in go but in other languages. So I remember PHP times and Symfony was quite nice for that time. And I like how modular it was. What else?</p> <p><strong>Miłosz</strong> [01:24:58]: Yeah, I&rsquo;m having a hard time coming up with something because I usually felt overwhelmed.</p> <p><strong>Robert</strong> [01:25:07]: I won&rsquo;t mention anything front end.</p> <p><strong>Miłosz</strong> [01:25:11]: Yeah. I think I would just say my favorite approach is having those smaller libraries used together. Not in a frameworky way. So I wouldn&rsquo;t say I have a favorite framework.</p> <p><strong>Robert</strong> [01:25:26]: Yeah. But yeah, it totally was useful to use from frameworks because you have some reference that you can, you know, how it could look like. So it&rsquo;s helpful. Okay. I&rsquo;m not sure. For the file structure it seems go has philosophy for some directories like cmd, pkg. So I wonder what is the correct way to structure an API project for the most cases? That&rsquo;s the hard one. I think we probably should do episode just about structure structure in your projects because it&rsquo;s pretty okay.</p> <p><strong>Miłosz</strong> [01:26:05]: It&rsquo;s okay. This is a follow up to the API project that will grow big. So I think the answer here is very similar to before. Don&rsquo;t overthink it in in the beginning because you don&rsquo;t know how the project will grow. Right.</p> <p><strong>Robert</strong> [01:26:24]: Yeah, yeah. But probably it&rsquo;s something that we will cover in the next episode in two weeks. But I think some sort of simple, clean architecture can work. So just having, let&rsquo;s say three packages even like application interfaces and adapters and you know, it doesn&rsquo;t differ that much from just having everything in one package, but it&rsquo;s already some start and it doesn&rsquo;t cost that much to do that. But again, probably something to go deeper in the next episode.</p> <p><strong>Miłosz</strong> [01:26:56]: Yeah. So for sure some separation makes sense if you use the clean architecture, but you don&rsquo;t have to worry about stuff like should you use it in cmd or pkg because there is no official Go guidelines for doing it.</p> <p><strong>Robert</strong> [01:27:12]: There is one package that looks like official, but it&rsquo;s not official. And I think the Go developers already mentioned that that it&rsquo;s a bit misleading. I don&rsquo;t remember the name of that, but it&rsquo;s something like I don&rsquo;t remember the you know what I mean?</p> <p><strong>Miłosz</strong> [01:27:29]: Yeah, I know this standard Go layout or something like that.</p> <p><strong>Robert</strong> [01:27:34]: Yeah, some standard Go something. So but yeah, it&rsquo;s not official. It&rsquo;s worth mentioning.</p> <p><strong>Miłosz</strong> [01:27:38]: But my point is it&rsquo;s not that important. You know, if you, if you use src and then you want to migrate to pkg, it&rsquo;s just you know, you do git move and you will do it for you and you will keep the history of the files. So that&rsquo;s not the important part. It&rsquo;s you can look up our wild workouts example. We have some similar structure to clean architecture. But, you know, you don&rsquo;t have to worry that much about it because you will need to adjust it anyway in the future. So I would avoid this idea of, you know, let&rsquo;s let&rsquo;s prepare this, let&rsquo;s prepare this perfect structure now and in the future we will just add features later. That usually will end bad because you don&rsquo;t know how the project will change over time. So instead, start with something simple. And you know, if I would take it to the extreme, you could start with a single file and see how long you can maintain it, and then just start splitting it into files. Of course it&rsquo;s extreme, so don&rsquo;t do it.</p> <p><strong>Robert</strong> [01:28:42]: But yeah, it depends. How big is your team? If your team is one person.</p> <p><strong>Miłosz</strong> [01:28:45]: Yeah, maybe it&rsquo;s fine. Maybe you can start with a single package and then then grow. I think it&rsquo;s much easier because you don&rsquo;t make assumptions about how the code will look like in two months, because you don&rsquo;t know, and it will be a surprise probably anyway.</p> <p><strong>Robert</strong> [01:28:59]: But yeah, if, for example, your team is ten people, Some lightweight, clean architecture definitely can help. So.</p> <p><strong>Miłosz</strong> [01:29:05]: Yeah. Yeah for sure. So. But as we mentioned last time, clean architecture can be really simple, right? Yeah, but maybe more on this in two weeks. Yep.</p> <p><strong>Robert</strong> [01:29:18]: All right. Okay. So it was comment for this one. So how it&rsquo;s connected to being idiomatic. So yeah, it&rsquo;s it&rsquo;s good to be idiomatic.</p> <p><strong>Miłosz</strong> [01:29:30]: But yeah this is a controversial topic. You know what&rsquo;s idiomatic. What&rsquo;s not idiomatic. Need to be careful with that. So you don&rsquo;t spend too much time worrying about being idiomatic in Go, which can mean different things. I think we have the fifth episode about unpopular opinions about Go. We have. So maybe maybe more later. So yeah, this is what we have already covered before about the mapping and what is our opinion about GoFr? We mentioned that we didn&rsquo;t use it, so nothing to add here, I think. Yeah. Daniel says we are doing a great job. You&rsquo;re welcome. Thanks for joining us. Grzegorz asks if we know Substack. It seems to me that your newsletter should be so accessible. Maybe you will gain new observers. Yeah, we read Substack. At least I do sometime. With our newsletter. We just prefer to be independent, I guess. So we don&rsquo;t plan migrating.</p> <p><strong>Robert</strong> [01:30:55]: We already did three migrations of our newsletter.</p> <p><strong>Miłosz</strong> [01:31:00]: But the nice part is it was invisible to our readers. And with Substack, you never know. It seems like a, you know, cool platform right now. But it&rsquo;s also a startup. So, you know, I think a similar thing happened with Medium ten years ago or so when they became very popular, and then they started putting paywalls. And basically, you have no control over how the how your newsletter looks like.</p> <p><strong>Robert</strong> [01:31:31]: And I&rsquo;m pretty sure that it doesn&rsquo;t have the all flexibility of newsletter platforms like. So we are doing a lot of things with our newsletter, like the podcast notifications and automations. So there is a lot of stuff under the hood, and I&rsquo;m quite sure that it may not support that.</p> <p><strong>Miłosz</strong> [01:31:51]: So yeah, so we don&rsquo;t plan on creating but thank you for the suggestion. You can share our newsletter with your colleagues to help us get new observers. And then Wilson says go fx is great. Why would you ever want to migrate away from it? Okay. I guess you misread GoFr. We didn&rsquo;t use go fx, right?</p> <p><strong>Robert</strong> [01:32:26]: Yeah. Yeah, but from what I see, it&rsquo;s So this one, as far as I remember, it was reflect based. So this was probably the</p> <p><strong>Miłosz</strong> [01:32:36]: This is a reactive framework or this is a dependency injection?</p> <p><strong>Robert</strong> [01:32:39]: Dependency injection.</p> <p><strong>Miłosz</strong> [01:32:40]: Okay.</p> <p><strong>Robert</strong> [01:32:40]: So I think I think it was reflect based. So it was.</p> <p><strong>Miłosz</strong> [01:32:46]: Okay. Yeah.</p> <p><strong>Robert</strong> [01:32:48]: So it was the thing.</p> <p><strong>Miłosz</strong> [01:32:49]: So yeah I mean it&rsquo;s a library, not a framework probably. Right. So you can if you wanted to migrate out of it. It&rsquo;s probably easier than just a framework.</p> <p><strong>Robert</strong> [01:32:59]: Yeah, yeah. But yeah, in terms of dependency injection again, we it&rsquo;s not that hard to do it by hand. And it&rsquo;s super explicit and it&rsquo;s much easier to work with. So yeah we recommend this approach even again don&rsquo;t be afraid of big files because it&rsquo;s really simple to handle.</p> <p><strong>Miłosz</strong> [01:33:16]: So yeah. What are your thoughts on urfave CLI or is there an example to the DIY go CLI program? We like it, right? This is the one we use for the tdl cli.</p> <p><strong>Robert</strong> [01:33:34]: I think I&rsquo;m just checking because I don&rsquo;t remember.</p> <p><strong>Miłosz</strong> [01:33:38]: Once, but I think we do. And I think it&rsquo;s quite cool.</p> <p><strong>Robert</strong> [01:33:46]: Yeah, yeah. So I think that this is our favorite one. So we have our CLI for our training platform and we are using exactly this one. So other alternatives I think there are not that idiomatic if you are thinking about that, but they have also some weird quirks. And this one has a very nice API and just works super nicely.</p> <p><strong>Miłosz</strong> [01:34:10]: So we don&rsquo;t have an example I think of building a CLI tool.</p> <p><strong>Robert</strong> [01:34:16]: But you can check our CLI tool for platform.</p> <p><strong>Miłosz</strong> [01:34:19]: Oh yeah, you can, you can do it and.</p> <p><strong>Robert</strong> [01:34:21]: I can put it probably to this chat into the screen.</p> <p><strong>Miłosz</strong> [01:34:26]: And our going one evening training has some CLI exercises but it&rsquo;s very basic so I&rsquo;m not sure if that&rsquo;s useful. But thank you for the suggestion. We may maybe we should write some article on this, although I think the documentation should be good enough to start this. Really? Not that much.</p> <p><strong>Robert</strong> [01:34:52]: I&rsquo;ve put the link in the screen if you would like to see some CLI example, so you can check this one. It&rsquo;s a framework worth mentioning. I mean it&rsquo;s if you ask the question how easy it will be replaced with something else. Quite easy.</p> <p><strong>Miłosz</strong> [01:35:07]: So I&rsquo;m just maybe we speak about CLI frameworks. There&rsquo;s also the bubbletea the one which I used for the pq library in Watermill. Maybe we can also link it later. I mean, it&rsquo;s a bit different because it&rsquo;s for building TUIs, but if you consider building a CLI app, that might be useful.</p> <p><strong>Robert</strong> [01:35:36]: All right. So I think there are no more questions on the chat. So last couple of seconds to leave something and and and. Yeah, it&rsquo;ll be it&rsquo;ll be time to finish for today. So as we mentioned a bit earlier. So we have our newsletter. So this is the best way to be notified about new episodes. So if you are subscribing to us on YouTube or any podcasting application actually a couple people asked us when will be available on podcasting applications. So we&rsquo;d like to record one more episode and we&rsquo;ll upload all three episodes there. And if you&rsquo;re listening on Apple Podcasts on Spotify, it means that we already did that. But we highly recommend to subscribe to our newsletter so you will not miss any new episode because, you know, YouTube algorithm or Spotify algorithm. We don&rsquo;t. We cannot impact if it will deliver you the notification newsletter. You will always know that there is a new episode, and the next episode will be about pretty hot topic about clean architecture, and I think we&rsquo;ll have a lot of nice insights. So highly recommended to be with us. And also, it will be great to hear your comments on that, because it would be great to hear some external feedback, let&rsquo;s say, for that. And yeah, if you have any comments questions, please leave it on under the YouTube video or on Apple Podcasts or Spotify. We&rsquo;ll read everything. And if you can include anything to a new episode, please leave it. Yeah. Don&rsquo;t remember. Don&rsquo;t forget to give us a thumb up in the YouTube or five star review in Apple Podcasts and Spotify, so it can help us to reach more people and give us some motivation to record new episodes. And yeah, something more?</p> <p><strong>Miłosz</strong> [01:37:38]: That&rsquo;s it. Thank you, everyone, for joining us.</p> <p><strong>Robert</strong> [01:37:40]: Thank you. And see you in two weeks. Bye!</p> <p><strong>Miłosz</strong> [01:37:42]: Bye bye</p>When it’s worth to write low-quality codehttps://threedots.tech/episode/when-to-write-low-quality-code/Wed, 05 Mar 2025 17:00:00 +0000https://threedots.tech/episode/when-to-write-low-quality-code/<h2 id="quick-takeaways">Quick Takeaways</h2> <ul> <li><strong>High-quality code</strong> is mainly about keeping good <strong>iteration speed</strong> over time - can you add features without breaking what works?</li> <li><strong>Not all code needs to be high quality</strong> - focus your efforts on the code that&rsquo;s most important, changes often, or creates the most value.</li> <li><strong>The right time to refactor</strong> is after you know your product has value, but before technical debt gets too big - make small improvements bit by bit.</li> <li><strong>Architecture decisions</strong> need more thought than other code quality factors since they&rsquo;re hardest to change later - ask &ldquo;how hard would this be to remove?&rdquo;</li> <li><strong>Balance between MVP and quality</strong> depends on your situation - for experiments, cut corners on purpose; for critical systems or enterprise products, focus on quality from the start.</li> </ul> <h2 id="introduction">Introduction</h2> <p>In the first episode of No Silver Bullet live podcast, we talk about the balance between writing high-quality code and taking shortcuts.</p> <p>Based on nearly 20 years of working together on various projects, we discuss when it makes sense to move fast rather than aim for perfect code, and how to avoid technical debt that can kill your project.</p> <p>We focus on making mindful engineering decisions instead of blindly following rules like &ldquo;always do X&rdquo; or &ldquo;never do Y&rdquo;. Different situations need different approaches to code quality.</p> <h2 id="notes">Notes</h2> <ul> <li><strong>&ldquo;No Silver Bullet&rdquo;</strong>: The classic 1986 paper by Fred Brooks that our podcast name references, discussing how there&rsquo;s no single development that will solve all software engineering challenges.</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Flearn"><strong>Our Learning Platform</strong></a>: The project we discussed that started as an MVP and was later refactored</li> <li><strong>Architecture Patterns</strong>: <ul> <li>Clean / Hexagonal Architecture (we&rsquo;ll cover in more depth in a future episode)</li> <li>Domain-Driven Design (DDD)</li> </ul> </li> <li><strong>Pareto Principle</strong>: The 80/20 rule we mentioned - often 20% of the code creates 80% of the value</li> <li><strong>Future Episodes</strong>: <ul> <li>Why you shouldn&rsquo;t use frameworks in Go</li> <li>Is Clean Architecture Overengineering?</li> </ul> </li> <li><strong>Development Practices</strong>: <ul> <li>Mob Programming - team programming approach for handling complex refactoring</li> <li>Time-boxing - setting specific time limits for refactoring efforts</li> </ul> </li> </ul> <h2 id="quotes">Quotes</h2> <blockquote> <p>Quality is not about some kind of elegance of code because it&rsquo;s just an artificial thing. It&rsquo;s not really helpful for your team if it&rsquo;s pretty.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>Often you have in the code places that you don&rsquo;t touch often or it&rsquo;s not earning a lot of money&hellip; Ask what are the places that we&rsquo;re changing the most often? What are the places that are creating most of the value?</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>I remember one project when we started applying Domain-Driven Design&hellip; Everyone loved it in the team&hellip; But what I also remember is that it had no paying users and we had to shut it down.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>If you did your dirty POC, don&rsquo;t miss the time when you should clean it up, because it&rsquo;s easy to go into a spot where it&rsquo;s no longer possible to do that.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>After working with [legacy systems] for long enough, you might think, &lsquo;I&rsquo;ve had enough of this, I won&rsquo;t allow my next project to rot like this one.&rsquo; It sounds like a good idea, but it can also be a trap.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>Sometimes it can be even the opposite. Having everything super consistent can actually be worse than having inconsistent things, because keeping this consistency requires effort&hellip; Sometimes it requires you to use an approach that is not optimal just because &lsquo;we&rsquo;re doing it consistently.&rsquo;</p> <footer> <strong>Robert</strong> </footer> </blockquote> <blockquote> <p>A useful mental model here is to care about the useful product first, not the technical design, which of course is important, but I think it&rsquo;s easy to overvalue it.</p> <footer> <strong>Miłosz</strong> </footer> </blockquote> <blockquote> <p>Try to find some places where you can do refactoring in one week&hellip; Often you don&rsquo;t need to rewrite an entire service. You can just do some refactoring in the code and iterate on that.</p> <footer> <strong>Robert</strong> </footer> </blockquote> <h2 id="timestamps">Timestamps</h2> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D3s">00:00:03 - Introduction</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D82s">00:01:22 - What is code quality?</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D235s">00:03:55 - Why quality matters</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D420s">00:07:00 - Traits of a healthy codebase</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D586s">00:09:46 - What high-quality is not</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D812s">00:13:32 - High quality outcomes</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D885s">00:14:45 - When to skip high quality</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D1167s">00:19:27 - High-quality project story</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D1350s">00:22:30 - Impact on company success</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D1554s">00:25:54 - MVP issues</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D1780s">00:29:40 - How to approach refactoring</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D2136s">00:35:36 - Balancing code quality</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D2163s">00:36:03 - Learning platform example</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D2490s">00:41:30 - When an MVP makes sense</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D2637s">00:43:57 - Where not to cut corners</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D3374s">00:56:14 - CI/CD is crucial</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D3617s">01:00:17 - Company culture</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D3993s">01:06:33 - Good enough &amp; edge cases</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D4278s">01:11:18 - Quality tips</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DqLf2VoWzuTU%26amp%3Bt%3D5087s">01:24:47 - Q&amp;A Session</a></li> </ul> <h2 id="transcript">Transcript</h2> <p><strong>Miłosz</strong> [00:03]: Hello everyone! Today we talk about writing low-quality code. So most often you want to build high quality software, which makes sense, but sometimes you do not. Sometimes good enough is better than perfect. But when does it make sense? And how to avoid technical debt killing your project? I am Miłosz.</p> <p><strong>Robert</strong> [00:27]: And I&rsquo;m Robert. And this is No Silver Bullet Life podcast where we discuss mindful backend engineering. We spent together with Miłosz almost 20 years working together in multiple teams and multiple projects. And during that time we learned that following advice like &ldquo;always do X&rdquo; or &ldquo;never do Y&rdquo; is not the best idea. It can limit your growth or lead your projects to failure. In this show, we&rsquo;ll share multiple perspectives that will help you to make smart choices that we believe may help you to be promoted to senior or principal engineer earlier.</p> <p><strong>Miłosz</strong> [01:04]: We want to make it interactive. So if you have any questions, drop them in the chat. We will answer them at the end. We plan a Q&amp;A session. Or if you have any comments about what we are currently talking about, we might pick them up early during the conversation.</p> <p><strong>Robert</strong> [01:22]: Exactly, but I think it would make sense to start with the question what the code quality is and why it matters. Because this is not something that we are often thinking of. I mean, many people are trying to implement a high quality code, but without really thinking why they are doing that. And what does it mean that it&rsquo;s high quality and what it gives to that person? So what do you think Miłosz about that?</p> <p><strong>Miłosz</strong> [01:46]: I guess it has many, many layers, probably a long topic in itself. But at a very basic level, I think you can think of the software quality as, you know, the few bugs it has, and this software does its job well. But I think for today that&rsquo;s not that interesting topic because everyone understands that probably. For me what&rsquo;s more interesting is the codebase quality, let&rsquo;s say. And the best indicator for me of it is the iteration speed. So how fast can you add features to your product or just develop your product and while not breaking what it already does well. And what&rsquo;s probably important is you do it over time. So it&rsquo;s not for a short project, but over months or years. As the team grows and as the product changes, you are able to keep working with a good pace. I guess in other words, you are not afraid to change code. You can always come back to it and add something new or modify it. So yeah, I would summarize it as iteration speed combined with developer experience. And I guess they often go together well, which doesn&rsquo;t mean the business is successful because of them yet. But often if you have a team that likes working with a software, it usually indicates that the project is doing well, at least in terms of how fast it is developed. What do you think?</p> <p><strong>Robert</strong> [03:47]: Yes, I think you are right. And well, what can I add more?</p> <p><strong>Miłosz</strong> [03:52]: It&rsquo;s a good question why it matters at all. But I guess code quality is something, you know, a topic that exists as long as software itself. And, you know, we have many books and papers from decades ago. People were already thinking about this topic. And for me, I come back to it every time I talk with some friends who recently changed jobs. And they usually tell me something like, you know, it&rsquo;s okay, but I didn&rsquo;t realize the codebase can be that bad.</p> <p><strong>Robert</strong> [04:31]: But I think it&rsquo;s quite funny in disguise because you can have signals like that. It&rsquo;s hard to work with that. But for other side, you can see in the age of AI, let&rsquo;s say, many people that are you see that they can iterate with code pretty quickly. Just use AI to build stuff. And well, quality is not best in this case, but in some cases they are taking money, making a lot of money. So it can probably indicate that it&rsquo;s probably not that simple. And there is probably some hidden complexity there.</p> <p><strong>Miłosz</strong> [04:59]: And I wonder how it will change. I guess no one knows at the moment how AI will change this. When I work with, you know, like code generating tools, I mean, AI generating tools right now for code, it&rsquo;s often, you know, there are like two extremes. I&rsquo;m either super impressed by how well it does or it produces some garbage. And if I didn&rsquo;t know what it&rsquo;s doing, I would be lost probably in the generated code.</p> <p><strong>Robert</strong> [05:33]: I think we can argue if AI can improve and maybe handle this code better. Let&rsquo;s see. So obviously large language models have many limitations. Probably many of them can be overcome. But yeah, it&rsquo;s probably let&rsquo;s see. It&rsquo;s probably I would not probably say anything, but it will be like that because I know that it&rsquo;s easy to say that, yeah, it will be solved. AI will be better, but I think it&rsquo;s not that simple. And all this old knowledge that you mentioned that from old papers, old books, I think it&rsquo;s quite funny that sometimes those papers, books have 40, 50 years old even. And, you know, they are still pretty relevant. A lot of people forgot about that, about them. But I think they have a very good point. By the way, fun fact, you can probably check from where our live podcast name is coming from.</p> <p><strong>Miłosz</strong> [06:26]: I guess we can summarize it as we still have quality issues as an industry and probably will be even more important topic now. So yeah, I wonder what are some, you know, traits of a healthy codebase. Do you have something that you can, some ways in which you can describe a healthy codebase?</p> <p><strong>Robert</strong> [07:00]: Yes. I think all of that will be connected to what you said earlier, that it should have a goal of supporting faster iteration. So the first thing that is coming to my mind are tests, obviously. So if you have tests, you are able to, you have tests and they are pretty fast, you are able to iterate on things pretty fast, because one thing is that you are not afraid that you will break anything that somebody or you did earlier, and if they are fast, you&rsquo;re iterating quickly. So this is important thing. The second thing that is also quite important is modularity. And it&rsquo;s especially important when your team is bigger than just you or just five people, maybe just ten people. Because in this case, when you have much bigger team, having good modularity helps you to change things independently. So I think it&rsquo;s pretty important to consider this in the high code quality. And the third thing is that how easy it is to understand your code? Because you can have a lot of tests. You can have multiple people working on this codebase, but if it&rsquo;s taking ages to understand how something works and nobody really knows how something works, it&rsquo;s also very hard to work, to implement something, because you don&rsquo;t know what you should touch. And it&rsquo;s taking really, really long time to changing logic there.</p> <p><strong>Miłosz</strong> [08:27]: Yeah, that makes sense. And probably all this is automated, so you don&rsquo;t waste time running in circles during development. So it also kind of sounds like developer experience to me.</p> <p><strong>Robert</strong> [08:44]: Yeah, definitely.</p> <p><strong>Miłosz</strong> [08:45]: It&rsquo;s rather developer experience-related than product-related.</p> <p><strong>Robert</strong> [08:51]: From other side. I have some impression that often developer experience is really about infrastructure. So you know, DevOps and all this stuff. And obviously it&rsquo;s important. It&rsquo;s supporting all those things, but I think it&rsquo;s not the most important thing. I mean, you can have great CI so it will help you to run your tests. But if your tests are bad, the best CI in the world will not help you. If your modularization is bad and you know having the best Kubernetes cluster in the world will not help you because even it can, I think, make things worse. Because changing one thing will require changes in ten microservices for example. So this is opposite of.</p> <p><strong>Miłosz</strong> [09:30]: Yeah, like like we&rsquo;ve seen in real life.</p> <p><strong>Robert</strong> [09:33]: Yep, yep.</p> <p><strong>Miłosz</strong> [09:34]: The platform can be great but it doesn&rsquo;t matter. It doesn&rsquo;t help if you don&rsquo;t have those more basic pieces in order. For me, it&rsquo;s also easier to define what isn&rsquo;t high-quality code. And I come back to times where we started programming a long time ago, and we didn&rsquo;t know many patterns back then, but somehow I still knew there is a code I like working with and code I don&rsquo;t like working with. And later I realized it basically boils down to I like working with the code I wrote recently, because I have all this context in my mind, it&rsquo;s easier to think about it. And I think it&rsquo;s also a common trap that when you enter a new codebase, you might assume it&rsquo;s low quality, because maybe it follows some other patterns that you know and you have no mental model of it yet, you don&rsquo;t know where to find what. And that&rsquo;s why, you know, it&rsquo;s like, it often happens that a new joiner in the team recommends overhauling everything because it&rsquo;s weird or confusing. So that&rsquo;s and it&rsquo;s also about our own code for sure. Like, I&rsquo;m sure you remember, like thinking what an idiot wrote this. And then of course it&rsquo;s&hellip;</p> <p><strong>Robert</strong> [11:12]: Git blame: Miłosz</p> <p><strong>Miłosz</strong> [11:14]: Yeah, of course it was you or me. But this is pretty much the same thing for me. So you forget the codebase over time and you don&rsquo;t remember it, but it doesn&rsquo;t matter it&rsquo;s low quality. It&rsquo;s just hard to work with for now because you don&rsquo;t have this context yet. And similarly, quality is not about some kind of elegance of code because it&rsquo;s just it&rsquo;s an artificial thing. It&rsquo;s not really helpful for your team if it&rsquo;s, let&rsquo;s say, pretty.</p> <p><strong>Robert</strong> [11:54]: And I think it&rsquo;s also hard to measure impact of that. I mean, if you think that your code is elegant, so how you see how it affects any of positive stuff that you mentioned earlier. So yeah, definitely, this is a trap. And it also reminds me that some people like some kind of consistency in the code. So it&rsquo;s actually kind of quite close to that, that people like to have everything consistent in the code. And I would say that it can be even opposite. So having everything super consistent can actually be worse than having inconsistent things, because keeping this consistency, it requires effort to do that. And sometimes it requires you to use some approach that is not optimal because it&rsquo;s okay, we&rsquo;re doing it consistently, it doesn&rsquo;t make sense, but we&rsquo;ll do it because somebody said that, okay, we&rsquo;re doing it consistently. At the end, you just have garbage.</p> <p><strong>Miłosz</strong> [12:49]: Since you mentioned that DevOps and platforms, it&rsquo;s maybe a good example of this because if you have this dream of having a uniform platform that all teams use in the team in the company, then all of them need to follow some similar standards.</p> <p><strong>Robert</strong> [13:07]: Oh, it&rsquo;s the worst if somebody from outside of your team is coming to you and no, no, you shouldn&rsquo;t do it like that. You need to do it in that because and it&rsquo;s even worse if this is the person that really like consistency. So yeah. Yeah, definitely.</p> <p><strong>Miłosz</strong> [13:19]: So consistency is useful sometimes. Sometimes, not really. As usual, there&rsquo;s always these extremes are bad. Yeah. And I guess we can summarize this as high quality codebase means quicker delivery of code overall like from very high-level perspective. This is the outcome you want. You want to deliver quickly with stable pace. So for me, I think&hellip;</p> <p><strong>Robert</strong> [13:57]: Probably it&rsquo;s a useful question to probably ask yourself like okay, will it help me to. So if you are thinking about code quality, will it help me to deliver stuff faster? Or maybe not. Maybe it is just elegance and it will not give us much.</p> <p><strong>Miłosz</strong> [14:15]: Maybe it&rsquo;s easier if you think of it as maintainability. If you want to keep working on this project over time and do it with a steady pace, that sounds like something where you want high quality.</p> <p><strong>Robert</strong> [14:31]: The thing that we mentioned already a couple of times is the fact that we are often talking mostly about how to write high-quality code, but I think it would be interesting question to ask if we always should aim to highest quality code, because it might may make sense. May sound like it makes sense. I mean, you would like to deliver fast, so we&rsquo;re keeping the code quality high, but literally</p> <p><strong>Miłosz</strong> [15:06]: Yeah. Does it make sense to always aim for high quality?</p> <p><strong>Robert</strong> [15:09]: Yeah, yeah.</p> <p><strong>Miłosz</strong> [15:10]: Well, as you know, there is no silver bullet, so probably not. Sometimes low quality can be a strategy or a tool you choose consciously. But also, as always, it depends on the context and some factors probably. Do you know some?</p> <p><strong>Robert</strong> [15:37]: I think that there are a couple of things that could help us with thinking if we should aim for high quality or not. So the first situation that I can imagine is that if we don&rsquo;t know if this code will be useful for us, so we&rsquo;re doing some experiment. We&rsquo;re doing some product that we don&rsquo;t know if anybody will use. And we know that our long term maintainability may not exist because we may discover that, okay, actually it&rsquo;s useless. Nobody will use that. So spending hours on making this code maintainable, it will be just waste of time because often maintainability requires some effort at the beginning. Often also we are implementing things that we&rsquo;re not really touching after implementing that. So if you think about your projects so very often, it&rsquo;s like that you have some modules or some parts of the applications that you are modifying often and reading often. But there are also some parts that somebody written couple years ago even, nobody have idea how it works, but nobody is really touching that because there is no need to touch that. And it&rsquo;s totally fine. I mean, it may sound bad, like we should clean up everything or whatever, but it&rsquo;s the reality that we&rsquo;ll never have time to do that. And it&rsquo;s also worth mentioning that we are also trying always to do applications as robust as we can, as stable as we can, but we forget that often those applications are not really mission critical applications. And if it will be down for day, half day or hour, nobody will care or even if somebody will be a bit upset, it&rsquo;s fine.</p> <p><strong>Miłosz</strong> [17:21]: It&rsquo;s not a nice thing to think about if you develop the application.</p> <p><strong>Robert</strong> [17:25]: I&rsquo;m not saying that all applications should be like that, but some applications definitely doesn&rsquo;t require that big that big stability, et cetera, et cetera. So it&rsquo;s also worth mentioning that it&rsquo;s not always that critical to just have stable applications. Again, I know that it may be not always logical, but I think it&rsquo;s often visible if something will really be broken because, okay, if you ask your stakeholders if okay, it should be stable. Yeah. Yeah, of course you have to make it stable. Later it&rsquo;s down for a day. Nobody really cares.</p> <p><strong>Miłosz</strong> [18:04]: But as someone developing this product, you probably don&rsquo;t want to think about it like that. You don&rsquo;t want to work on something that doesn&rsquo;t matter. But sometimes it is the case. There are more critical projects. I have a theory of how the extreme take on high quality starts because we often work with some legacy systems. And, you know, after working with one for long enough, you might think, oh, yeah, I have had enough of this, I won&rsquo;t allow my next project to rot like this one. So from now on, we will start doing everything high quality and we won&rsquo;t accept any technical debt or some half measures. And it sounds like a good idea, but it can also be a trap if you swing too far to the other extreme.</p> <p><strong>Robert</strong> [19:17]: Do you remember some projects that you worked in and it maybe didn&rsquo;t need to have that high quality code?</p> <p><strong>Miłosz</strong> [19:27]: I remember the one when we started applying Domain-Driven Design and similar patterns in Go the first time, and I&rsquo;m often reminded of it when I talk with former colleagues who worked with us on it, and they usually, over a beer, mention something like, do you remember this project? It was so great. It had all the the best patterns and it was so cool to work with. Everyone loved it in the team and I remember, it is true. But what I also remember is that it had no paying users and we had to shut it down.</p> <p><strong>Robert</strong> [20:15]: At least it was beautiful.</p> <p><strong>Miłosz</strong> [20:19]: Yeah. It might be dangerous to daydream like this because most projects don&rsquo;t look like that. Are not perfect examples of patterns or whatever you want to see in them. But this project was actually a very important moment for my career because I realized how little all those discussions mattered in the end. Like, we try to do the best job we could, but after, you know, after you see the project dead, then you realize, you know, maybe this one hour meeting on naming conventions or something like that wasn&rsquo;t that crucial in the end, you know, of course, you never know. That&rsquo;s the hard part. But I think it&rsquo;s like a good perspective coming from this previous approach of &ldquo;I hate technical debt, I will do everything to do things right&rdquo; to more balanced approach of &ldquo;Yeah, maybe we don&rsquo;t always need to spend so much time on, you know, the high quality stuff.&rdquo;</p> <p><strong>Robert</strong> [21:41]: If I&rsquo;m thinking about that, it&rsquo;s also one interesting thing. So survivorship bias. So you mentioned that okay this project failed. But I can imagine if this project wouldn&rsquo;t fail we can think at the end that yeah okay. So it&rsquo;s all thanks to our elegant code etc. and I think you should all watch out for that. That okay, if people are saying like as we said at the beginning, always do X because it will make your project successful. And it worked in our big projects that everybody knows. But you know, you don&rsquo;t know if this was the thing that made this project successful. And it&rsquo;s probably pretty unlikely. I mean, good technology is useful, but I think it&rsquo;s not the most important thing at the end to reach the product market fit.</p> <p><strong>Miłosz</strong> [22:30]: Maybe it&rsquo;s just not related that much. You know, there are companies that are very successful, but with probably terrible code quality. And on the other hand, there are failed companies with with great code.</p> <p><strong>Robert</strong> [22:46]: And I think it&rsquo;s also the opposite. So there are even more successful companies with with great code quality, but I think we will go touch on that a bit later.</p> <p><strong>Miłosz</strong> [22:55]: Maybe a useful mental model here is to care about the useful product first, not the technical design, which of course is important, but I think it&rsquo;s easy to overvalue it.</p> <p><strong>Robert</strong> [23:16]: And also we don&rsquo;t want to sound like okay, never write good quality code. We&rsquo;ll go into that. No worries. Where to, when to it makes sense to go with high quality from the beginning and we&rsquo;re not too sure or take shortcuts, so wait for a while for that. But I think we may also sound now like, okay, always write bad code. It&rsquo;s not also like that.</p> <p><strong>Robert</strong> [23:43]: What I think is also pretty important is the fact that it&rsquo;s more maybe related to what your boss expects from you. So for example, if your boss is saying that okay, it needs to be great code scalability code. So your and you are creating for example, proof of concept. So your job is to ask boss, hey, are you sure that it&rsquo;s the right time for that? And if he will say that, yeah, yeah, we need to handle thousands of transactions per second and make this code great. And you are not able to convince the boss for going the shortcuts. Well, probably it&rsquo;s better for you to say what he ordered. I mean, at the end, he&rsquo;s kind of your customer, and you are building what your boss is ordering from you, so not to be in the situation, like, okay, in one year, for example, your traffic is super big and your boss is asking, well, but you promised us that it will be scalable and it&rsquo;s not. And you are saying, oh, you know, we needed to do something and it&rsquo;s also not the best situation. So also watch out. Watch out for that.</p> <p><strong>Miłosz</strong> [24:57]: For sure there is some responsibility on you to explain. Maybe this is not the best idea. Especially if it will take a long time to implement, which usually it will. It&rsquo;s if you aim for like huge scale or something before you really need it. But at some point, yeah, you&rsquo;re not the owner.</p> <p><strong>Robert</strong> [25:21]: And remember, if you ask it for the first time, you&rsquo;ll always hear the question. No, no, it needs to be scalable and high quality. You need to go deeper and say, okay, we can do that. But for example, instead of doing this feature for this time, it will take this time. And, you know, be pretty specific. And you know, your boss is thinking in terms of money and time is money. So, you know, if you use this argument, it can help. But again, it&rsquo;s not always possible.</p> <p><strong>Miłosz</strong> [25:54]: Maybe we could go the other way now and choose some opposite project where actually high quality would be nice to have.</p> <p><strong>Robert</strong> [26:03]: I remember one project like that and this is a good example that it&rsquo;s hard to find a good time to do this refactoring or improvement, because sometimes you are starting this project as a proof of concept. You&rsquo;re doing it in a dirty way, but at some point you are at the point where fixing that will take really, really a lot of time. And I remember one project like that. And yeah, in this case, we should start the, let&rsquo;s say, refactoring much earlier because it will be easier. And now at the end, again, we were in situation that total rewrite would be the only way to fix that. And unfortunately reality is that it&rsquo;s often hard to do total rewrite. And even if you are able to do that, it&rsquo;s often happening that it&rsquo;s failing, unfortunately.</p> <p><strong>Miłosz</strong> [27:16]: You mentioned about the project that started as an MVP, right?</p> <p><strong>Robert</strong> [27:21]: Yeah, yeah.</p> <p><strong>Miłosz</strong> [27:26]: So it started as some sloppy code, maybe just to validate some idea and then grew over time into something that&rsquo;s hard to refactor.</p> <p><strong>Robert</strong> [27:37]: Yes, yes, exactly. And again, it validated the MVP in some way and well, but I think the point when we should start to do refactoring was already gone.</p> <p><strong>Miłosz</strong> [27:50]: I think that&rsquo;s the hardest question. Like when is the moment when doesn&rsquo;t matter to it doesn&rsquo;t make sense to keep going the current way. I mean, where is the moment in in the project&rsquo;s lifetime when you work on the MVP and then you decide, okay, we need to do something about it. We need to change the approach. And when does it make sense to keep going the sloppy way.</p> <p><strong>Robert</strong> [28:28]: It&rsquo;s an interesting question. I think it&rsquo;s probably hard to answer. So it&rsquo;s probably somewhere related to the question. If you start to see that it&rsquo;s useful and anybody will use that and it will make some money and&hellip;</p> <p><strong>Miłosz</strong> [28:45]: Maybe if you ask yourself this question often, then it&rsquo;s a good sign that probably something could use some refactoring here.</p> <p><strong>Robert</strong> [28:55]: But I think you know what the trap here is that if you are working in a project and it&rsquo;s bad, it&rsquo;s easy to get used to that. I mean, so you get used to how bad quality of this project is and you&rsquo;re starting to think like, yeah, okay, maybe it needs to be like that. What&rsquo;s useful is probably when some new people are joining your team and every person is saying how bad this codebase is. Obviously, you need to also take in mind that often people are just complaining that code is bad, and when it&rsquo;s not.</p> <p><strong>Miłosz</strong> [29:28]: Maybe they don&rsquo;t know it yet.</p> <p><strong>Robert</strong> [29:30]: Yeah. But again&hellip;</p> <p><strong>Miłosz</strong> [29:32]: On the other hand, new joiners often have this fresh perspective that&rsquo;s also useful.</p> <p><strong>Robert</strong> [29:37]: Definitely. But I think I can have. So unfortunately, it&rsquo;s hard to give one good answer when to do that. But I think I can give you some hints on how to approach that, because I think that this is also important part though. How to approach such refactor. The first thing that I would consider is timeboxing this, because it&rsquo;s easy to go with never ending refactoring project or total rewrite that very often are just failing because it&rsquo;s too big, it&rsquo;s hard to I mean, stakeholders usually doesn&rsquo;t like projects. Like, you are going to your cave for one year and trying to fix something. After one year, you&rsquo;re going like, oh, unfortunately, we need to have another two years, so it shouldn&rsquo;t be like that. So it&rsquo;s important to be able to cut it to some smaller pieces.</p> <p><strong>Miłosz</strong> [30:37]: And maybe start with some first iteration of changes in a small scope, you know, smaller module or something like that. So you see if it makes sense or not.</p> <p><strong>Robert</strong> [30:50]: And probably it&rsquo;s good to start with places that are often modified. So you can see the benefit of this. As we said about code quality, it&rsquo;s all about making your development faster. So if you are refactoring things that nobody touched for five years who cares?</p> <p><strong>Miłosz</strong> [31:07]: I think that&rsquo;s super controversial. I mean, when you think about refactoring a big project, I think the first intuition is to pick the least used thing, you know, where nothing happens because it&rsquo;s low risk, so you won&rsquo;t break anything. We can do whatever. But yeah, first of all, probably it&rsquo;s simpler because it&rsquo;s not used much. And second, there is little benefit in refactoring it in the first place. Maybe it doesn&rsquo;t need to be refactored ever, because who cares if it&rsquo;s not used that much? So that&rsquo;s tricky part.</p> <p><strong>Robert</strong> [31:48]: The second thing that&rsquo;s also pretty important is trying to find place that is not that big and you can change it quickly. So we are not talking about year because it&rsquo;s super hard to, you know, do one year refactoring of the thing and later deploy that. It&rsquo;s super risky or you will probably lose a lot of trust. Try to find some places where you can do. I know one day is maybe very optimistic, but let&rsquo;s say one week. Start with the question what you can refactor in one week, you can deploy after one week and try to do it in small parts. So often you don&rsquo;t need to rewrite an entire service. You can just do some refactoring in the code and just iterate on that. The good thing is that if you will do it on smaller iterations, you can see if you will break something where it&rsquo;s broken. Because if you are refactoring something for one year, it&rsquo;s super hard to later figure out what&rsquo;s wrong here, because usually you are refactoring some logic that is hard to understand and it&rsquo;s a mess to try to fix it later.</p> <p><strong>Miłosz</strong> [32:59]: And most probably also key here is to make it working end to end, right? So you don&rsquo;t work on some unused piece of code for a long time and then plug it in. But do the changes in a kind of vertical slice so it touches on the entire codebase, and you can actually deploy it and see if it works or not.</p> <p><strong>Robert</strong> [33:22]: I know that it may sound like a stretch goal, especially for harder projects that okay, how you can do refactoring and deploy it after one day. Okay. So maybe if not one day, maybe one week, but okay, if you are not able to do it in one week parts, it&rsquo;s maybe worth asking questions what you can do to deliver it in a week iterations. Maybe we need to add CI for this service / project. Maybe you need to add some tests. No, I think it&rsquo;s useful to just start asking questions. Okay. Why I cannot cut this refactoring in smaller pieces. And if you spend some time to try to figure it out, you&rsquo;ll find out that, okay, it&rsquo;s actually possible. And you will see benefits really, very, very soon on that.</p> <p><strong>Miłosz</strong> [34:08]: There&rsquo;s one more trap I see here that when you work with code base that you consider legacy, whatever this means in this context, but basically some older project older, which means maybe it&rsquo;s one year old even, and it&rsquo;s easy to assume it just has to be this way because everyone keeps complaining about this project. That there is no time to refactor. And just at some point you just give up. And then comes the big rewrite, which is probably not going to help, or it&rsquo;s going to take massive amount of work and time to finish. So I guess maybe if you see the the project will be there for a while, it&rsquo;s not going away. It&rsquo;s probably a good time to start this refactoring earlier than later.</p> <p><strong>Robert</strong> [35:09]: And try to do this refactoring in small iterations. So again, not one year refactoring, aim for one day iterations of changes that you are doing there. If not, start with week. But I wouldn&rsquo;t go with one month because one month is already very, very long time. So if you are not able to do one week iterations on refactoring, let&rsquo;s ask questions why and what you can do to be able to do that.</p> <p><strong>Robert</strong> [35:38]: Does it mean that it&rsquo;s not possible to balance code quality? And we need to always start with ugly POC and later refactor it with time, with pain, sweat, and uncomfortable questions from our stakeholders.</p> <p><strong>Miłosz</strong> [36:03]: It probably boils down to this question of when. When to do it. That&rsquo;s what I have in mind. So to take for example, the our academy platform, which also started as an MVP, because we just had an idea to create something new and we had no idea if anyone would use it.</p> <p><strong>Robert</strong> [36:28]: As a context. So if somebody doesn&rsquo;t know. So I think three years ago, we had written down from scratch a platform for learning by building projects. So something that nobody is, I think, doing. And we didn&rsquo;t know if anybody will like that. So it was kind of risky project, especially that at that time we were working in a daily jobs. So we were doing it over nights, over weekends, and we just didn&rsquo;t have time to do it better. Now, okay, we know that people like that. Buy at that point we didn&rsquo;t know. But the good thing is that I think after those almost 20 years working with different projects, we found this good time when we started to clean it up. And it wasn&rsquo;t that painful.</p> <p><strong>Miłosz</strong> [37:20]: It was exactly this dilemma because it was a rough MVP. And, you know, I remember the first version was basically mostly TODO statements in the code.</p> <p><strong>Robert</strong> [37:35]: There&rsquo;s still a lot of TODOs there.</p> <p><strong>Miłosz</strong> [37:37]: If we had a proper design session on architecture and design, everything, I mean we had some. But to do it properly, I&rsquo;m not sure if we would not just give up and just, you know, didn&rsquo;t have the energy to do it. And I think you just scraped the initial POC in a weekend or something.</p> <p><strong>Robert</strong> [37:56]: I also remember that it&rsquo;s also quite funny how it&rsquo;s changed with the time. So at the beginning, I think our idea was that you push the code in GitHub and it will run some kind of CI to validate your exercises. And during that time we totally changed how it works. And if we&rsquo;ll go with this GitHub part and do it high quality, we&rsquo;ll lose a lot of time and will not be the correct state because we&rsquo;ll just spend time on things that didn&rsquo;t matter.</p> <p><strong>Miłosz</strong> [38:26]: I remember we had completely different idea about how the CLI would work, but then you just spent some evenings trying different UX approaches and we would never know it just before. But anyway, my point is that like last month or two, we were exactly at this spot where we have more time to refactor it. But you know, it&rsquo;s a tough decision because refactoring is also not not giving you any product improvement. So it doesn&rsquo;t feel like you improve the project that much at least from the business perspective. But I think it was a the perfect time to actually sit down and fix some of the issues so we don&rsquo;t continue with adding more hacks to it, because if we did, then one year later it probably would be too late to come out of it. So we fixed the CI improved it a bit. We migrated to Postgres from Firestore and refactor the codebase a bit. And again, it was mostly developer experience, but I think it was a good investment at the time.</p> <p><strong>Robert</strong> [39:53]: And it also didn&rsquo;t take ages at the end. I mean, you were doing ugly development for three years and yeah, it took some time, of course, but it wasn&rsquo;t like a one year project to fix that. And we&rsquo;re able to do rather small iterations.</p> <p><strong>Miłosz</strong> [40:12]: And it was also tricky to pick what to fix, because I think it is also dangerous to take a naive approach of &ldquo;let&rsquo;s look at all TODOs in the code and fix them,&rdquo; because there are still a lot in the code base, but most of them are probably not relevant if we didn&rsquo;t have to touch them for the last three years. So it&rsquo;s better to look for something that actually solves some problems, even if it&rsquo;s developer experience, which is quite important for an MVP.</p> <p><strong>Robert</strong> [40:55]: Especially in bigger teams. Because, you know, if you don&rsquo;t know us, we are two people team. So not that big team. But so we can still cut some shortcuts that wouldn&rsquo;t work in a bigger teams. But still there are some things that like CI, that we&rsquo;re not cutting corners because our time is pretty limited. We are just two and we are not doing only development. We&rsquo;re also doing a lot of things around. So, you know, having CI that runs ten minutes versus five, it&rsquo;s a really big difference for us.</p> <p><strong>Miłosz</strong> [41:29]: So if your MVP gets good traction and you know you want to continue developing it, I think it&rsquo;s better to do it earlier than later. At least some some initial improvements. But this is all about MVP&rsquo;s and I&rsquo;m wondering if you think this is the best approach for every project.</p> <p><strong>Robert</strong> [41:57]: You may guess that there is no silver bullet. Ha ha ha ha. Yeah, but so this approach worked pretty nicely for our project. That doesn&rsquo;t require super big scale. We don&rsquo;t have any external customers, if it will be down, okay, some people will be upset about it but it will not be that millions of dollars of our customers are lost, but sometimes we were working on such projects. So let&rsquo;s imagine that you are working in a project that will be later sold to some enterprise customers, and you know that you need to take into consideration bigger scale from the day one. And you know, if you cut some shortcuts and you will be lucky to have some customer with bigger scale, with bigger requirements on the reliability. And, you know, it will be not stable. Your boss may start to ask your questions like, okay, you promised us that it will be scalable. It will be handling 5000 requests per second, and it&rsquo;s barely handling 100 requests per second. And you know, it will be kind of your fault if you promised something else. And your boss may be a bit upset because it can lead to losing this customer. So it&rsquo;s not like you can always cut shortcuts. You need to be pretty aware of what kind of project you are working on. And it&rsquo;s no silver bullet. What can I say?</p> <p><strong>Miłosz</strong> [43:41]: It&rsquo;s probably a big decision up front. If you want to start with a rough POC or design something more like proper high quality architecture from the ground up.</p> <p><strong>Robert</strong> [43:57]: Do you think that there are some places where you should not cut corners ever? So in other words, if there are any places in your projects that you never, ever should. Okay, never say never, ever. But in most cases, you shouldn&rsquo;t cut corners. Do you have something like this in mind?</p> <p><strong>Miłosz</strong> [44:19]: I think if you want to break rules, it&rsquo;s best if know them beforehand. So you know the patterns and the better you know them, the better you know when it makes sense to break them. And that&rsquo;s how you know when cutting corners works best. And because I think our platform is a good example of where cutting corners on purpose is much different than doing them by accident, which might be sometimes something similar. But at least you you know how to fix them. And you can leave some some context for yourself in the future so you understand why this works in a simpler way right now. Why this is a hack, but it&rsquo;s not an afterthought, but something you do on purpose.</p> <p><strong>Robert</strong> [45:22]: In other words it would be good to understand if you break some rules why those rules exists.</p> <p><strong>Miłosz</strong> [45:33]: And especially now coming back to the AI discussion, which can spit out huge loads of code for you, which is very useful sometimes, especially if you work on front end and you don&rsquo;t want to do it. But still, if you don&rsquo;t understand what&rsquo;s going on behind the scenes it probably won&rsquo;t take you far. At least that was my experience so far.</p> <p><strong>Robert</strong> [46:03]: There&rsquo;s also another example that I think cutting corners doesn&rsquo;t make sense, and this is architecture. So for architecture there&rsquo;s one bad thing that is usually hard to change. So we often seen that we&rsquo;re working in some projects that were already successful and in terms of traffic customers. But there were some decisions that somebody made at the beginning of the project, and it was already impossible to change them. So we were able to refactor some services, what were inside of them, but still they were working environment where some other decision like using some library, using some framework.</p> <p><strong>Miłosz</strong> [46:52]: Frameworks are the worst for it.</p> <p><strong>Robert</strong> [46:54]: Yeah. And you know, if somebody were just be a bit more, more mindful at the beginning of this project and think about this architectural outcomes of this decision, it would be much, much easier to develop something. And sometimes we were participating in some rescue projects that were trying to fix those architecture problems, or maybe removing some frameworks that were used there, but it took like man-months to remove something. So let&rsquo;s imagine couple teams needed to work for a couple of weeks to remove one library. And it was like just crazy amount of time lost just because somebody used some library at the beginning of the project.</p> <p><strong>Miłosz</strong> [47:37]: And it was tightly coupled to all the microservices in the company.</p> <p><strong>Robert</strong> [47:44]: What helps in this scenario is modularization. So if you are able to modularize things so you can develop them independently. And this is for example a case in our learning platform. So we have some modules that are super ugly. And I have no idea why it works but it works. But we&rsquo;re not touching that because it works and it works properly. And if it will be the place when we&rsquo;ll be changing often, obviously we&rsquo;ll refactor that, but it works. So, you know, it&rsquo;s maybe it would be good to make it nicer. But let&rsquo;s be real, we&rsquo;ll never have time for that.</p> <p><strong>Miłosz</strong> [48:21]: That&rsquo;s a tricky part, right? Because the architecture should be probably high quality. You want to make it the proper way, but it&rsquo;s also easy to overengineer it. Right? Especially if you start with an MVP that doesn&rsquo;t need all this consideration before you can just be stuck with some system that you thought will handle all possible use cases in the future, but turns out it&rsquo;s not really needed. You will be fine with a simpler approach. So that&rsquo;s something to be careful about. And on the other hand, no design is usually bad design, so you probably want some initial idea at least about how the system will work.</p> <p><strong>Robert</strong> [49:19]: I think one of the best advices here is to build stuff. I mean, because it&rsquo;s really hard to give one advice like how to not over design things and how to see the things that will get into fire later. And what I think worked, at least for us best was just building, building, building and building. And after time you will break a couple of things. We&rsquo;ll see. You will see how in some cases it worked badly after time or aged badly, for example, I think it&rsquo;s, for example, useful to work in one project for a longer time to see how it will really what will get into fire from your initial assumptions and it&rsquo;s help. And yeah, this is what we always advise people to build, to build and do it mindfully. Because if you also do it not mindfully and just build stuff without thinking about that much, it will not help you much because you will be just in the same position for longer time and will not develop yourself.</p> <p><strong>Miłosz</strong> [50:26]: Yeah, it makes sense. Sometimes it also depends on the team size, right? And how experienced people are in the team. So, for example, if it&rsquo;s just the two of us working on the MVP and you know, I see you commit 50 TODOs in one commit, I&rsquo;m mostly okay with it because I know you will tidy it up later. But in contrast, if you have a team that&rsquo;s just formed a couple of months ago and people come from different backgrounds, don&rsquo;t have that much experience with the current architecture and stuff like that, it makes more sense to have more, you know rules in place.</p> <p><strong>Robert</strong> [51:21]: To summarize a bit. So I think there&rsquo;s one tip that can help you to maybe decide if something is important architecture decision. And this is the question that you can ask yourself. And this question is how? So if you are adding something to the code to think how hard it would be to remove that. So I mentioned the history of removing one library for a long time for the project. And if somebody would ask himself a question, okay, how hard it would be to remove this library. The answer will be &ldquo;super hard&rdquo; because it&rsquo;s just coupled to everything. And you can ask this question about almost everything. So if you are using some framework or you&rsquo;re, I don&rsquo;t know, using some external vendor, some database, whatever, you can always ask your question how hard it would be to remove that. And if you don&rsquo;t have that much experience already with building architecture, building products, it will help you to, you know, start thinking about things. And sometimes, unfortunately, the answer will be it will be hard to remove that. And okay, the follow up question will be do I really need to use that?</p> <p><strong>Robert</strong> [52:38]: Often, okay, if you think for that for a while, maybe not. Maybe some frameworks or external vendors, maybe you don&rsquo;t need to use that. But sometimes, unfortunately, you need to use things that you need to couple with. Like if you are using Postgres for example, removing this later will be harder. I will not say that it&rsquo;s impossible because it&rsquo;s possible, and it&rsquo;s probably unlikely that we&rsquo;ll do that, but it&rsquo;s a decision that you are taking that okay, I&rsquo;m using Postgres and I will need to stay with that. So if you see anything with you like need to stay for longer, really be sure that this is the thing that will not hurt you. Like Postgres for example, I would not say that using Postgres is let&rsquo;s say risky, but for example, using some vendor that, you know, just had founding one year ago and they released their proof of concept that and your CTO would like to use that. And you see that it will be just hard to remove from your code. Maybe it&rsquo;s not the best idea to use it. Maybe, I don&rsquo;t know, maybe Miłosz you have some good examples of such things.</p> <p><strong>Miłosz</strong> [53:53]: You definitely need to commit. And it&rsquo;s a good thing to commit to something because it simplifies many things. Just need to be careful when you pick what to commit to. So if it&rsquo;s a stable technology that&rsquo;s been around for a long time, like Postgres or basically any SQL database probably, that&rsquo;s probably very low risk. But as you said, if it&rsquo;s something experimental sometimes it also makes sense because it&rsquo;s maybe you know, something, it&rsquo;s a perfect match to your product. But the issue is if you pick too many of them and then the risk is super high that something will go wrong. And now we have to migrate. And yeah, migrations are fun.</p> <p><strong>Robert</strong> [54:45]: But again, remember some things may be experimental and not stable, but it may be easy to replace. So probably one good example are large language models like Claude or ChatGPT. So the API of it is very small. You are sending a list of strings, and you&rsquo;re receiving a list of strings in a very simplified way. But more or less it&rsquo;s that and replacing one with another. It&rsquo;s not a big deal at the end. So you know.</p> <p><strong>Miłosz</strong> [55:17]: That&rsquo;s kind of how our migration from Firestore to Postgres went recently, which but our code wasn&rsquo;t exactly ready for it from, from the start even though we promote it on our blog to to use clean architecture and use interfaces for it. We didn&rsquo;t care that much initially for the POC, but it was very easy to reintroduce. So that&rsquo;s the part about conscious corner cutting.</p> <p><strong>Robert</strong> [55:51]: But it&rsquo;s also probably worth mentioning that in our platform, we are not really storing that much data in Postgres or we didn&rsquo;t store that much data in Firestore.</p> <p><strong>Miłosz</strong> [55:59]: It was quite simple.</p> <p><strong>Robert</strong> [56:00]: Yeah, but there are applications where you have a lot of data and it will be, i would not say impossible because it&rsquo;s always kind of possible, but it will take a lot of time. Yeah. So there&rsquo;s also one thing that I think you generally should not cut corners on. And this is your pipeline. CI/CD pipeline. And we already touched on that a bit earlier. So why CI is important. So again CI when it&rsquo;s unstable, slow it&rsquo;s just wasting a ton of your time. Again, it&rsquo;s just me and Miłosz in our company and we are building our stuff. And you know, we don&rsquo;t have that much time to do that. And if our CI is unstable or slow, we have even less time. So making it fast and stable just allowed us to go faster. And again, at the beginning it often happens that it&rsquo;s unstable, but in general, from my experience is that if you try to not keep CI stable soon, it&rsquo;s really, really hard to improve it later because it&rsquo;s ongoing work to keep it stable and fast. And doing it later will just require a lot of time.</p> <p><strong>Miłosz</strong> [57:24]: And you keep using it every day, Every person in your team uses it. So all the tiny issues add up and you just keep wasting more and more time over time. It sounds like we talk about developer experience a lot today, but I think it&rsquo;s also quite important because the morale of your team is a big factor in pushing the project forward. Like if everyone&rsquo;s frustrated with a slow CI or, you know, flaky tests or a project that&rsquo;s hard to set up locally, it&rsquo;s a hard environment to iterate quickly on anything.</p> <p><strong>Robert</strong> [58:15]: And remember, if you need to convince your boss to have time to work on CI, remember your boss is thinking in terms of money. Get the average salary in your team say, okay, our CI pipeline is taking 20 minutes. I can take a tea, eat my breakfast during that time. It costs this amount of money. Our team has this amount of people improving performance of CI by 50, 70, 80% will save us this money.</p> <p><strong>Miłosz</strong> [58:47]: It&rsquo;s one of the best, the easiest metrics you can measure. It&rsquo;s very easy to track how many time you spend on waiting on CI, and how much it improves over time.</p> <p><strong>Robert</strong> [58:59]: And if it&rsquo;s, you know together with flaky tests, it&rsquo;s even worse. I mean, I&rsquo;ve seen CI in my life. That pipeline took 20 minutes, but, okay, I know it&rsquo;s not that bad. I heard stories about pipelines that are taking days, but let&rsquo;s don&rsquo;t go there. But, you know, if it&rsquo;s flaky and you&rsquo;re waiting 20 minutes three times, and you also there is some delay when you are retrying. It&rsquo;s just a very big waste of time. Yeah, and it&rsquo;s also a lot of frustration. That&rsquo;s it. And to make it worse, it&rsquo;s also a lot of context switching because okay, you can drink one tea, eat one breakfast, but after the second retry what you will do.</p> <p><strong>Miłosz</strong> [59:44]: That&rsquo;s also why we spent some time to refactor our CI in the last iteration because we had these issues. It&rsquo;s much better.</p> <p><strong>Robert</strong> [59:54]: And remember, it&rsquo;s also sometimes fine if you see that some tests are unstable, disable that. I mean, if they are flaky and you don&rsquo;t see value in them, disable them. Remove them. It&rsquo;s also fine.</p> <p><strong>Miłosz</strong> [01:00:11]: Maybe not fine, but better than retrying over and over.</p> <p><strong>Robert</strong> [01:00:16]: Definitely. And I think it&rsquo;s also important to mention one thing because, okay, I know that it&rsquo;s already a bit complicated, that there are different kind of projects when you can cut corners, should not cut corners, you shouldn&rsquo;t cut corners. But unfortunately, I need to add another level of complexity here. And this level of complexity is company culture. So, you know, I know that in some companies you can have time to refactor things after creating POC, but unfortunately it&rsquo;s not the case in all companies and in some companies out there. If you will develop something, you will never, ever have time to refactor it. So it just makes sense to do things right from the first day, spend more time to do it properly because you know that, okay, if I will not do it right now, I will never have time to do it properly and I will just have mess to work with forever. So it&rsquo;s also important to keep this dimension in.</p> <p><strong>Miłosz</strong> [01:01:18]: And it also depends on who decides about this choice, because you probably don&rsquo;t want to ask your manager or product person or whoever, &ldquo;should this project be high quality software?&rdquo;, because no one will tell you, &ldquo;No, it shouldn&rsquo;t be.&rdquo; So on one hand, it&rsquo;s an engineering decision, but on the other it also needs to be a dialogue sometimes with the management. If there&rsquo;s some tight deadline you need to reach or something like this, but it&rsquo;s easier to talk about, you know, not the technical quality. Like an example of bad idea is, okay, we will skip tests for this to make it faster. Instead, I would focus on cutting the scope of the feature of the product. And sometimes the Pareto principle applies here. So maybe you can find some, you know, like maybe you can cut 20% of the scope that will take 80% of the time to fix, to work on or to deliver. And if you have this dialogue with the product person or product team, then it&rsquo;s much easier to cut the scope in a way that won&rsquo;t affect your codebase quality. So you can keep your developer experience. You can keep having tests and a nice environment to work in, but you can cut some of the scope, at least in the current iteration or the first iteration. And the Pareto principle is maybe a nice idea here to ask about the project in general. Like maybe some parts of it can follow more strict high quality ideas and maybe some parts can be simpler.</p> <p><strong>Robert</strong> [01:03:32]: I&rsquo;m not sure if you actually explained how Pareto rule works.</p> <p><strong>Miłosz</strong> [01:03:37]: I didn&rsquo;t. So if you don&rsquo;t know it, the idea is that often things split in this 20/80 percentages. So let&rsquo;s say 20% of the code takes 80% of time to write because it&rsquo;s more complex. And the remaining 80% takes 20% because it&rsquo;s much easier.</p> <p><strong>Robert</strong> [01:04:03]: Or 20% of the code earns 80% of money. This is also very often the case.</p> <p><strong>Miłosz</strong> [01:04:08]: It turns out this applies to many things in life. So I wonder about the same. Apply that on the project level where we can focus the 80% of focus, let&rsquo;s say.</p> <p><strong>Robert</strong> [01:04:22]: We already touched on people that like consistency in the code, and we already touched a bit earlier that it&rsquo;s usually not best idea to do. And it&rsquo;s also with code quality. So some people would like to approach code like okay, it should be consistent quality and standards over entire code base. But unfortunately in practice it&rsquo;s a waste of time because like I mentioned multiple times, often you have in the code places that you are not touching often or it&rsquo;s not earning a lot of money. Or maybe it doesn&rsquo;t need to be stable because nobody will really care about. So I think some important ideas how to find this 20% that is the most important in our code base is asking a question, &ldquo;What are the places that we&rsquo;re changing the most often? What are the places that are creating most of the value of the company? Or what are the places that where the stability of our application matters the most?&rdquo; And in those places you can use some sophisticated methods like Domain-Driven Design or do some better layering, because this code usually that makes money is often changed. It&rsquo;s usually the most complicated code in our company, and it&rsquo;s just worth to keep it high quality because it will pay back pretty nicely because we are touching it often. It&rsquo;s complex, so we&rsquo;ll be able to work with that faster. And again, it&rsquo;s important to mention that not all code in your application needs to be bad. Not all code in your application should be good. It&rsquo;s well, for some magical reason, usually some sort of part or all. So sometimes it&rsquo;s enough to have 20% of application that is high quality. Sometimes it&rsquo;s closer to 80. It doesn&rsquo;t need to be always 80. I mean, it can be 60, let&rsquo;s say, or 90. But you know, the idea.</p> <p><strong>Miłosz</strong> [01:06:34]: And maybe the opposite of high quality is not low quality sometimes, but just &ldquo;good enough&rdquo; quality, which might be a tricky balance. But basically the idea is that you start with the happy path. And at least for the first iteration of a feature, you don&rsquo;t need to implement everything. You don&rsquo;t need to cover all edge cases that can happen, because there&rsquo;s very often a very long list of them. But you can focus on this 80% of important, often used features and implement them good enough. And of course, there&rsquo;s always this other factor here that if you don&rsquo;t cover the edge cases, then you have to support them manually very often. And it often makes sense because I remember we had projects where we skipped some manual support for the most weird edge cases, because even our stakeholders didn&rsquo;t know how to handle them. It was so complicated. So why spend weeks of developer&rsquo;s time automating it if it happens very, very rarely?</p> <p><strong>Robert</strong> [01:07:50]: In other words, if you&rsquo;re automating something, think if, for example, you are not spending a week on automating thing that it&rsquo;s taking, you know, five hours a year.</p> <p><strong>Miłosz</strong> [01:08:01]: Eexactly. I had a project like this before. And when I calculated it was a dumb process that we had to do every month, but when I calculated it, it took two hours over the year for one person. So it feels dumb that you have to do it manually, but then how can you do it? How can you automate it so it makes it worth to save two hours a year?</p> <p><strong>Robert</strong> [01:08:33]: I think I have a good example even from today. So the downside of developing our training platform after hours is that we didn&rsquo;t have time for developing very basic features, like we didn&rsquo;t have feature for changing your email address. I know it sounds silly, but again we did most of this time over weekends and after work and we were just cutting all functionalities that were not possible. And okay, you can think what can be hard with changing email address. And yeah, I had the same idea this morning. So I thought, okay, it&rsquo;s super simple, let&rsquo;s even try to use Claude Code for generating this. Four hours later. I found that there is a bit more complexity for that. So we don&rsquo;t have actually that many requests to change email address over the year. But I already spent those four hours and it&rsquo;s there with some shortcuts done anyway. But it&rsquo;s often like that you have some dumb thing that you are doing rarely, but you think that, okay, automating that, it will just take one hour and one week later, you see that, okay, it was a bit harder. So I just saved my minus four days and eight hours.</p> <p><strong>Miłosz</strong> [01:10:01]: But on the other hand sometimes the small issues compound. And you may have, you know, you might spend a bit of time on one issue a year. But if there are 20 issues like that, you may find yourself doing support for full time. So again, balance. You need to pick what needs to be fixed now and postpone what doesn&rsquo;t need to be fixed right now.</p> <p><strong>Robert</strong> [01:10:38]: I think it depends a lot on the company culture. So the thing that I mentioned a bit earlier. So in startups you can probably live for a while with spreadsheets, CSVs, and, you know, your company is based on spreadsheets and CSVs and it&rsquo;s fine for the beginning. After you are scaling, it&rsquo;s probably good to maybe keep track of that, of how much time you are spending on such stupid things and balance that.</p> <p><strong>Miłosz</strong> [01:11:18]: I guess it&rsquo;s good to set some expectations also on yourself so you don&rsquo;t feel bad about having some low quality code somewhere in the codebase. And I remember feeling bad, especially when hiring new people. You know, you have some parts of the project where you don&rsquo;t want them to look, but it&rsquo;s completely normal, right? Most projects have parts like that, especially if you have some legacy software that has been there for a long time. So I think you also need to accept that some parts of it will be there. And be careful about this mindset, as you know, things need to be nice and tidy everywhere, which I think is common among developers. I also feel it sometimes that, you know, I would like things to be nice and tidy and all the corner cases covered and all the TODOs fixed, but very often it&rsquo;s not really useful to to do it.</p> <p><strong>Robert</strong> [01:12:35]: And one more thing. If you are joining some new project and you see some old code that you think is bad, try to maybe not blame previous coders because you don&rsquo;t know actually in what conditions they needed to work.</p> <p><strong>Miłosz</strong> [01:12:50]: Sometimes it&rsquo;s hard.</p> <p><strong>Robert</strong> [01:12:51]: Yeah, yeah. How long they needed to spend on some code. So for example, I have an example that I worked in one project and later I was talking to one person that was complaining a bit why it&rsquo;s that bad. And later I added a bit of context that we written this code in probably two days and didn&rsquo;t touch it mostly later because we didn&rsquo;t have time, but it worked good enough. So it&rsquo;s like it was not our bad will to write something badly. It was just we&rsquo;re doing some hackathon or whatever and we just implemented it quickly. It worked good enough and that&rsquo;s it. And yeah, I think it&rsquo;s also making your life a bit easier, if you will just try to assume that your previous colleagues that are no longer with you did your best. Especially that I also seen it multiple times that after some person is leaving, some people are tending to blame this person that oh, okay, this is bad because of this person. And you know, truth is usually a bit more sophisticated. And often those people that are blaming this person that left, they&rsquo;re also not without any blame. So, you know, watch out for that.</p> <p><strong>Miłosz</strong> [01:14:09]: Now that you mention hackathons, it&rsquo;s also an interesting topic to think about. Like how do you run a hackathon and then not end up with a pile of garbage, which we did a few times. It&rsquo;s interesting. But that&rsquo;s an extreme example. Most of the time you probably don&rsquo;t create a new product in three days.</p> <p><strong>Robert</strong> [01:14:36]: But again, I think in general the idea is good because sometimes it&rsquo;s allowing you to really cut corners, because you need to cut corners because you have some deadline to do that. And the outcome I think sometimes can be really interesting because you can go much, much further than you think that normally you wouldn&rsquo;t be able to do that because sometimes it can even take months because of some stupid conventions. And maybe at the end you will find out that, okay, maybe actually we don&rsquo;t need to do that because it&rsquo;s no value in that. So yeah, I can really encourage you to run some hackathons if you are able to do that and, you know, try to not do hackathons that don&rsquo;t care about functionality, to not use any bad words. And yeah, I think it can be an interesting exercise, but it&rsquo;s also, again, important to clean it up in places that we mentioned a bit earlier.</p> <p><strong>Miłosz</strong> [01:15:37]: But sometimes, the high quality, low quality difference is not that big. I mean, high quality sometimes doesn&rsquo;t require so much effort, even for a hackathon, right, for example, like using the repository pattern or even clean architecture maybe.</p> <p><strong>Robert</strong> [01:15:59]: In some lightweight form.</p> <p><strong>Miłosz</strong> [01:16:02]: Yeah. It&rsquo;s not like a huge investment upfront. And once you do it, it&rsquo;s easier to manage the project. So even for hackathons, you could use some of the more advanced patterns, let&rsquo;s say. And it won&rsquo;t slow you down.</p> <p><strong>Robert</strong> [01:16:25]: And the thing that we mentioned a couple of times. So try to be mindful about code quality and remember that it&rsquo;s something that you need to learn with building over time. So after you build a lot of projects you see better where you can cut corners, where you shouldn&rsquo;t cut corners. So during this episode, we did give you some hints. But again, I think practice, practice and practice, it&rsquo;s very important. And it&rsquo;s also fine to be wrong here. So even if with Miłosz we have pretty lot of experience, sometimes we&rsquo;re also wrong in deciding if we should go with higher or lower quality. And it&rsquo;s fine. I mean, as long as it&rsquo;s not some decision that is not reversible, it&rsquo;s fine. So I think there are a couple of questions that it&rsquo;s worth asking. So if you are thinking about code quality. So the first thing is that something that we mentioned at the beginning. So if this code is more an experiment, or we know that, okay, we already have customers there or it will be potentially sold to some enterprise customers that will need to just make it working properly from the day one. So it&rsquo;s good to start with that.</p> <p><strong>Miłosz</strong> [01:17:46]: It will also help you decide if you want to use an MVP or not for this project. And a similar question is if this project is mission critical, if something terrible will happen, if it goes down for a couple of hours, let&rsquo;s say. As I said before, we don&rsquo;t like to think our code is not important, but maybe at least parts of it are not.</p> <p><strong>Robert</strong> [01:18:18]: It&rsquo;s also good to think, okay, what will happen with that after we develop that. Maybe this is feature complete part and will not touch it in the next year. I know that, okay, the chance that we will touch any code in five years is probably not that low, but maybe in one year what&rsquo;s the chance that we will be touching that. If we will not be touching this code in one year, we assume we never know, but we can have some assumption. Maybe we can cut some corners there because if we will not be touching that what it will give us if we make it high quality.</p> <p><strong>Miłosz</strong> [01:18:53]: And if you touch it, then it&rsquo;s good to have some context of the approach that we used. So for example when refactoring our Academy platform it was useful to have this context in the comments or the todos that scattered it. And they don&rsquo;t look nice, but it was super helpful for me to know okay, that&rsquo;s why we took this approach. But it also doesn&rsquo;t mean that you have to get rid of all the TODOs. Sometimes it just, you know, it just exists there, and it&rsquo;s fine. We don&rsquo;t need to to worry about it. Because if you don&rsquo;t change it over time, what&rsquo;s the issue?</p> <p><strong>Robert</strong> [01:19:41]: Like prehistorical paintings in caves?</p> <p><strong>Miłosz</strong> [01:19:46]: Yeah, kind of.</p> <p><strong>Robert</strong> [01:19:48]: I love those comments in the code like, &ldquo;it shouldn&rsquo;t be like that. We should fix it ASAP.&rdquo; Git blame: five years, ago and everything is fine.</p> <p><strong>Miłosz</strong> [01:19:59]: It mostly shows that it wasn&rsquo;t that important maybe. And I remember having some time in the sprint when we are out of work and it&rsquo;s time to look through the backlog. And I remember looking at the technical debt issues that we added over the years before, months before, and we always complained there was no time to do them. And finally there is time. And I looked at them and I realized I don&rsquo;t want to work on it because it doesn&rsquo;t seem important. So then you can just delete them and don&rsquo;t worry.</p> <p><strong>Robert</strong> [01:20:43]: And if you did your dirty POC, don&rsquo;t miss the time when you should clean it up, because it&rsquo;s easy to go into spot when it&rsquo;s no longer possible to do that. And I think it&rsquo;s very nicely visible in some products that it&rsquo;s visible that they were able to develop it quickly at the beginning. But you can see maybe three years later that they were not able to add any single feature to this product because it&rsquo;s so hard to touch. Everybody is so afraid to touch and change anything there, and it&rsquo;s just slow death for the product. And it&rsquo;s already too late because it has too many functionalities that it&rsquo;s hard to replace and it&rsquo;s maybe just not justifiable job. You may not justify to rewrite it from scratch.</p> <p><strong>Miłosz</strong> [01:21:42]: It&rsquo;s probably worth to keep in mind also that code quality is not the end goal. It will help your projects succeed over time, especially in the long run. But be careful not to make it the metric, to not care too much about the quality itself, which is hard to define in the first place.</p> <p><strong>Robert</strong> [01:22:06]: I know that there are many people that are thinking like, I don&rsquo;t care if this project will be successful. I just care about money. So if you are this person, right, it&rsquo;s fine. But if you are trying to find a well-paid job, it&rsquo;s much easier if you have some success stories. So you just didn&rsquo;t work in the projects that were mediocre or failures because, well, the market is pretty hard now, and I was interviewing many people in my life, and it&rsquo;s super easy to see if people really worked in their projects that were successful, and they had some positive impact on how those projects were delivered and how they succeeded. And, you know, if you are just working in the projects that &ldquo;I don&rsquo;t care if it&rsquo;s successful&rdquo;, really, it will be much harder for you. So if it&rsquo;s you, really, I highly recommend to try to care, even from a selfish perspective.</p> <p><strong>Miłosz</strong> [01:23:06]: Probably also watch out for some extreme tips like &ldquo;never do X&rdquo;, &ldquo;always do Y&rdquo;, because most of the time it&rsquo;s not so simple. There are more subtle corner cases and context you need to understand.</p> <p><strong>Robert</strong> [01:23:28]: I think there are multiple dimensions of parameters let&rsquo;s say that are affecting that. So if you are listening to us from the beginning, so you see that just code quality can have that many dimensions and you see that out there there are a lot of people that are saying that, oh, you don&rsquo;t need to care about code quality, or code quality is super important for how fast you&rsquo;re delivering. So you can see that it&rsquo;s not that easy. And I think it&rsquo;s just showing that a lot of people that didn&rsquo;t probably work in many projects or didn&rsquo;t work mindfully in these projects may not give you the best advice.</p> <p><strong>Miłosz</strong> [01:24:07]: It&rsquo;s hard to boil it down to some very simple rules. I think it&rsquo;s quite lazy if someone tells you something like &ldquo;just just keep it simple&rdquo;. Yes, of course, I want to keep it simple, but you can&rsquo;t just take a 12-person team and tell them, just keep it simple guys, it will be fine. So there&rsquo;s many, many different ways you can approach any project.</p> <p><strong>Robert</strong> [01:24:38]: Check good sources, some good live podcasts, for example.</p> <p><strong>Miłosz</strong> [01:24:46]: Okay, I think we are nearing the end.</p> <p><strong>Robert</strong> [01:24:48]: Yes, I think we should be able to go into our Q&amp;A now. So sorry for not looking for the chat, but our multitasking abilities are limited. So let&rsquo;s see what questions we have on the chat. So we&rsquo;re mentioning a bit of our learning platform. So it&rsquo;s a proof that we have a lot of fans of how it works. And yeah, how the strategy of cutting corners at the beginning and fixing it later, it works. So yeah, we have &ldquo;Go Event-Driven was really good. Highly recommended for everyone to check out.&rdquo; That&rsquo;s true.</p> <p><strong>Miłosz</strong> [01:25:32]: Thank you.</p> <p><strong>Robert</strong> [01:25:35]: All right. So time for the first question. Do you want to take it?</p> <p><strong>Miłosz</strong> [01:25:39]: Okay. So Rohit asks, &ldquo;do you think that careful design decisions maybe at low level would avoid refactoring later on? Sometimes I have made decisions that to priority addition of changes over code quality. Is that normal?&rdquo; Maybe that&rsquo;s the question whether you want to start with a POC and do something dirty and then figure it out. Or do you want to create some proper architecture from the ground up.</p> <p><strong>Robert</strong> [01:26:21]: I think it&rsquo;s probably important to set some expectations here. So I think it&rsquo;s good to spend some effort on design because if you always have some design so there is no that you have no design. If you will not make design you will have bad design. But it&rsquo;s not that you will not have the design, but if you try to do your best, even spend ages on the proper design you probably will also not do it totally right. And it&rsquo;s fine because you have some assumptions at the beginning how it will work, but it will change over time when you&rsquo;re implementing stuff, and at some point you will end up with need to refactor something and it&rsquo;s totally fine. So like we mentioned example of our platform for trainings. So our initial idea was totally different. And at the end we ended up with something different and design wasn&rsquo;t perfect, but we were able to adjust it with the time.</p> <p><strong>Miłosz</strong> [01:27:27]: I think the part I would be careful about here is the avoid refactoring later on. So this is not something you want to aim for. You don&rsquo;t want to make something perfect now, so you avoid refactoring later because it will anyway happen. So rather better to adjust as you go with some small improvements rather than try to come up with a perfect solution that will cover all future use cases. That usually ends badly.</p> <p><strong>Robert</strong> [01:28:01]: Remember about the question that I suggested at some point. So I think design is the most important for things that are hard to change and to know if something is hard to change. Ask a question. Is it hard to remove? If it&rsquo;s hard to remove, probably it&rsquo;s good to think about the good design or thinking if you need to redo it in this way. I hope we answered your question. If not, please leave a follow up on the chat. Yeah. Second question is a bit simpler. So, &ldquo;hey guys, will you be releasing on Apple Podcasts later?&rdquo;. Yes, we&rsquo;ll release on Apple Podcasts, but we would like to have three episodes done, so it will take more or less one month will be releasing it to another three platforms, so it will stay just on the YouTube for since we will release two more episodes and later it will be on Apple Podcasts, Spotify or whatever application for listening podcasts that you are using. It will be there. If you are in our newsletter. We&rsquo;ll also send a summary tomorrow. Tomorrow? Yeah, I guess tomorrow. We&rsquo;ll send summary, transcript, and all this interesting stuff. Another question. &ldquo;Is hexagonal architecture worth in the real world?&rdquo; What do you think, Miłosz?</p> <p><strong>Miłosz</strong> [01:29:25]: I want to say yes, but.</p> <p><strong>Robert</strong> [01:29:28]: There is no silver bullet!</p> <p><strong>Miłosz</strong> [01:29:31]: But I can tell you that, we used it a lot in many different projects and teams. Hexagonal, clean architecture or whatever you want to call it. And one thing I noticed is like this. This is the pattern that many former colleagues tell us about that they introduced in their new team. And the new team members also love it. So I think it shows that&rsquo;s something developers like working with. And we do too. But of course, if it&rsquo;s a simple POC or some scrappy MVP, you can probably skip it. But then also it is not a big effort upfront. At least, maybe that&rsquo;s just my perspective because I&rsquo;m used to it so much.</p> <p><strong>Robert</strong> [01:30:28]: I think it requires more effort if you are doing it for the first time. So I will definitely not do it for the first time. Especially that I think clean architecture is sometimes kind of the trap. So people are trying are overcomplicating that.</p> <p><strong>Miłosz</strong> [01:30:44]: And we will be talking about it in four weeks, so I think we will cover it more in depth, but I think it&rsquo;s one of the most useful patterns we use overall. And also not that complicated. And it&rsquo;s not even about the exact approach, but just having some layers, splitting the important code from the less important code.</p> <p><strong>Robert</strong> [01:31:10]: TL;DR yes, it&rsquo;s worth.</p> <p><strong>Miłosz</strong> [01:31:15]: Definitely in the real world it is useful.</p> <p><strong>Robert</strong> [01:31:19]: It&rsquo;s just important to not over-complicate that, unfortunately a lot of people are doing and okay, also what other people are doing is applying clean architecture without understanding that. So it&rsquo;s leading to a complicated code.</p> <p><strong>Miłosz</strong> [01:31:35]: That&rsquo;s the issue with many patterns. It&rsquo;s very hard to understand what someone means by clean architecture. do you remember the rant on Reddit about how clean architecture is the worst thing to happen in Go? And you know, I then read the thread. And basically the author said that you shouldn&rsquo;t use more than three layers.</p> <p><strong>Robert</strong> [01:32:05]: Instant question. So how many layers you are using there? TEN?</p> <p><strong>Miłosz</strong> [01:32:11]: That&rsquo;s the general issue with naming. I think it&rsquo;s similar with Domain-Driven Design.</p> <p><strong>Robert</strong> [01:32:16]: Maybe they are combining clean architecture with hexagonal and onion and using all layers from that.</p> <p><strong>Miłosz</strong> [01:32:24]: It&rsquo;s very hard to understand what someone means by one of the acronyms used in tech.</p> <p><strong>Robert</strong> [01:32:32]: Long story short, we&rsquo;ll touch a bit more on that in the next episode. Please remember to subscribe on our landing page of the podcast so you can. You&rsquo;ll not miss that. All right. Next question. &ldquo;That&rsquo;s actually how I&rsquo;m developing first version, I write the POC, then I do the refactoring in cycles and until I&rsquo;m happy with the story the coding is telling.&rdquo; Yep, it&rsquo;s a good approach.</p> <p><strong>Miłosz</strong> [01:33:04]: That&rsquo;s iterating</p> <p><strong>Robert</strong> [01:33:09]: Yep. Okay, the next one. &ldquo;Is it better to refactor or code review every certain interval? Because in startup environment almost no one wants to add review as their task.&rdquo; I think that this is interesting question.</p> <p><strong>Robert</strong> [01:33:29]: So what&rsquo;s my experience in a similar situation is that if you have problem with people doing your code review, it&rsquo;s probably something wrong with team responsibilities. If in the team. I mean, if people are kind of working on things individually, not like a team, it&rsquo;s a problem because it&rsquo;s not the team&rsquo;s responsibility to deliver something, but it&rsquo;s individual responsibility to deliver something. And from my experience, this is the root cause because, well, if you are responsible for your feature, why you should review other people&rsquo;s work, you kind of don&rsquo;t care. And if you are doing more team work, so you&rsquo;re planning the work as a team. You&rsquo;re implementing entire feature as a team. You know, it&rsquo;s interest of all of you to deliver it faster. But I&rsquo;m not sure.</p> <p><strong>Miłosz</strong> [01:34:25]: But, you know, I think the question is more about the interval. So the way I understand this, should you do like two sprints of product work and then one sprint of refactoring or review? And I don&rsquo;t have good experiences with this approach of this, you know, like dedicating entire sprint to code quality. I don&rsquo;t remember it ending well, at least in the projects I was in. So I don&rsquo;t know. What do you think, Robert? I think it&rsquo;s better to iterate. So just do small improvements as you go.</p> <p><strong>Robert</strong> [01:35:14]: Yes. There&rsquo;s also one thing in this question that I think it&rsquo;s also pretty relevant.</p> <p><strong>Miłosz</strong> [01:35:18]: Oh, unless you do what we did. So we actually had to dedicate this one iteration pretty much to changing, to refactoring the code base so you can start working quicker. So sometimes this &ldquo;go faster&rdquo; initiative also can work. I would just be careful with stuff like okay, maybe it&rsquo;s about if you know what you want to do beforehand. So if you know we have this issue with the CI, let&rsquo;s focus one week on improving the CI. Then it sounds pretty good to me. But if you want to do something like okay, we worked six weeks on the product. Now let&rsquo;s take two weeks to fix the TODOs. That sounds like more tricky part because you don&rsquo;t know what you what you want to solve. So I would try to, you know, first understand what you want to fix.</p> <p><strong>Robert</strong> [01:36:21]: And there is one more thing that, so please let us know if we&rsquo;ve answered this question properly. We&rsquo;re after ten hours work day and it&rsquo;s almost 8 p.m. here in Poland. There&rsquo;s also one more thing that may be worth mentioning here. So for some hardcore refactorings. So what we also recommend for that is mob programming. So you are gathering with the team. One person is coding and you&rsquo;re kind of helping this person. I will not cover how mob programming works, but I think that not a lot of people know about that. Worth checking, especially for harder refactorings because it&rsquo;s easier to kind of synchronize with the work that you are doing with the entire team. I know that for some people it&rsquo;s like, oh, we&rsquo;re wasting entire team&rsquo;s time for this. And it would be easier if one person will work on that. But for other side, somebody needs to review that. And the good thing about mob programming is that you have real time review. So everybody is looking on that and you are kind of doing a review together with them.</p> <p><strong>Miłosz</strong> [01:37:27]: The bad part is that it promotes looking for consistency. So you have to be very careful about cutting offtopic discussions or nit picking of how something works.</p> <p><strong>Robert</strong> [01:37:43]: But I think it&rsquo;s good for things that let&rsquo;s say you will be working on for a week and it wouldn&rsquo;t be possible to do code review of that, because sometimes it happens that you have that big refactoring that there&rsquo;s no way to do code review of that. If you are doing mob programming, there is kind of no need of doing code review. All right. So I hope we answered. If not, please let us know. All right. So the next one Miłosz maybe you would like to pick up this question?</p> <p><strong>Miłosz</strong> [01:38:14]: &ldquo;It is always good practice to keep your code at a minimal level without looking for perfection, but at least enough clean to be comfortable to give it to someone else without being nervous about it.&rdquo; Yep, sounds like a good baseline level. Like, don&rsquo;t don&rsquo;t stress too much about perfection. But you know, take effort. Yeah I think that&rsquo;s actually a good question to ask yourself. Am I comfortable giving it to someone else and having them work on it? Would I be ashamed of myself. I used to, you know, half-joke about this, about our platform, that if we hired someone to work on it, our reputation would be over because they would see all the TODOs in the code. Yeah. So that&rsquo;s pretty good. Good idea.</p> <p><strong>Robert</strong> [01:39:15]: All right. And so time for so far, the last question. If anybody has more questions, you have still some time. So &ldquo;how do you measure that you&rsquo;re overthinking and overengineering.&rdquo; So I&rsquo;m not sure what you think about that, but I would say that what I personally like is I think that most of us doesn&rsquo;t like, but it&rsquo;s sometimes also useful. So having some timebox or deadline. So if you&rsquo;re timeboxing your work for some time, or giving yourself some deadline, or maybe some days doing it externally. So it&rsquo;s kind of forcing you to not overthink things because, okay, you have some time to do that and you need to focus on thinking on things that really matter. And I think I noticed it for the first time when I had some external deadlines that I noticed that, okay, I needed to refocus on things that did matter and other things I didn&rsquo;t have time to run it. Again, I know that we don&rsquo;t like deadlines, but this is the small, useful part of them. Definitely. I&rsquo;m not sure what&rsquo;s your take on this?</p> <p><strong>Miłosz</strong> [01:40:29]: I think it&rsquo;s hard to measure yourself because we don&rsquo;t like to think that we are wrong about something. But what I like is cutting scope. And I think it&rsquo;s something we did this week, actually. And when we we have a new project going on and we had an initial idea how to do it and with like a brainstorming session and then we thought, okay, let&rsquo;s now remove everything that is not absolutely required to make it work. And let&rsquo;s just finish this v0.1, deploy it and see if someone uses it. And very often you later realize, oh, actually, this is good enough. Maybe we didn&rsquo;t need all this complexity. We initially planned for. Or as Robert mentioned before, with our platform, we had much more complex setup in mind, first with CI connected to it and all of it, and then we stripped it from all this and focused on the critical path. And that usually helps because you can see that maybe you don&rsquo;t need all this.</p> <p><strong>Robert</strong> [01:41:55]: Also having some kind of MVP, maybe connected with some deadline that you&rsquo;re putting yourself on that doesn&rsquo;t need to be that strict. But again it helps. And I think what also helps is to have some end-to-end part working. So for example now we are working to adding some AI on our platform. But also I think we also have some nice idea how to have it working end to end with some limited functionality. And we&rsquo;re not overthinking that that much because okay, we&rsquo;re focusing on the simplest part and we can start to overthink it later after we&rsquo;ll see that it was working. Because the problem with overthinking is that you can overthink things, that you later find out that it&rsquo;s not useful, and you need to overthink another thing.</p> <p><strong>Miłosz</strong> [01:42:45]: Sometimes it also helps to have someone else who can tell you, you know, maybe you just want to use this AI API or this database. Maybe you should do it in your side project, not in this one. You know, like someone who can tell you if you if you go too far, it should be easier for someone else. But then you need high levels of trust to accept this.</p> <p><strong>Robert</strong> [01:43:20]: Okay. So I think it would be it for today so we can finish up. So if somebody doesn&rsquo;t know us yet. So I highly recommend to check our blog. I don&rsquo;t know if you can see QR code. It&rsquo;s not that big. Let me try. We&rsquo;re using this platform for the first time, so let&rsquo;s do it like this. Okay. It&rsquo;s not so big.</p> <p><strong>Miłosz</strong> [01:43:54]: True MVP.</p> <p><strong>Robert</strong> [01:43:55]: This is QR code with our blog, so highly recommend to check this out so there is more content there that is mostly about Go. So if you know Go it may be interesting for you. If you don&rsquo;t know Go, it may be also interesting for you. If you liked this live podcast episode, fun fact is that this is the first episode that we recorded ever, so I hope that you liked this. So if you liked this, I highly recommend you to subscribe to us on YouTube so it can reach more people. And, you know, you can be also sure that you are not missing any new episode because we are already planning a couple more. Oh, I see that actually a lot of people just joined us because it was 70 people and now it&rsquo;s 94. Hello for everybody who just joined, we&rsquo;re finishing, but you can scroll to the beginning to check that. So yeah, if you would like to also to support us, please let all your friends know about that so it will help us to release new episodes. What else? So we already have a couple more episodes planned. So the next one will be why you shouldn&rsquo;t use frameworks in Go. So if you are writing Go, it may be very relevant for you. If you&rsquo;re not writing in Go, it may be also relevant because I think it may be pretty universal advice. The next episode will be Is Clean Architecture Overengineering. So we covered a bit about that earlier, but we&rsquo;ll go a bit deeper in this episode.</p> <p><strong>Miłosz</strong> [01:45:50]: That&rsquo;s a broad topic.</p> <p><strong>Robert</strong> [01:45:52]: Yeah. After that, we&rsquo;ll cover how we believe you can learn faster. So we also covered a bit of that today, but we&rsquo;ll cover definitely much more. Also if you&rsquo;d like to not miss an episode we totally recommend to subscribe to our newsletter on our blog, because YouTube may not send you notification or Twitter may not send you a notification. In our newsletter, you will be sure that you are receiving the notifications so you will not miss the episode. And yeah, if you have any comments questions and you are listening it a bit later, leave us a comment on the YouTube. We&rsquo;ll read that. And if you have anything that we should include in the future, please write down and we&rsquo;ll try to include it in the next episode. Please also give us a thumbs up if this is already uploaded to Spotify, Apple Podcasts, or whatever, please also leave us a five star review so it can reach more people. Something to add Miłosz?</p> <p><strong>Miłosz</strong> [01:46:57]: Thank you, Robert, for meeting me today.</p> <p><strong>Robert</strong> [01:46:59]: Yeah. Thank you.</p> <p><strong>Miłosz</strong> [01:47:00]: See you in two weeks.</p> <p><strong>Robert</strong> [01:47:01]: Yeah. See you in two weeks. Thank you.</p> <p><strong>Miłosz</strong> [01:47:03]: Thank you everyone. Bye bye.</p> <p><strong>Robert</strong> [01:47:05]: Bye bye.</p>Iteration #1: "Go Faster"https://threedots.tech/post/iteration-1/Wed, 26 Feb 2025 00:00:00 +0100https://threedots.tech/post/iteration-1/<p>We registered Three Dots Labs as a company in 2014, hoping to quit our jobs soon to work on our products. The &ldquo;soon&rdquo; turned into ten years of launching side projects and working evenings and weekends. We finally <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2F">decided to take the leap</a> last year, and we plan to share how it&rsquo;s going. If you&rsquo;re in a similar spot, you may find it helpful.</p> <p>By the end of 2024, we spent some time planning the next year and our approach to work. We reviewed our backlog and discussed what we wanted to do next. We&rsquo;ll experiment with 11-week iterations, with a week off in between. It&rsquo;s not like the usual sprint — it&rsquo;s more a way to stay focused during this time.</p> <p>We&rsquo;ve just wrapped up the first iteration, and you&rsquo;ll find the summary of it below. Our main goal was to get ready for the next iterations. There were many things we wanted to wrap up before starting new projects. We also gave ourselves time to chill a bit after working two full-time jobs for a long time.</p> <p>The main theme was &ldquo;go faster&rdquo;, so it&rsquo;s mostly housekeeping stuff. We decided it&rsquo;s important to do it now because there are just two of us, and we can&rsquo;t afford to waste time during development. Our ultimate goal is to create a place where we love to work, and frustrating things like slow CI or a messy codebase are real motivation killers.</p> <p>What we do now falls roughly into three categories:</p> <ul> <li>Academy (our learning platform).</li> <li>Watermill (our open-source library).</li> <li>Content (blog posts, podcast, etc.).</li> </ul> <h1 id="academy">Academy</h1> <p>We had an idea for hands-on programming training a few years ago, but we weren&rsquo;t sure if people would enjoy learning like this. We created an MVP to see if it&rsquo;s technically possible. <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F" target="_blank">Go in One Evening</a> was the first training we created.</p> <p>The project started as a scraped-together prototype that we iterated on to improve the UX. When it gained traction, we built more features and created <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fevent-driven%2F" target="_blank">another, much bigger training</a>. We did all this while still working full-time, so we struggled to find time and never bothered to refactor the project. We ended up with quite a low-quality codebase that made the most money out of all our products. (I used to half-joke that if the codebase ever leaked, our reputation would be over.)</p> <p>If you ever worked on a professional project, you know it&rsquo;s an extremely common scenario. There are projects with great technical quality that no one uses, and they die. And there are terrible big balls of mud that make millions. But having technical debt is fine if you take time to clean up some of the mess.</p> <p>For us, it&rsquo;s an interesting place to be in. When you work as a developer, it&rsquo;s easy to complain about not having enough time to <em>do things right</em>, while the <em>business</em> needs to move fast and make money. Now, we feel both pressures at the same time. We need to move the product forward but don&rsquo;t want to get stuck working with a messy codebase. The frustration quickly adds up.</p> <p>We finally have time for things like this, so we focused on them during the last few months.</p> <h2 id="moving-to-a-monorepo-and-updating-the-ci">Moving to a monorepo and updating the CI</h2> <p>Our CI got slow and fragile over time, and as you may know, it&rsquo;s one of the most frustrating things to have around when working on a project. It&rsquo;s also your main feedback loop when committing code. <strong>You waste precious time if you have to wait or fight to make the CI pass.</strong></p> <p>While cleaning it up, we also decided to move to a monorepo, which makes sense when only the two of us are working on it. We ended up with one repository that keeps all of our active projects (including this blog) and a set of GitHub Actions that run only when the relevant project changes.</p> <p>It went smoothly, apart from the regular experience of testing the CI (commit, push, wait, read the error, repeat). It also improved the developer experience right away.</p> <figure class="img-center" role="group" aria-describedby="caption-Various CI workflows in one repository."> <img title="" loading="lazy" decoding="async" class="img " width="322" height="369" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fiteration-1%2Fimages%2Fci_hu478c0ccbc4de18a052607c55a21c27b6_99274_322x369_resize_q80_h2_lanczos_3.webp" alt="CI Workflows" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fiteration-1%5C%2Fimages%5C%2Fci_hu478c0ccbc4de18a052607c55a21c27b6_99274_322x369_resize_lanczos_3.png"" /> <figcaption id="caption-Various CI workflows in one repository." class="caption-Various-CI-workflows-in-one-repository"> Various CI workflows in one repository. </figcaption> </figure> <figure class="img-center" role="group" aria-describedby="caption-The pipeline for Academy."> <img title="" loading="lazy" decoding="async" class="img img-wide" width="2013" height="428" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fiteration-1%2Fimages%2Fci-2_hu8ee6a01855bb625afd6d10d689d02370_87851_2013x428_resize_q80_h2_lanczos_3.webp" alt="CI Jobs" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fiteration-1%5C%2Fimages%5C%2Fci-2_hu8ee6a01855bb625afd6d10d689d02370_87851_2013x428_resize_lanczos_3.png"" /> <figcaption id="caption-The pipeline for Academy." class="caption-The-pipeline-for-Academy"> The pipeline for Academy. </figcaption> </figure> <p>As you can see, the test times could still be improved. That&rsquo;s mainly because of the cache and dependencies.</p> <p>The best part is that we can now deploy just what has changed. For example, changing something on the website won&rsquo;t deploy the backend, and vice versa.</p> <h2 id="migrating-out-of-firestore">Migrating out of Firestore</h2> <p><strong>We started Academy with Firestore as the database.</strong> It&rsquo;s easy to start a project with it, as there&rsquo;s no need to set up any infrastructure. It was also about 10x cheaper for our use case than hosted SQL.</p> <p>But working with the document model is far from ideal for typical web applications. You can&rsquo;t just run a quick SQL query to get the data you need among all collections. Migrations are painful, as you must write code to transform the data. And the emulator used in the local environment often has its own issues.</p> <p><strong>So, we decided to migrate to Postgres.</strong></p> <p>Do you know how useful it is to hide your storage code behind an interface in case you want to change the database? We promote it a lot, but we didn&rsquo;t care that much for the MVP. Some people claim you never change the database, so it&rsquo;s not an argument for using interfaces. But we had to deal with this exact use case.</p> <p>First, I changed the code to use the clean architecture pattern. Thanks to this, I could test both implementations with the same tests, and changing between databases was a single config change. <strong>It&rsquo;s super helpful to be able to quickly roll back a risky change like this.</strong> It also helped to clean up the code from a few hacky patterns we used in the MVP.</p> <p>Still, the change wasn&rsquo;t as easy as just replacing the implementations. <strong>Migrations are always more complex than they seem.</strong> I spent about two weeks finishing everything. The refactoring alone took a few days. Then, I had to set up the infrastructure, write the migration script, run tests, and debug issues. We also keep two regions of the platform (EU and US), so it was another factor to consider.</p> <p>It&rsquo;s a good example of a tough refactoring decision since this change did not improve the product in any way. In this case, we agreed it&rsquo;s worth it for the long-term benefits. Small issues add up over time, especially if it&rsquo;s something as critical for the project as the database.</p> <h2 id="jumping-around-the-exercises-in-the-cli">Jumping around the exercises in the CLI</h2> <p>We added a minor but often requested feature to our <code>tdl</code> CLI tool. You can now jump to another exercise you completed before.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fiteration-1%2Fimages%2Fjump.gif" alt="Jumping around the exercises" onerror="this.onerror='null';this.src=''" /> <h1 id="watermill">Watermill</h1> <p><a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">Watermill</a> is our open-source Go library for building event-driven applications. We released Watermill 1.4 last autumn but didn&rsquo;t wrap up everything we wanted to.</p> <p>A few PRs needed our attention, like <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-kafka%2Fpull%2F25" target="_blank">#25 in watermill-kafka</a>. Often, a PR looks like a trivial change, but then it turns out to be a rabbit hole. In this case, we had to adjust the tests to make the changes work, which was a solid day of Robert&rsquo;s work.</p> <p>Another example of tough dilemmas is the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F402" target="_blank">issue #402</a>. We often don&rsquo;t foresee all use cases when adding a new feature. Then, we must decide whether to keep the current behavior, even if it&rsquo;s incorrect, or change it and potentially break someone&rsquo;s code. Together with the fix, Robert updated the CQRS examples to show the best way to use the component.</p> <p>We deprecated the <code>gogo/proto</code> marshaler and introduced a new one, <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fcommit%2Fbb1925414b8671ae06e95385ba03ad4ad5f01c02" target="_blank"><code>cqrs.ProtoMarshaler</code></a>. It was long overdue, as <code>gogo/proto</code> has been deprecated for a long time.</p> <p>Finally, watermill-sql v4 is still a release candidate. New issues are coming in, and I&rsquo;ve been struggling to stabilize the tests with the recent changes.</p> <p>Maintaining an open-source project often feels like a full-time job, and we can&rsquo;t spend all our time like this. At the same time, we want Watermill to stay independent of external sponsors so no one tries to monetize it, which would probably push it towards a vendor lock-in approach.</p> <p>On the other hand, it has a nice synergy with our Go Event-Driven training, so we have a good reason and motivation to keep improving it. We&rsquo;ll try to find a reasonable balance here. As always, <strong>big shoutout to all contributors who help us push the project forward.</strong></p> <h1 id="content">Content</h1> <p>On our blog, it was a relatively quiet end of the year. I released <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthe-over-engineering-pendulum%2F">The Over-Engineering Pendulum</a>, a less technical post about my experience with over-engineering in startups. We want to share more on similar topics in the future.</p> <p>Last year, we ran a couple of Watermill release parties and live Q&amp;A sessions for Go Event-Driven trainees. We liked the format, and we wanted to experiment with it more. During this iteration, we started preparing to launch our live podcast, <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fno-silver-bullet%2F" target="_blank">No Silver Bullet</a>. You can read more on it in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2F">Robert&rsquo;s post</a>.</p> <p><strong>We have a long list of topics for the blog and the podcast.</strong> After long sessions of sorting them, we ended up with close to 150 items.</p> <h1 id="changing-the-newsletter-platform">Changing the Newsletter Platform</h1> <p>Our newsletter is the primary way to communicate with our readers and also how we sell our trainings. We&rsquo;ve been using ConvertKit to send emails, and it worked quite well, but the automation workflows were a huge pain to work with. We had to do some nasty workarounds to make sure the ebooks were delivered reliably to newsletter subscribers. The product wasn&rsquo;t improving, so we moved out to avoid spending more time on it.</p> <p>This wasn&rsquo;t the first migration we made. A few years ago, we moved off MailChimp for similar reasons. This time, we moved to MailerLite, which seems to be a better fit for us. It took about two weeks on Robert&rsquo;s end, but hopefully, it will save us headaches in the future.</p> <h1 id="plans-for-the-next-iteration">Plans for the next iteration</h1> <p>Looking forward, I hope we&rsquo;re done with migrations for now. 😅</p> <p>We have a few goals for 2025, but we try to not plan too far ahead. Often, we get a new random idea and want to pursue it right away (that&rsquo;s how the Academy started).</p> <p>We want to experiment with the live podcast episodes and see where they lead. Another experiment is adding new features to the Academy backed by LLMs. We have some ideas that should turn out more interesting than the usual &ldquo;we now use AI chatbot as customer support&rdquo; stories. Our main goal is to help people move through the training more smoothly.</p> <p>We definitely want to release another training in the coming months. We don&rsquo;t fully know the scope yet — we&rsquo;ll ask our newsletter subscribers for feedback soon.</p> <p>Another goal is finishing <em>Go with the Domain</em>, and releasing a revised edition. It&rsquo;s probably what we get asked for the most, but we&rsquo;re not sure about any dates yet.</p> <p>Thanks for reading! I hope to see you live during one of the No Silver Bullet episodes and on the Iteration #2 summary.</p>We quit our jobs to help people write software more mindfullyhttps://threedots.tech/post/we-quit-our-jobs/Sun, 19 Jan 2025 00:00:00 +0100https://threedots.tech/post/we-quit-our-jobs/<p><strong>Leaving a principal software engineer role while having a newborn kid, a mortgage to pay, and a house being built may not sound like the best idea. Still, I took a significant pay cut so I could make a living by educating people about software.</strong> Some people literally told me that I’m crazy.</p> <p>Well, Miłosz (the co-founder of this blog) and I decided to <strong>build a company we love to work at without VC funding.</strong></p> <p>It wasn’t an easy decision. Finances, of course, are always a concern. Also, we may not have as much time to work after hours as we used to. Let’s also be brutally honest; we won’t be alive forever. Still, we have families we need to provide for, and we want to build a company we can be proud of.</p> <p>Let’s start with how it all began.</p> <h2 id="our-journey">Our Journey</h2> <p><strong>I met Miłosz in 2008, when we were in high school. At that time, we started writing software together — after school and after work (sometimes, we even worked at the same company).</strong> <strong>For a decade, we have been working two full-time jobs.</strong></p> <p>Our after-hours projects were more or less successful. For example, in high school, one of our online game servers became the biggest one in Poland. We earned enough money so that Miłosz could buy a car, and we even threw high school parties. We were making good money for the time!</p> <p>During that time, we saw how different software development strategies work in different scenarios. <strong>We learned how to cut corners and write code in the simplest way possible.</strong> For two-team projects, following <em>&ldquo;the simple way&rdquo;</em> was usually fine, but for bigger projects, we needed to learn more advanced techniques.</p> <h3 id="starting-this-blog">Starting this Blog</h3> <p>A couple years later, when we were leading and mentoring teams, we saw that some techniques were hard to grasp for our less experienced teammates. Sharing our knowledge to a wider audience sounded like a good idea. <strong>So, 7 years ago, we started this blog. Since then, we have created 39 detailed articles.</strong> A good example is our article about <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2F" target="_blank">Live website updates with Go, SSE, and htmx</a>. <strong>There are a couple other articles describing SSE in Go, but most of them are quite surface-level.</strong> We like to go extra deep to show real-life scenarios.</p> <p>It’s great to see that there is a demand for in-depth content about complex techniques. In fact, every year, our blog is visited by around 270k people!</p> <h3 id="watermill---monetizing-open-source-while-staying-open-source">Watermill - Monetizing Open Source While Staying Open Source</h3> <p>Around the time we started our blog, we also worked on a project that explored how event-driven architecture would make the application faster and easier to develop. Unfortunately, most of our team didn’t have any experience with this kind of architecture.</p> <p><strong>At that time, we asked ourselves a question: <em>“How can we make building event-driven Go services as simple as HTTP API?”</em> This is how Watermill was born. It is now the most popular Go library to build event-driven applications</strong> (at least according to GitHub stars, since we don’t have a better measure).</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1470" height="974" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2Fstars_huf12319c201fc6cb4049f684437fc8606_111924_1470x974_resize_q80_h2_lanczos_3.webp" alt="GitHub stars history" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwe-quit-our-jobs%5C%2Fstars_huf12319c201fc6cb4049f684437fc8606_111924_1470x974_resize_lanczos_3.png"" /> <p>You may be asking yourself: <strong><em>“Don’t you want to monetise it? Is it time to change the license and start charging people?”</em>. That’s not our plan.</strong> We treat Watermill like a good marketing tool, and that’s it. We have also heard that Watermill is useful for people, and that makes us happy!</p> <p><strong>We are self-funded, so we can afford such extravagance.</strong> Truly, we don’t believe that potential income would be worth losing our community’s trust.</p> <h3 id="the-process-of-finding-better-ways-to-share-knowledge">The Process of Finding Better Ways to Share Knowledge</h3> <p>About 4 years ago (3 years after starting our blog), we released our book <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F" target="_blank">Go With The Domain</a>. We already knew the content we were creating was useful. This was the first in-depth book about Domain-Driven Design in Go. <strong>We later heard from people that their teams used it as a guide on how to develop their applications.</strong> But it was still missing one critical piece.</p> <p><em>You can read tons of books and watch hundreds of videos and take all sorts of courses, but if you don’t put into practice what you have learned, you won’t accomplish anything.</em>* There is no shortcut here; you need to mindfully use things you want to learn and evaluate the tradeoffs and hidden complexity. <strong>The best way to learn is by working on real-life projects. Not TODO apps or calculators; such applications are too simple. You need to learn how to overcome problems you’ll likely face while building real-life applications.</strong></p> <h3 id="learning-in-the-age-of-ai">Learning in the Age of AI</h3> <p>These days, everyone (including me and Miłosz) uses AI during the development. Learning practical skills is even more important now than ever before. <strong>AI is widening the gap between junior and principal engineers. Junior engineers now struggle to find a good job. Ironically, it’s hard to find skilled principal engineers who can build applications that AI can’t handle (and likely won’t be able to handle anytime soon).</strong></p> <p>Principal engineers can do their job faster, while junior engineers may spend hours mindlessly copy-pasting AI-generated code without actually learning anything. The demand for engineers who can do simple applications is decreasing, as AI can handle these jobs more efficiently.</p> <p>If you are afraid of AI taking your job, you should find a way to make yourself indispensable.</p> <h3 id="building-a-custom-learning-platform">Building a Custom Learning Platform</h3> <p><strong>Learning by practice requires a lot of time and patience. You need to find a good project theme and proper materials.</strong> It may also be frustrating when you are stuck and don’t have anybody to ask for guidance.</p> <p><strong>To speed up the learning process, we built a learning platform that supports users through building complex, real-life projects.</strong> It’s not yet another platform where you watch videos and code in the browser. It’s a way to put what you’ve learned to practical use.</p> <p>Building a learning platform from scratch requires more time than running an open-source project or a blog. We didn’t have a choice; we had to work on it during the weekends and on holidays. We have been very productive! People were surprised when we told them we were going to work full-time at Three Dots Labs. They asked, <em>“Aren&rsquo;t you already working at Three Dots Labs full-time?”</em></p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1280" height="804" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2Fhacking_hu03bf45678594842b9f3cba4a8d1ce4b9_221203_1280x804_resize_q80_h2_lanczos.webp" alt="Hacking" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwe-quit-our-jobs%5C%2Fhacking_hu03bf45678594842b9f3cba4a8d1ce4b9_221203_1280x804_resize_q80_lanczos.jpeg"" /> <div class="code-ref"> Friday night. 9:37 p.m. Time to party? Nope! Coding! </div></p> <p>We created a CLI tool, which clones and executes code. <strong>Our CLI will clone exercises for you, then execute them. It will save you a lot of time.</strong> This is why the feedback loop is so fast:</p> <video class="lazy" autoplay muted loop playsinline style="max-width: 638px; width: 100%; max-height: 445px; margin: 0 auto; display: block; object-fit: cover; border-radius: 7px;"> <source data-src="proxy.php?url=https%3A%2F%2Fcdn.threedots.tech%2Fcli-no-arrow.mp4%3FExpires%3D2693480430%26amp%3BKeyName%3Dcdn-bucket-key%26amp%3BSignature%3D5349BYRMdwJddLYsC5hE-9n42pA%3D" type="video/mp4"> </video> <h4 id="a-platform-is-nothing-without-content">A Platform is Nothing Without Content</h4> <p>Our first training on the platform is called “Go In One Evening.” <strong>Many people said, “No way! It’s not possible to learn Go that fast!” We love to build things that people don’t believe can be real.</strong></p> <p>We were leading teams of people who had never written a single line of Go in their lives. We saw for ourselves how quickly they became productive - so we know it’s possible. Data we have gathered confirms this.</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1422" height="414" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2Fexecutions_huc6f9b2f6be569b8a4121bf12f01f4ede_111670_1422x414_resize_q80_h2_lanczos_3.webp" alt="Go In One Evening statistics" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwe-quit-our-jobs%5C%2Fexecutions_huc6f9b2f6be569b8a4121bf12f01f4ede_111670_1422x414_resize_lanczos_3.png"" /> <div class="code-ref"> Statistics on how long it took to finish <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F" target='_blank'>Go In One Evening</a>:<br>50% of trainees finished training in less than 7h, and 75% finished training in less than 12h </div></p> <p>The agenda includes creating a simple social media platform clone and building the <code>ls</code> CLI tool. It&rsquo;s designed for software engineers who know how to code already but want to learn Go. We don’t waste your time with how variables and functions work; you already know that stuff. Instead, we focus on things that will be essential in your work.</p> <p>“Go In One Evening” is not our only training. <strong>We also have our flagship training, “Go Event-Driven,” where we teach how to build production-grade, event-driven systems. The core idea is the same: we teach by building real-life projects.</strong></p> <figure class="img-center" role="group" aria-describedby="caption-What people say about our trainings"> <img title="" loading="lazy" decoding="async" class="img " width="1992" height="824" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2Freviews_hu7c611640d0437dabef0d4eaa786a34ca_243706_1992x824_resize_q80_h2_lanczos_3.webp" alt="Our platform reviews" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwe-quit-our-jobs%5C%2Freviews_hu7c611640d0437dabef0d4eaa786a34ca_243706_1992x824_resize_lanczos_3.png"" /> <figcaption id="caption-What people say about our trainings" class="caption-What-people-say-about-our-trainings"> What people say about our trainings </figcaption> </figure> <p>I won’t spend much time describing how it works; I would encourage you to try it!</p> <h4 id="financials">Financials</h4> <p>Last year, 746 people joined our trainings, translating to $129,741 in revenue.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1046" height="362" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2Fpaddle_hu0312bd6fb9f0b263366bd67c47893529_44549_1046x362_resize_q80_h2_lanczos_3.webp" alt="Our earnings for the last year" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwe-quit-our-jobs%5C%2Fpaddle_hu0312bd6fb9f0b263366bd67c47893529_44549_1046x362_resize_lanczos_3.png"" /> <p>While that may sound like very good money, it’s less than we earned at our jobs. Our income is not stable yet, so we don’t take it for granted. <strong>If you have a steady office job, you may not appreciate that you will always be paid for your work. It doesn’t matter if you have a bad month.</strong> In this business, however, you can work on a project for a year, earn $0, and waste your time and money. Hackernews is full of such stories:</p> <figure class="img-center" role="group" aria-describedby="caption-Hacker News is full of “failure stories”"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="1714" height="748" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2Fhn_hu12192971bbc82210829b0a9b8114a361_563681_1714x748_resize_q80_h2_lanczos_3.webp" alt="Non-success stories" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwe-quit-our-jobs%5C%2Fhn_hu12192971bbc82210829b0a9b8114a361_563681_1714x748_resize_lanczos_3.png"" /> <figcaption id="caption-Hacker News is full of “failure stories”" class="caption-Hacker-News-is-full-of-failure-stories"> Hacker News is full of “failure stories” </figcaption> </figure> <hr> <h2 id="what-are-our-next-plans">What Are Our Next Plans?</h2> <p>This is where we are right now. What are our next steps?</p> <p>We want to continue doing all <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%23initiatives" target="_blank">initiatives</a>, like our blog and open-source projects. But we have a couple new things on our plate, including:</p> <ul> <li>starting our own podcast,</li> <li>doing more online trainings, and</li> <li>sharing what we are working on (building in public).</li> </ul> <p><strong>That may not sound very original; hundreds of people are doing the same things. But there are a couple of things we want to do that are totally different!</strong></p> <p>Let me explain.</p> <h3 id="why-do-we-need-another-live-podcast">Why Do We Need Another Live Podcast?</h3> <p>If you go to the gym, you may hear the term <em>bro science</em>. <strong>Some guys will tell you things like, <em>“Take this pill, and you can exercise less and still get bigger. Don’t be a moron who wastes time.”</em> For less experienced people, that may sound appealing, especially when you hear it from some big guys.</strong> The same thing happens in the software world.</p> <p><strong>There are a lot of bros who give software advice that is supposed to be a silver bullet and make your life easier.</strong> I don’t accuse anyone of lying; they are probably sharing what worked for them. However, people should stop advising as if it&rsquo;s the silver bullet for all problems and acting as though their solution is the only way to do things. <strong>Something that works for a solo developer will not necessarily work on a global financial platform. Social media algorithms make this problem worse because they favor polarization and extreme opinions.</strong></p> <p>You may ask, <em>“Why should I care?”</em>. Companies are good at rejecting people who don’t know how to solve company problems. If you don’t want to find yourself out of a job market, you need to learn how to adapt. <strong>I’ve been interviewing people for years, and I could easily tell whether someone is truly interested in solving real-world problems or just follows CV-driven development.</strong></p> <p>If you are ambitious, you probably prefer to work with passionate people as well. Joining such teams is not easy – they usually have a high bar to join them.</p> <p><strong>That&rsquo;s why we&rsquo;re creating a space where you can learn battle-tested programming practices that actually scale - from solo side hustles to complex enterprise systems. No bro science, just real-world experience.</strong></p> <h4 id="polarization-in-software">Polarization in Software</h4> <p>Polarization is quite prevalent in the software community now. There are two very distinct camps out there:</p> <ol> <li><strong>Engineers</strong>: For them, technical excellence is important; they care about maintainability</li> <li><strong>Shippers</strong>: For them, technology is secondary; they care mostly about shipping as many features that customers can use as quickly as possible; they are known to take shortcuts</li> </ol> <p>There is an ongoing war between those two camps. You often hear very different arguments from both camps, like: <em>“It won’t scale,”</em>, <em>“Just ship it,”</em> <em>“That’s not how it should be done,”</em> <em>“You don’t need unit tests,”</em><br> “It won’t be maintainable,”_ and <em>“Quality doesn’t matter; we can always rewrite it.”</em></p> <p>Both camps have valid points. <strong>The problem starts when people are unable to see the other side. Often, <em>Engineers</em> focus too much on technical excellence, and they may not be delivering anything usable for far too long. On the other hand, <em>Shippers</em>, after releasing a successful MVP, may wade into technical solutions that won’t work for bigger teams or products.</strong></p> <h4 id="cant-we-do----both">Can’t We Do . . . Both?</h4> <p>We should be open to diverse opinions. Wouldn’t it make more sense to merge these two approaches?</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="535" height="448" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2Fprod-eng.svg" alt="Product engineering" onerror="this.onerror='null';this.src=''" /> <p>It’s not easy to learn how to balance the pros and cons of shipping and engineering. This requires time. We need to learn how to build software, and we also need to understand how this software affects development. Miłosz covered this topic nicely in the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fthe-over-engineering-pendulum%2F">The Over-Engineering Pendulum</a> post. Even though Miłosz and I have been building software together for almost two decades, we still struggle with this balance.</p> <h3 id="new--no-silver-bullet-live-podcast-a-software-deep-dive-from-multiple-perspectives">NEW! 🎉 No Silver Bullet Live Podcast: A Software Deep Dive from Multiple Perspectives</h3> <p>No Silver Bullet ideally illustrates the message and unique selling point of our live podcast: showing multiple perspectives on which tools and techniques work in various scenarios.</p> <p><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fno-silver-bullet%2F"> <img title="" loading="lazy" decoding="async" class="img img-center" width="350" height="350" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2Fpodcast-cover_hu56b28629061b023ffd5ef89e0e0d6d0a_3440468_350x350_resize_q80_h2_lanczos_3.webp" alt="No Silver Bullet live podcast" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwe-quit-our-jobs%5C%2Fpodcast-cover_hu56b28629061b023ffd5ef89e0e0d6d0a_3440468_350x350_resize_lanczos_3.png"" /> </a></p> <p>Over the last 17 years, we have written software for various projects. From legacy companies to startups. From simple applications to global settlement platforms. Most of the time, we acted as hands-on engineers, but we used to be managers and founders as well. <strong>We want to share our unique, diverse experience with you. We want to teach you how to apply patterns and use appropriate tools for different projects.</strong></p> <p><strong>Like always, we aim to push the bar even higher. We want you to be part of our show so you can comment or ask questions about the topics we cover. It’s a win-win; we can go deeper into topics you care about and improve our content.</strong></p> <p>In No Silver Bullet, we’ll cover more general software engineering topics. But Go-related topics are also in the queue!</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="2050" height="1340" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2Fepisodes_hu27e7989666e1226152fc7546d5986ffc_1412134_2050x1340_resize_q80_h2_lanczos_3.webp" alt="Future episodes list" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwe-quit-our-jobs%5C%2Fepisodes_hu27e7989666e1226152fc7546d5986ffc_1412134_2050x1340_resize_lanczos_3.png"" /> <p><strong>Our setup is ready; we will go live for the first time at the beginning of March!</strong> We have a list of the first 5 episodes on our live podcast page. We want to go deep for every topic, so we will stream an episode every two weeks.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="960" height="704" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2Fsetup_hu4f80fdb9eafb21f9364749fba7af9d80_172983_960x704_resize_q80_h2_lanczos.webp" alt="Live podcast setup" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwe-quit-our-jobs%5C%2Fsetup_hu4f80fdb9eafb21f9364749fba7af9d80_172983_960x704_resize_q80_lanczos.jpeg"" /> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don’t miss our live podcast episodes! <br>Join our newsletter with over 18k subscribers! </span> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/we-quit-our-jobs/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="building-in-public"> <input type="hidden" name="fields[blog_series]" value="66;117;105;108;100;105;110;103;32;105;110;32;112;117;98;108;105;99"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h3 id="new--next-trainings-and-training-mentoring">NEW! 🎉 Next Trainings and Training Mentoring</h3> <p>Trainings are the best way for Three Dots Labs to make money. <strong>We also know that these trainings give people the most value for their money. Our trainings are a great way to leverage your skills and, hopefully, find a better-paying job.</strong></p> <p>You may be wondering: “When will the next training be available?”</p> <figure class="img-center" role="group" aria-describedby="caption-“When will training be available?” This is a question we hear on a daily basis!"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="1946" height="1296" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwe-quit-our-jobs%2Fasking_hu841627b2873e3a2e3a23f44306f42ff5_560424_1946x1296_resize_q80_h2_lanczos_3.webp" alt="People asking for new trainings" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwe-quit-our-jobs%5C%2Fasking_hu841627b2873e3a2e3a23f44306f42ff5_560424_1946x1296_resize_lanczos_3.png"" /> <figcaption id="caption-“When will training be available?” This is a question we hear on a daily basis!" class="caption-When-will-training-be-available-This-is-a-question-we-hear-on-a-daily-basis"> “When will training be available?” This is a question we hear on a daily basis! </figcaption> </figure> <p>Before we launch the next training, we want to finish our mentoring functionality, which will greatly help you learn these skills even faster. <strong>When you are learning something new, it’s important to be able to ask for help. This can also help you save a lot of time with problems that may be solved by someone more experienced quickly.</strong> With the current state of LLMs and our knowledge of your most common questions and obstacles, we can give you hints like we would sit next to you! I&rsquo;m already sure that it will be one of the coolest features that we have.</p> <p><strong>We aim to announce the next training sometime in March.</strong></p> <h4 id="why-we-are-focused-on-go-for-now">Why We Are Focused on Go for Now</h4> <p>Go is one of the best-paid languages, according to the JetBrains State of Developer Ecosystem Report 2024. You don’t need a PhD to learn it. Compared to other newer languages (like Rust), finding a job as a Go engineer is much easier.</p> <p>This year, we will remain focused on Go trainings. If you don’t know Go yet, how about learning a new programming language with the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F" target="_blank">&ldquo;Go In One Evening&rdquo;</a> training?</p> <h3 id="new--building-in-public">NEW! 🎉 Building in Public</h3> <p>We want to share our journey as we create a sustainable business that we love — and that you can benefit from. If you are interested in following us, we will share what worked and what didn’t work every 12 weeks. We’ll share information about our finances, and we’ll discuss what mistakes we made along the way. Miłosz is already working on a summary of the first iteration, which will be out soon!</p> <h3 id="aim-for-sustainable-growth">Aim for Sustainable Growth</h3> <p><strong>We aim to build a dream company that we genuinely love to work at, a company we don’t want to sell.</strong> We are realistic; we always keep a list of things we don&rsquo;t like to do but need to get done. It’s all a matter of finding the right balance.</p> <p>Often, the <em>“fun things”</em> are not justifiable from a business perspective. Since we don’t have any external deadlines, though, we can work on things that matter and make sure the products we are putting out are the best they can possibly be. We can spend this one extra mile to change good things into outstanding.</p> <p>Like with our trainings platform — we spent a lot of time optimising code execution and deploying it in multiple locations to make it fast. In effect, you may feel like it&rsquo;s executed locally. Would it change a lot if it were slower? Probably not, but it&rsquo;s cool!</p> <h3 id="will-we-succeed">Will We Succeed?</h3> <p>Our unusual approach may seem naive to many people. But based on our experience, we believe we will succeed. <strong>We always love to question the status quo and blaze new trails.</strong> After all, we created <a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">Watermill</a> and wrote <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F" target="_blank">Go With The Domain</a>. Now, we are working on online trainings.</p> <p>You can expect to see new articles on the blog, and we will continue working on <a href="#watermill---monetizing-open-source-while-staying-open-source">Watermill</a>.</p> <p>Will this strategy work? What if our assumptions are wrong? Will we have to return to full-time jobs to pay our mortgages?</p> <p><strong>The best way to follow our journey is by joining our newsletter. We prefer this over social media, as we aim for a long-term relationship with you. Social media posts show up, then disappear. An email stays in your inbox forever. We are also independent of its algorithms and owners.</strong></p> <p>If you are already one of our 17k newsletter subscribers, please tell your colleagues about our <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fno-silver-bullet%2F">live podcast</a>!</p> <p>Will you cheer us on? Do you think we will succeed? Let us know!</p>The Over-Engineering Pendulumhttps://threedots.tech/post/the-over-engineering-pendulum/Tue, 17 Dec 2024 00:00:00 +0100https://threedots.tech/post/the-over-engineering-pendulum/<p>I used to picture my dream job as this: I work for an early-stage startup that recently raised a round. The business idea is promising, there&rsquo;s much to build, everything seems possible, and we have time to prove the product-market fit. I join as the first engineer, lay the foundations, and pick the tech stack.</p> <p>Can you imagine a better gig? Finally, you get to do things your way. No more arguing and no old code dragging you down. This time, the project will have a high-quality codebase and no technical debt. Starting from scratch feels great — I&rsquo;m excited just thinking about it.</p> <p>In the end, I joined early-stage startups after someone else had laid the groundwork. The upside was that I could see the mistakes from a safe distance instead of making them myself. I ended up rethinking and refactoring existing codebases more than bootstrapping them.</p> <p>After a few rounds of this and hearing startup stories from friends, <strong>I was shocked at how similar startups are.</strong> (I love the irony — they try to become <em>unicorns</em>, after all.) One common issue stood out: <strong>Product teams often struggle due to the mess caused by over-engineering in the early days.</strong></p> <p>Once the team grows, it suddenly takes ages to deliver what was trivial before. Eventually, someone asks: &ldquo;Why does it take so long?&rdquo; The engineers are surprised and frustrated. It can&rsquo;t be the codebase — it follows the best industry standards.</p> <p><strong>It&rsquo;s extremely difficult for founders to notice this before it&rsquo;s too late.</strong> Often, it&rsquo;s a surprise even to the technical leaders.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="723" height="477" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthe-over-engineering-pendulum%2Fimages%2Fvelocity_hub41b6c2b28fe08ae322c78ac4807ce5c_45238_723x477_resize_q80_h2_lanczos_3.webp" alt="Velocity" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fthe-over-engineering-pendulum%5C%2Fimages%5C%2Fvelocity_hub41b6c2b28fe08ae322c78ac4807ce5c_45238_723x477_resize_lanczos_3.png"" /> <h2 id="how-over-engineering-starts">How Over-Engineering Starts</h2> <p>You&rsquo;ve probably seen over-engineering before. It&rsquo;s a two-person team using ten microservices and obsessing over tiny performance tweaks where they don&rsquo;t matter. Or adopting the popular but unstable technology and going big on infrastructure when a single VM would be good enough. Or reinventing solved problems and implementing them from scratch.</p> <p>It&rsquo;s fun to build complex systems and imagine that technology gives you the edge. But it&rsquo;s rarely true — most software is boring. Only small parts of what you create are unique.</p> <p>Still, there are reasons to think big. The founders have a grand vision for the technology, and they make it clear: <em>We need to build for scale. We&rsquo;re creating an operating system to disrupt our industry. We have to write the core software now to simply add features later.</em></p> <p>Perhaps this leads to overthinking what has already been solved for years. If you&rsquo;re building the next unicorn, why use one database running on one server? It doesn&rsquo;t make you unique among other wannabes. If you aim for the stars, better design something that supports this imaginary future.</p> <p>But you also need to deliver and validate quickly at this stage. You must keep the momentum as you build the product over time, so you fear getting lost in a messy codebase. You feel pressure to keep the standards high.</p> <p>It&rsquo;s tempting to over-engineer in the hope of avoiding this mess, but it only makes it worse. <strong>It takes the time that you could spend on actually helpful essentials.</strong> Who cares if you implemented a service mesh if your CI pipeline isn&rsquo;t green? Do you need that custom framework if the local environment is awful to work with? How will horizontal scaling save you from unmaintainable code?</p> <h2 id="high-quality">High Quality</h2> <p>Your challenge is finding the balance between over-engineering and building a messy prototype. It&rsquo;s somewhere between moving fast and doing things right.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="719" height="251" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthe-over-engineering-pendulum%2Fimages%2Fbalance_hu927565a5023343308232fa2ff7b8c070_26198_719x251_resize_q80_h2_lanczos_3.webp" alt="Balance" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fthe-over-engineering-pendulum%5C%2Fimages%5C%2Fbalance_hu927565a5023343308232fa2ff7b8c070_26198_719x251_resize_lanczos_3.png"" /> <p>We tend to see speed and high quality as opposites. You need to be fast to stay alive. But what does quality even mean?</p> <p>Engineers have strong opinions and vague ideas about quality. Some understand it as &ldquo;I don&rsquo;t like this code. It should use another design pattern.&rdquo; They are religious about formatting. They moan, &ldquo;Give us time to refactor.&rdquo; It&rsquo;s a naive take, far from product engineering. It uses quality as an excuse to boost the ego. Code shouldn&rsquo;t be the end goal.</p> <p>Another take is thinking of quality as flexibility. Building systems that are easy to extend and configure feels rewarding. It&rsquo;s cool if you want to solve code puzzles for fun. But if you want to deliver working software, you need some constraints. Focus on what&rsquo;s in front of you. Use a proven, boring tech stack. Stay away from <em>creative</em> or <em>dynamic</em> solutions. <strong>If you&rsquo;re getting excited about building something universal to solve issues in the future, check if you could simplify it into something you can use now.</strong></p> <p>You likely won&rsquo;t need to scale anytime soon. But if you invest in the basics, they will serve you well once you need to do it.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="861" height="478" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthe-over-engineering-pendulum%2Fimages%2Ffoundations_hudb85fb02766deaf7061bdb3b2101fb92_68968_861x478_resize_q80_h2_lanczos_3.webp" alt="The foundation you actually need" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fthe-over-engineering-pendulum%5C%2Fimages%5C%2Ffoundations_hudb85fb02766deaf7061bdb3b2101fb92_68968_861x478_resize_lanczos_3.png"" /> <h2 id="the-essentials">The Essentials</h2> <p>In the early days, I like to focus on the developer experience. Instead of creating a framework or platform that supports all possible use cases, I prefer to prepare a great environment for engineers to work in.</p> <p>There&rsquo;s much to build when starting from scratch, and new work quickly piles up. Good foundations limit time wasted on non-product things. They help you iterate quickly, which has real value compared to refactoring for code prettiness.</p> <p>Above all else, <strong>frustration is the worst productivity killer.</strong> It can radically change an engineer&rsquo;s mindset from &ldquo;it doesn&rsquo;t feel like work&rdquo; to &ldquo;it&rsquo;s the last thing I want to do.&rdquo;</p> <p>My first essential pick is <strong>an automated build system</strong> (a CI/CD pipeline) that runs tests and static analysis, builds the project and deploys it to production — ideally, a modern one that is easy to configure. The same steps run on feature branches, so the main branch is never broken.</p> <p>It doesn&rsquo;t have to be complex. Make it run <code>git pull</code> if that&rsquo;s what you need. It&rsquo;s easy to set up, and the benefits will be there every day for all engineers. You can&rsquo;t afford to have deploys blocked by broken changes or spend time discussing trivial issues a linter could catch.</p> <p>Next, prepare <strong>a great local development environment.</strong> It should be easy to set up, work everywhere, and run fast. You should be able to run the CI tools locally with the same versions and results. Use live code reloading so it&rsquo;s pleasant to work with.</p> <p>Finally, <strong>create guidelines on how to do common things.</strong> Think of the building blocks from which you build the application. Assembling elements is easier than creating a universal framework. It lets you keep some standards without trying to predict all future use cases.</p> <p>It&rsquo;s more about healthy constraints than doing things right. You don&rsquo;t want to discuss every week how to write an HTTP handler, call an external API, use database transactions, or run a function periodically. Decide on something and <strong>cut the discussions early so you can focus on writing the code that matters.</strong></p> <p>In any case, <strong>watch out for decisions that are hard to reverse.</strong> Opinionated frameworks make starting easy, but getting out of them can take long months.</p> <h2 id="aim-for-the-middle">Aim for the Middle</h2> <p>I like the pendulum metaphor: once we go to one extreme and get bitten by the results, we tend to avoid it at all costs and swing the other way. It applies to many things in life, including technology hype cycles.</p> <p>For example, consider delivering software. A service is a way of deploying code, not an isolation unit. So, use modules for isolation. But be ready to spin up a separate service if needed. Sticking with a massive monolith forever is as extreme as starting with a distributed system. But somehow, we tend to swing from one extreme to the other.</p> <p>Similarly, once we work with a messy codebase, we start paying extra attention to <em>quality</em>. Then, it&rsquo;s easy to go too far in the other direction and over-engineer your next project.</p> <p>I still find it challenging to find this balance. It&rsquo;s a topic that leads to never-ending discussions with no clear answers. There&rsquo;s one thing I&rsquo;m sure helps: <strong>If you have seen the extremes, at least you know to aim somewhere in the middle.</strong></p> <img title="" loading="lazy" decoding="async" class="img img-center" width="570" height="445" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthe-over-engineering-pendulum%2Fimages%2Fpendulum_hu26c63a64258b33130a4ba1e7afdacbbc_45645_570x445_resize_q80_h2_lanczos_3.webp" alt="Pendulum" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fthe-over-engineering-pendulum%5C%2Fimages%5C%2Fpendulum_hu26c63a64258b33130a4ba1e7afdacbbc_45645_570x445_resize_lanczos_3.png"" /> <h2 id="before-its-too-late">Before It&rsquo;s Too Late</h2> <p>When I hear of teams struggling with messy systems, it strikes me how a simple decision in the past could have prevented it. Once you&rsquo;re there, migrating out is incredibly difficult. In contrast, building from scratch feels much easier.</p> <p>I wish we had metrics to track this, but I&rsquo;m skeptical if that&rsquo;s possible. For example, achieving high test coverage is easy, but it doesn&rsquo;t mean your tests do anything meaningful.</p> <p>One thing I&rsquo;ve seen work well is <strong>regularly shipping small features.</strong> The key skill is splitting the work into chunks that make sense by themselves. The naive approach is splitting by artificial boundaries, such as one feature per <em>microservice</em> or separate backend and frontend parts. Instead, aim for changes that are end-to-end complete but small enough to ship quickly.</p> <p>Watch out for making it a target metric, though. It&rsquo;ll encourage shortcuts in the code, pulling you towards the messy codebase.</p>Watermill 1.4 Released (Event-Driven Go Library)https://threedots.tech/post/watermill-1-4/Tue, 29 Oct 2024 00:00:00 +0100https://threedots.tech/post/watermill-1-4/<p>It&rsquo;s Autumn over here, and it usually means another release of Watermill! 🍂 It&rsquo;s hard to believe it&rsquo;s already been five years since the v1.0 release.</p> <p>In case you&rsquo;re new to Watermill, here&rsquo;s TL;DR.</p> <p><strong><a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">Watermill</a> is a Go library for building message-driven or event-driven applications the easy way.</strong> Think of it like an HTTP router but for messages. It&rsquo;s a library, not a framework, so you don&rsquo;t need to change your architecture to use it.</p> <p>Over the last year, Watermill grew to <strong>7.5k GitHub stars and 55 contributors,</strong> just on the main repository. It supports <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2F" target="_blank">12 Pub/Subs</a> including Kafka, Redis Streams, NATS, Google Cloud Pub/Sub, Amazon SQS/SNS, SQL, and RabbitMQ.</p> <p>To learn more, see <a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">the documentation</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples" target="_blank">examples</a>.</p> <h2 id="whats-new">What&rsquo;s new?</h2> <p>Let&rsquo;s see what happened in Watermill over the last year.</p> <p>For the complete list of changes, see the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Freleases" target="_blank">GitHub releases page</a>.</p> <h3 id="hall-of-fame-">Hall of Fame 🏆</h3> <p><strong>Big thank you to everyone who contributed to all Watermill repositories since the last release! 💪</strong></p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FAlexCuse" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FAlexCuse.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @AlexCuse</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Farthurspa" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Farthurspa.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @arthurspa</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fbalenio" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fbalenio.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @balenio</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdferstay" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdferstay.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @dferstay</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdiegofrata" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdiegofrata.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @diegofrata</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdkotik" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdkotik.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @dkotik</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Ffinaltrip" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Ffinaltrip.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @finaltrip</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Figtulm" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Figtulm.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @igtulm</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fivaaaan" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fivaaaan.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @ivaaaan</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fkahowell" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fkahowell.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @kahowell</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FKirylJazzSax" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FKirylJazzSax.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @KirylJazzSax</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fllulioscesar" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fllulioscesar.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @llulioscesar</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Flooklose" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Flooklose.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @looklose</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmarczahnfc" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmarczahnfc.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @marczahnfc</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FMariscal6" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FMariscal6.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @Mariscal6</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fminghsu0107" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fminghsu0107.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @minghsu0107</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmountcount" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmountcount.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @mountcount</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FprOOrc" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FprOOrc.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @prOOrc</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FProvet" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FProvet.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @Provet</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fqulaz" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fqulaz.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @qulaz</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Frjfonseca" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Frjfonseca.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @rjfonseca</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Frussell" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Frussell.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @russell</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FSargtLa" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FSargtLa.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @SargtLa</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsevaorlov" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsevaorlov.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @sevaorlov</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fshaneing" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fshaneing.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @shaneing</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FSmixi" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FSmixi.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @Smixi</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fstong1994" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fstong1994.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @stong1994</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Ftobiasjaster" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Ftobiasjaster.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @tobiasjaster</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fthejoeejoee" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fthejoeejoee.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @thejoeejoee</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fthpk" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fthpk.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @thpk</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Funjello" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Funjello.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @unjello</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvitorarins" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvitorarins.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @vitorarins</a> </li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fwolfapple" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fwolfapple.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @wolfapple</a> </li> </ul> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="side-note-on-design">Side Note on Design</h4> <p>Watermill 1.0 was released in 2019, and the public API has been stable. I especially like the modular design, which allows you to combine low-level components like building blocks to create more complex features. You can see this in rather simple logic of components like <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fcomponents%2Ffanin%2Ffanin.go" target="_blank">FanIn</a>, <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fpubsub%2Fgochannel%2Ffanout.go" target="_blank">FanOut</a>, or the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fcomponents%2Fforwarder%2Fforwarder.go" target="_blank">Forwarder</a>.</p> <p>You can also notice this approach in the new features described below. We added a few simple components that work well separately, but you can combine them for more interesting effects.</p> </p></div> </div> <h3 id="delayed-postgresql-requeuer">Delayed PostgreSQL Requeuer</h3> <p>One common issue developers new to messaging have is dealing with errors. People often share an output like the one below, asking what&rsquo;s happening.</p> <figure class="img-center" role="group" aria-describedby="caption-Infinite loop of errors."> <img title="" loading="lazy" decoding="async" class="img " width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-4%2Fimages%2Ferrors.gif" alt="" onerror="this.onerror='null';this.src=''" /> <figcaption id="caption-Infinite loop of errors." class="caption-Infinite-loop-of-errors"> Infinite loop of errors. </figcaption> </figure> <p>What you see above is a single message failing to process and being retried indefinitely. It blocks other messages, so it seems like the application is stuck in an infinite loop.</p> <p>This is how most Pub/Subs work, and it&rsquo;s crucial if you care about message ordering. <strong>But sometimes, you don&rsquo;t want a broken message blocking the entire topic.</strong></p> <p>We used to work with Google Cloud Pub/Sub a lot, and one helpful feature it has is retrying messages. Once a message fails, you can make it reappear in the queue after a delay. Other messages are not blocked, and the failed message can be retried later.</p> <p>It&rsquo;s a helpful feature I missed in other Pub/Subs. There are ways to recreate this, but it&rsquo;s not as straightforward. For example, you can use delayed messages with RabbitMQ, but you need a plugin, so it&rsquo;s not compatible with all providers.</p> <p>I thought it would be cool to have this feature in Watermill working out of the box, and here we are!</p> <p><strong>Watermill now supports a universal requeuer component.</strong> It works with all Pub/Subs, but for now, it also requires PostgreSQL to be around (this can be extended in the future with more implementations). The PostgreSQL is used as an intermediate poison queue where the failed messages go until they are retried. It also supports delaying the messages, so you can avoid this infinite loop of errors.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1379" height="461" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-4%2Fimages%2Frequeue-diagram_hu16bff37e8fa7f1a77355e53289023bd5_42470_1379x461_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwatermill-1-4%5C%2Fimages%5C%2Frequeue-diagram_hu16bff37e8fa7f1a77355e53289023bd5_42470_1379x461_resize_lanczos_3.png"" /> <p>There are a few layers of how this works, and I&rsquo;ll dive deeper into them below. <strong>Let&rsquo;s first see the high-level API you need to get started.</strong> You can see the full example in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2F_examples%2Freal-world-examples%2Fdelayed-requeue%2Fmain.go" target="_blank">Watermill repository</a>.</p> <p>First, create the SQL requeuer. You need to pass a publisher to the regular Pub/Sub so it knows where to route messages.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">delayedRequeuer</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">sql</span><span class="p">.</span><span class="nf">NewPostgreSQLDelayedRequeuer</span><span class="p">(</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DelayedRequeuerConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">DB</span><span class="p">:</span> <span class="nx">db</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Publisher</span><span class="p">:</span> <span class="nx">redisPublisher</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Logger</span><span class="p">:</span> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2F_examples%2Freal-world-examples%2Fdelayed-requeue%2Fmain.go%23L40" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/delayed-requeue/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2F_examples%2Freal-world-examples%2Fdelayed-requeue%2Fmain.go%23L40" target="_blank">Full source</a> </div> <p>In your router, add middleware provided by the requeuer.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">router</span> <span class="o">:=</span> <span class="nx">message</span><span class="p">.</span><span class="nf">NewDefaultRouter</span><span class="p">(</span><span class="nx">logger</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">router</span><span class="p">.</span><span class="nf">AddMiddleware</span><span class="p">(</span><span class="nx">delayedRequeuer</span><span class="p">.</span><span class="nf">Middleware</span><span class="p">()</span><span class="o">...</span><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2F_examples%2Freal-world-examples%2Fdelayed-requeue%2Fmain.go%23L69" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/delayed-requeue/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2F_examples%2Freal-world-examples%2Fdelayed-requeue%2Fmain.go%23L69" target="_blank">Full source</a> </div> <p>Finally, run the requeuer:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">delayedRequeuer</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}()</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2F_examples%2Freal-world-examples%2Fdelayed-requeue%2Fmain.go%23L108" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/delayed-requeue/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2F_examples%2Freal-world-examples%2Fdelayed-requeue%2Fmain.go%23L108" target="_blank">Full source</a> </div> <p>That&rsquo;s it if you&rsquo;re happy with the default settings. The messages will be moved to the SQL poison queue if an error happens. After a delay, they will be moved back to the original topic.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> The <code>PostgreSQLDelayedRequeuer</code> is available in watermill-sql v1.4.0-rc.1: <strong>it&rsquo;s still a release candidate.</strong> Read more below in the section on watermill-sql. </p></div> </div> <h3 id="poison-queue-cli">Poison Queue CLI</h3> <p>When working with poison queues, the main challenge is visibility into what&rsquo;s happening. You need to know what messages are failing and why.</p> <p>Watermill 1.4 ships with a <code>pq</code> CLI tool to help you check this.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go install github.com/ThreeDotsLabs/watermill/tools/pq@latest </span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">DATABASE_URL</span><span class="o">=</span>postgres://user:password@localhost:5432/db </span></span><span class="line"><span class="cl">pq -backend postgres -topic requeue </span></span></code></pre></div> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-4%2Fimages%2Fpq.gif" alt="" onerror="this.onerror='null';this.src=''" /> <p>You can preview the poison queue, move messages back to the original topic, or delete them.</p> <h3 id="delayed-messages">Delayed Messages</h3> <p>Delayed messages can be useful for more than requeuing. Many scenarios involve delays: &ldquo;Ask the customer for feedback in two days&rdquo;, &ldquo;Send an invoice reminder in a week&rdquo;, etc. It&rsquo;s not a complex logic to implement, but if you already use events, you can leverage it and have delays working out of the box.</p> <p>Watermill&rsquo;s new <code>delay</code> package allows you to add delay metadata to messages. <strong>The delay metadata does nothing by itself. You need to use a Pub/Sub implementation that supports it to make it work.</strong> For now, it&rsquo;s supported by the PostgreSQL Pub/Sub with <code>NewDelayedPostgreSQLPublisher</code> and <code>NewDelayedPostgreSQLSubscriber</code>.</p> <p>There are two APIs you can use. If you work with raw messages, use <code>delay.Message</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">msg</span> <span class="o">:=</span> <span class="nx">message</span><span class="p">.</span><span class="nf">NewMessage</span><span class="p">(</span><span class="nx">watermill</span><span class="p">.</span><span class="nf">NewUUID</span><span class="p">(),</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;hello&#34;</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="nx">delay</span><span class="p">.</span><span class="nf">Message</span><span class="p">(</span><span class="nx">msg</span><span class="p">,</span> <span class="nx">delay</span><span class="p">.</span><span class="nf">For</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span> <span class="o">*</span> <span class="mi">10</span><span class="p">))</span> </span></span></code></pre></div><p>If you use the CQRS component, use <code>delay.WithContext</code> instead (since you can&rsquo;t access the message directly). You can also use <code>delay.Until</code> instead of <code>delay.For</code> to specify <code>time.Time</code> instead of <code>time.Duration</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">ctx</span> <span class="p">=</span> <span class="nx">delay</span><span class="p">.</span><span class="nf">WithContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">delay</span><span class="p">.</span><span class="nf">Until</span><span class="p">(</span><span class="nx">invoiceDueDate</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">err</span> <span class="o">:=</span> <span class="nx">commandBus</span><span class="p">.</span><span class="nf">Send</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>See the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Fdelayed-messages" target="_blank">full example</a> for more details.</p> <h3 id="postgresql-queue">PostgreSQL Queue</h3> <p>The Delayed PostgreSQL Pub/Sub is based on the new <em>queue</em> schema adapter.</p> <p>It&rsquo;s a simpler schema that doesn&rsquo;t support consumer groups. In return, it allows you to specify a custom <code>WHERE</code> clause to filter messages by a custom condition, like metadata or payload fields. You can also enable <em>delete on ack</em>, so the table doesn&rsquo;t grow with time: the messages are deleted after they are acknowledged.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">sub</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">sql</span><span class="p">.</span><span class="nf">NewSubscriber</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">sql</span><span class="p">.</span><span class="nx">SubscriberConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">SchemaAdapter</span><span class="p">:</span> <span class="nx">sql</span><span class="p">.</span><span class="nx">PostgreSQLQueueSchema</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">GenerateWhereClause</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">params</span> <span class="nx">sql</span><span class="p">.</span><span class="nx">GenerateWhereClauseParams</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="p">[]</span><span class="nx">any</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="s">&#34;metadata-&gt;&gt;&#39;urgent&#39; = true&#34;</span><span class="p">,</span> <span class="p">[]</span><span class="nx">any</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">OffsetsAdapter</span><span class="p">:</span> <span class="nx">sql</span><span class="p">.</span><span class="nx">PostgreSQLQueueOffsetsAdapter</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">DeleteOnAck</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><h3 id="requeuer">Requeuer</h3> <p>The <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fcomponents%2Frequeuer%2Frequeuer.go" target="_blank"><code>Requeuer</code></a> component is a wrapper on the <code>Router</code>, similar to the <code>Forwarder</code>, but simpler. It listens to messages on one topic and routes them to another, dynamically generated one.</p> <p>You can combine it with the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fmessage%2Frouter%2Fmiddleware%2Fpoison.go" target="_blank"><code>Poison</code></a> middleware that moves failed messages to a separate topic. You can use the <code>Requeuer</code> to move them back to the original topic based on the Poison metadata. This is exactly what the <code>PostgreSQLDelayedRequeuer</code> does. It combines the <code>Requeuer</code>, <code>Poison</code> middleware, <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fmessage%2Frouter%2Fmiddleware%2Fdelay_on_error.go" target="_blank"><code>DelayOnError</code></a> middleware, and PostgreSQL queue adapter and hides all this behind a simple API.</p> <h3 id="aws-snssqs-pubsub-v10">AWS SNS/SQS Pub/Sub v1.0</h3> <p>Using SNS or SQS makes a lot of sense when your services are hosted on AWS. <strong>Adding support for Pub/Subs managed by AWS was one of the most requested message brokers.</strong></p> <p>In the current release, we have added support for both SNS and SQS. Thanks to that, you can choose the tool that fits your needs better.</p> <p><strong>While adding support for SQS is not challenging, implementing a proper Pub/Sub on top of SNS is not trivial.</strong> SNS differs from other Pub/Subs: it&rsquo;s not just a matter of subscribing to a topic and receiving messages. You need to create a queue, subscribe to the topic, create a valid access policy, and then receive messages from the queue. It also doesn&rsquo;t help that AWS error messages are sometimes a bit cryptic.</p> <p><strong>The good news is that we already made this work for you, and you can use it out of the box.</strong></p> <img title="" loading="lazy" decoding="async" class="img img-background img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-4%2Fimages%2Faws.svg" alt="" onerror="this.onerror='null';this.src=''" /> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">newSubscriber</span> <span class="o">:=</span> <span class="kd">func</span><span class="p">(</span><span class="nx">name</span> <span class="nx">subName</span><span class="p">)</span> <span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">Subscriber</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">subscriberConfig</span> <span class="o">:=</span> <span class="nx">sns</span><span class="p">.</span><span class="nx">SubscriberConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">AWSConfig</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">Config</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Credentials</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">AnonymousCredentials</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">OptFns</span><span class="p">:</span> <span class="nx">snsOpts</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">TopicResolver</span><span class="p">:</span> <span class="nx">topicResolver</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">GenerateSqsQueueName</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">snsTopic</span> <span class="nx">sns</span><span class="p">.</span><span class="nx">TopicArn</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">topic</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">sns</span><span class="p">.</span><span class="nf">ExtractTopicNameFromTopicArn</span><span class="p">(</span><span class="nx">snsTopic</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="s">&#34;&#34;</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;%v-%v&#34;</span><span class="p">,</span> <span class="nx">topic</span><span class="p">,</span> <span class="nx">subName</span><span class="p">),</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">sqsSubscriberConfig</span> <span class="o">:=</span> <span class="nx">sqs</span><span class="p">.</span><span class="nx">SubscriberConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">AWSConfig</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">Config</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Credentials</span><span class="p">:</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">AnonymousCredentials</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">OptFns</span><span class="p">:</span> <span class="nx">sqsOpts</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">sns</span><span class="p">.</span><span class="nf">NewSubscriber</span><span class="p">(</span><span class="nx">subscriberConfig</span><span class="p">,</span> <span class="nx">sqsSubscriberConfig</span><span class="p">,</span> <span class="nx">logger</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>As always, you can find a fully working example (including a local emulator so that you can run it in Docker locally):</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Fpubsubs%2Faws-sqs" target="_blank">SQS example</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Fpubsubs%2Faws-sns" target="_blank">SNS example</a></li> </ul> <p>Big thanks to <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FMariscal6" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2FMariscal6.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @Mariscal6</a> for creating a big part of the implementation!</p> <h3 id="watermill-amqp-v3-released">watermill-amqp: v3 released</h3> <p>We released a major version of the AMQP Pub/Sub. The breaking change was introduced in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-amqp%2Fpull%2F27" target="_blank">PR #27</a> originally contributed by <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Ftobiasjaster" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Ftobiasjaster.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @tobiasjaster</a> .</p> <p>The <code>BuildTopology</code> method of <code>TopologyBuilder</code> was missing the routing key. We decided to introduce a <code>params</code> struct to make it more flexible in the future.</p> <h4 id="upgrading">Upgrading</h4> <p>You don&rsquo;t need to do anything if you use the default <code>TopologyBuilder</code>.</p> <p>If you implement a custom <code>TopologyBuilder</code>, update the <code>BuildTopology</code> method. It now takes <code>params amqp.BuildTopologyParams</code> instead of <code>queueName string, exchangeName string</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">- BuildTopology(channel *amqp.Channel, queueName string, exchangeName string, config Config, logger watermill.LoggerAdapter) error </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+ BuildTopology(channel *amqp.Channel, params BuildTopologyParams, config Config, logger watermill.LoggerAdapter) error </span></span></span></code></pre></div><p>Inside the method, replace <code>queueName</code> with <code>params.QueueName</code> and <code>exchangeName</code> with <code>params.ExchangeName</code>.</p> <h3 id="watermill-sql-v3-and-v4">watermill-sql v3 and v4</h3> <p>We introduced the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-sql%2Freleases%2Ftag%2Fv3.0.0" target="_blank">v3 major version</a> of watermill-sql a while ago. This made the PostgreSQL implementation more bulletproof.</p> <p>Now, the v4 is around the corner. Similar to watermill-amqp, we decided to make <code>SchemaAdapter</code> and <code>OffsetsAdapter</code> interfaces more flexible. Instead of raw arguments, they now take a <code>params</code> struct and always return an error.</p> <p>It&rsquo;s a release candidate because we want to include one more breaking change so we don&rsquo;t bump the major version again soon. There&rsquo;s an ongoing development in <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-sql%2Fpull%2F29" target="_blank">PR #29: streamline the database interfaces and add pgx adapter</a> contributed by <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjulesjcraske" target="_blank"><img src="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjulesjcraske.png%3Fsize%3D50" width="25" height="25" style="margin: 0;" loading="lazy" decoding="async"/> @julesjcraske</a> . The aim is to allow watermill-sql to be used with pgx and ORMs not compatible with <code>database/sql</code>, a common request from the community. Stay tuned!</p> <h3 id="mapping-watermills-log-levels-to-slog">Mapping Watermill&rsquo;s log levels to slog</h3> <p>Sometimes, the default <code>INFO</code> logs of Watermill may be too verbose for your application. To help you with that, we added a new feature to map Watermill&rsquo;s log levels to slog levels.</p> <p>You can now map Watermill&rsquo;s log levels to slog levels with <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fslog.go" target="_blank"><code>NewSlogLoggerWithLevelMapping</code></a>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">logger</span> <span class="o">:=</span> <span class="nf">NewSlogLoggerWithLevelMapping</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="kc">nil</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">map</span><span class="p">[</span><span class="nx">slog</span><span class="p">.</span><span class="nx">Level</span><span class="p">]</span><span class="nx">slog</span><span class="p">.</span><span class="nx">Level</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">slog</span><span class="p">.</span><span class="nx">LevelInfo</span><span class="p">:</span> <span class="nx">slog</span><span class="p">.</span><span class="nx">LevelDebug</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><h3 id="new-docs-website">New docs website</h3> <p>We refreshed Watermill&rsquo;s documentation. It should be now easier to find your way around it.</p> <p>See the new version at <a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">watermill.io</a>.</p> <h3 id="refreshed-logo">Refreshed logo</h3> <p>Watermill&rsquo;s Gopher got a new look! 🎨</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="400" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-4%2Fimages%2Fwatermill.svg" alt="" onerror="this.onerror='null';this.src=''" /> <h3 id="new-examples">New Examples</h3> <p>In case you missed it, there&rsquo;s <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx" target="_blank">a new example featuring Server-Sent Events and htmx</a>.</p> <p>You may also like the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Ftree%2Fmaster%2F05-distributed-transactions" target="_blank">examples on distributed transactions</a> where the outbox pattern is used with Watermill.</p> <p>There are also the mentioned above examples on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Fdelayed-messages" target="_blank">delayed messages</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Fdelayed-requeue" target="_blank">delayed requeue</a></p> <h3 id="releases-dashboard">Releases Dashboard</h3> <p>You can now track the releases of all Watermill repositories in one place: <a href="proxy.php?url=https%3A%2F%2Freleases.threedots.tech" target="_blank">releases.threedots.tech</a></p> <h3 id="terraform">Terraform</h3> <p>We set up a Terraform repository to manage all Pub/Sub repositories. If you want to take a look, it&rsquo;s here: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-terraform" target="_blank">watermill-terraform</a>.</p> <h2 id="celebrating-the-launch">Celebrating the Launch</h2> <p>Just like the last two times, we&rsquo;re hosting a live event to celebrate the release of Watermill v1.4!</p> <p><strong>We will share more details about the changes, show live demos, and answer your questions during Q&amp;A.</strong></p> <p>We start on October 30 at:</p> <ul> <li>🇪🇺 6 PM CEST</li> <li>🇺🇸 9 AM PT / 12 PM EST</li> <li>🇮🇳 9:30 PM IST</li> </ul> <p>Join us <a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DJr3BbrtNdUA" target="_blank">on YouTube</a> (<a href="proxy.php?url=https%3A%2F%2Fwww.google.com%2Fcalendar%2Frender%3Faction%3DTEMPLATE%26amp%3Bamp%3Bdates%3D20241030T170000Z%252F20241030T180000Z%26amp%3Bamp%3Bdetails%3DYouTube%253A%2520https%253A%252F%252Fwww.youtube.com%252Fwatch%253Fv%253DJr3BbrtNdUA%26amp%3Bamp%3Blocation%3Dhttps%253A%252F%252Fwww.youtube.com%252Fwatch%253Fv%253DJr3BbrtNdUA%26amp%3Bamp%3Btext%3DWatermill%2520v1.4%2520Release%2520party%2520%25F0%259F%258E%2589" target="_blank">Add the event to Google Calendar</a>)</p> <p>We will also have news about our <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fevent-driven%2F" target="_blank">Go Event-Driven training</a>.</p> <p>We have a tradition of running a quiz during the live events. The prize is <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgophers%2F">a unique crotchet Gopher</a>. 😎</p> <figure class="img-center" role="group" aria-describedby="caption-Meet the Gophers at our live event!"> <img title="" loading="lazy" decoding="async" class="img " width="1600" height="1583" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-4%2Fimages%2Fgophers_huda607f6f59ea068ea5547488d17d40af_501062_1600x1583_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwatermill-1-4%5C%2Fimages%5C%2Fgophers_huda607f6f59ea068ea5547488d17d40af_501062_1600x1583_resize_q80_lanczos.jpg"" /> <figcaption id="caption-Meet the Gophers at our live event!" class="caption-Meet-the-Gophers-at-our-live-event"> Meet the Gophers at our live event! </figcaption> </figure> <p>See you there!</p>Optimising and Visualising Go Tests Parallelism: Why more cores don't speed up your Go testshttps://threedots.tech/post/go-test-parallelism/Thu, 17 Oct 2024 00:00:00 +0200https://threedots.tech/post/go-test-parallelism/<p>Recently, I struggled for a couple of hours to understand why the API tests of one project were slow. In theory, we designed tests to run in a fully parallel way &ndash; the duration of tests should be close to the longest-running test. Unfortunately, the reality was different. <strong>Tests took 7x longer than the slowest test without using 100% available resources.</strong></p> <p>In this article, I will show you a few techniques to help you understand and optimize your tests execution. Optimizing tests that use CPU efficiently is simple (in most cases just add more resources). We&rsquo;ll focus on a scenario of optimizing single-threaded, CPU-heavy integration, component, API, and E2E tests.</p> <h2 id="its-hard-to-fix-a-problem-that-you-cant-see">It&rsquo;s hard to fix a problem that you can&rsquo;t see</h2> <p>It&rsquo;s difficult to understand how tests run from the output generated by <code>go test</code>. <strong>You can see how long each test took, but you don&rsquo;t know how long a test waited to run. You also can&rsquo;t see how many tests ran in parallel.</strong> It becomes even harder when your project has thousands of tests.</p> <p><strong>Surprisingly, I didn&rsquo;t find any tool that helps visualize Go test execution. As a hobbyist frontend engineer, I decided to build my own over a weekend.</strong></p> <h3 id="vgthttpsgithubcomroblaszczakvgt-the-missing-tool-for-visualizing-go-tests"><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target="_blank"><code>vgt</code></a> the missing tool for Visualizing Go Tests</h3> <p><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target="_blank"><code>vgt</code></a> can parse JSON Go test output to create a visualization. The quickest way to use it is by calling:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go <span class="nb">test</span> -json ./... <span class="p">|</span> go run github.com/roblaszczak/vgt@latest </span></span></code></pre></div><p>or by installing and running</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go install -u github.com/roblaszczak/vgt </span></span><span class="line"><span class="cl">go <span class="nb">test</span> -json ./... <span class="p">|</span> vgt </span></span></code></pre></div><p>In a perfect world, this is how ideal test execution of tests that are not CPU-bound should look:</p> <figure class="img-center" role="group" aria-describedby="caption-All non-CPU bound tests are executed in parallel. Output from vgt."> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt1.png" class="glightbox" data-glightbox="description: .caption-All-non-CPU-bound-tests-are-executed-in-parallel-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="2674" height="1456" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt1_hu1de67792d4d0b303b23c10a8c0cf8eff_269482_2674x1456_resize_q80_h2_lanczos_3.webp" alt="vgt cli tool" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fgo-test-parallelism%5C%2Fvgt1_hu1de67792d4d0b303b23c10a8c0cf8eff_269482_2674x1456_resize_lanczos_3.png"" /> </a> <figcaption id="caption-All non-CPU bound tests are executed in parallel. Output from vgt." class="caption-All-non-CPU-bound-tests-are-executed-in-parallel-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> All non-CPU bound tests are executed in parallel. Output from <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target='_blank'>vgt</a>. </figcaption> </figure> <p>Each bar represents the test execution time of a single test or subtest. Unfortunately, the tests I was recently debugging looked more like this:</p> <figure class="img-center" role="group" aria-describedby="caption-These tests can be optimized. Output from vgt."> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt2.png" class="glightbox" data-glightbox="description: .caption-These-tests-can-be-optimized-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="2674" height="1456" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt2_hu1de67792d4d0b303b23c10a8c0cf8eff_254560_2674x1456_resize_q80_h2_lanczos_3.webp" alt="vgt cli tool" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fgo-test-parallelism%5C%2Fvgt2_hu1de67792d4d0b303b23c10a8c0cf8eff_254560_2674x1456_resize_lanczos_3.png"" /> </a> <figcaption id="caption-These tests can be optimized. Output from vgt." class="caption-These-tests-can-be-optimized-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> These tests can be optimized. Output from <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target='_blank'>vgt</a>. </figcaption> </figure> <p>While the CPU was not fully used, tests ran one by one. It is also a good sign: we have a big room for improvement.</p> <h2 id="parallelizing-go-tests">Parallelizing Go tests</h2> <p>By default, in Go all tests within a single package are run sequentially. <strong>This is not a problem when tests use all CPU cores efficiently and tests are split into multiple packages. But our CPU wastes cycles when a database query, API call, or sleep blocks tests &ndash; especially if we have a lot of tests in a single package.</strong></p> <figure class="img-center" role="group" aria-describedby="caption-Tests without t.Parallel() flag. Output from vgt."> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-no-parallel.png" class="glightbox" data-glightbox="description: .caption-Tests-without-tParallel-flag-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="2672" height="1456" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-no-parallel_huae1fbf201fd4c69a0b320230e413e2e4_173838_2672x1456_resize_q80_h2_lanczos_3.webp" alt="vgt cli tool" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fgo-test-parallelism%5C%2Fvgt-no-parallel_huae1fbf201fd4c69a0b320230e413e2e4_173838_2672x1456_resize_lanczos_3.png"" /> </a> <figcaption id="caption-Tests without t.Parallel() flag. Output from vgt." class="caption-Tests-without-tParallel-flag-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> Tests without t.Parallel() flag. Output from <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target='_blank'>vgt</a>. </figcaption> </figure> <p>To fix this problem, <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Ftesting%23T" target="_blank"><code>*testing.T</code></a> provides the <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Ftesting%23T.Parallel" target="_blank"><code>t.Parallel()</code></a> method, which allows tests and sub-tests to run in parallel.</p> <div class="notice warning"> <div class="notice-head"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20" > <path d="M10 1L1 18h18L10 1zM10 7v6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <circle cx="10" cy="15.5" r="1" fill="currentColor"/> </svg> <p>Warning</p> </div> <div class="notice-body"><p> <p><code>t.Parallel()</code> is not a silver bullet and should be used with caution.</p> <p><strong>Use <code>t.Parallel()</code> only for tests that have blocking operations like database queries, API calls, sleeps.</strong> It can also make sense for CPU-heavy tests using only a single-core.</p> <p>For fast unit tests the overhead of using <code>t.Parallel()</code> will be higher than running them sequentially. <strong>In other words, using <code>t.Parallel()</code> for lightweight unit tests will likely make them slower.</strong></p> </p></div> </div> <h3 id="parallelism-limit">Parallelism limit</h3> <p>Even if you use <code>t.Parallel()</code>, it doesn&rsquo;t mean that all tests will run in parallel. To simulate this scenario, I wrote an example test that will simulate 100 tests doing API calls.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestApi_parallel_subtests</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="p">&lt;</span> <span class="mi">100</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;subtest_%d&#34;</span><span class="p">,</span> <span class="nx">i</span><span class="p">),</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nf">simulateSlowCall</span><span class="p">(</span><span class="mi">1</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">simulateSlowCall</span><span class="p">(</span><span class="nx">sleepTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="nx">sleepTime</span> <span class="o">+</span> <span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nf">Duration</span><span class="p">(</span><span class="nx">rand</span><span class="p">.</span><span class="nf">Intn</span><span class="p">(</span><span class="mi">1000</span><span class="p">))</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>As long as the target server is not overloaded and tests are appropriately designed, we should be able to run all tests in parallel. <strong>In this case, running all tests should take at most 2 seconds. But it took over 16 seconds instead.</strong></p> <p>Despite using <code>t.Parallel()</code>, the execution graph shows many gray bars representing pauses. Tests marked as <code>PAUSED</code> are limited due to the parallelism limit.</p> <figure class="img-center" role="group" aria-describedby="caption-These tests can be optimized. Output from vgt."> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-parallel-limit.png" class="glightbox" data-glightbox="description: .caption-These-tests-can-be-optimized-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="2692" height="1456" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-parallel-limit_huc04bfd19741158cd65b05785253e3246_882845_2692x1456_resize_q80_h2_lanczos_3.webp" alt="vgt cli tool" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fgo-test-parallelism%5C%2Fvgt-parallel-limit_huc04bfd19741158cd65b05785253e3246_882845_2692x1456_resize_lanczos_3.png"" /> </a> <figcaption id="caption-These tests can be optimized. Output from vgt." class="caption-These-tests-can-be-optimized-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> These tests can be optimized. Output from <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target='_blank'>vgt</a>. </figcaption> </figure> <p>First, let&rsquo;s understand how tests with <code>t.Parallel()</code> run. If you&rsquo;re curious, you can check the source code of the <a href="proxy.php?url=https%3A%2F%2Fcs.opensource.google%2Fgo%2Fgo%2F%26amp%3B%2343%3B%2Frefs%2Ftags%2Fgo1.23.1%3Asrc%2Ftesting%2Ftesting.go%3Bl%3D1800" target="_blank"><code>testing</code></a> package. What&rsquo;s important, test parallelism is set to <code>runtime.GOMAXPROCS(0)</code> by default, which returns the number of cores reported by the OS.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">parallel</span> <span class="p">=</span> <span class="nx">flag</span><span class="p">.</span><span class="nf">Int</span><span class="p">(</span><span class="s">&#34;test.parallel&#34;</span><span class="p">,</span> <span class="nx">runtime</span><span class="p">.</span><span class="nf">GOMAXPROCS</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="s">&#34;run at most `n` tests in parallel&#34;</span><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fcs.opensource.google%2Fgo%2Fgo%2F%26amp%3B%2343%3B%2Frefs%2Ftags%2Fgo1.23.1%3Asrc%2Ftesting%2Ftesting.go%3Bl%3D442" target="_blank">cs.opensource.google/go/go/&#43;/refs/tags/go1.23.1:src/testing/testing.go;l=442</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fcs.opensource.google%2Fgo%2Fgo%2F%26amp%3B%2343%3B%2Frefs%2Ftags%2Fgo1.23.1%3Asrc%2Ftesting%2Ftesting.go%3Bl%3D442" target="_blank">Full source</a> </div> <p>On my Macbook, <code>runtime.GOMAXPROCS(0)</code> returns 10 (as I have a 10-core CPU). <strong>In other words, it limits tests run in parallel to 10.</strong></p> <p>Limiting tests to the number of our cores makes sense when they are CPU-bound. More parallelism will force our OS to do more expensive context switching. But when we are calling a database, API, or any blocking I/O, it makes tests longer without fully using our resources.</p> <p>The situation can be even worse when API tests run in CI against an environment deployed in a separate VM or cloud. Often, CI runners for API tests may have 1-2 CPUs. With 1 vCPU, API tests will run one by one. We can simulate this by setting the env <code>GOMAXPROCS=1</code>.</p> <figure class="img-center" role="group" aria-describedby="caption-Tests with 1 vCPU &ndash; this is how it may look in CI. Output from vgt."> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-maxproc1.png" class="glightbox" data-glightbox="description: .caption-Tests-with-1-vCPU----this-is-how-it-may-look-in-CI-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="2492" height="1444" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-maxproc1_hu3ffbddc5f30f178cd5f094933617fcd3_602246_2492x1444_resize_q80_h2_lanczos_3.webp" alt="vgt cli tool" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fgo-test-parallelism%5C%2Fvgt-maxproc1_hu3ffbddc5f30f178cd5f094933617fcd3_602246_2492x1444_resize_lanczos_3.png"" /> </a> <figcaption id="caption-Tests with 1 vCPU &ndash; this is how it may look in CI. Output from vgt." class="caption-Tests-with-1-vCPU----this-is-how-it-may-look-in-CI-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> Tests with 1 vCPU &ndash; this is how it may look in CI. Output from <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target='_blank'>vgt</a>. </figcaption> </figure> <p>Parallelism is effectively set to 1, so we see a lot of gray bars representing waiting time. To fix this problem, we can use the <code>-parallel</code> (or <code>-test.parallel</code> &ndash; they have the same effect) flag. <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fcmd%2Fgo%2Finternal%2Ftest" target="_blank">Go documentation says</a>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"> -parallel n </span></span><span class="line"><span class="cl"> Allow parallel execution of test functions that call t.Parallel, and </span></span><span class="line"><span class="cl"> fuzz targets that call t.Parallel when running the seed corpus. </span></span><span class="line"><span class="cl"> The value of this flag is the maximum number of tests to run </span></span><span class="line"><span class="cl"> simultaneously. </span></span><span class="line"><span class="cl"> While fuzzing, the value of this flag is the maximum number of </span></span><span class="line"><span class="cl"> subprocesses that may call the fuzz function simultaneously, regardless of </span></span><span class="line"><span class="cl"> whether T.Parallel is called. </span></span><span class="line"><span class="cl"> By default, -parallel is set to the value of GOMAXPROCS. </span></span><span class="line"><span class="cl"> Setting -parallel to values higher than GOMAXPROCS may cause degraded </span></span><span class="line"><span class="cl"> performance due to CPU contention, especially when fuzzing. </span></span><span class="line"><span class="cl"> Note that -parallel only applies within a single test binary. </span></span><span class="line"><span class="cl"> The &#39;go test&#39; command may run tests for different packages </span></span><span class="line"><span class="cl"> in parallel as well, according to the setting of the -p flag </span></span><span class="line"><span class="cl"> (see &#39;go help build&#39;). </span></span></code></pre></div><div class="notice tip"> <div class="notice-head"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillrule="evenodd" cliprule="evenodd" d="M12 0c6.6274.0 12 5.37258 12 12 0 6.6274-5.3726 12-12 12C5.37258 24 0 18.6274.0 12 0 5.37258 5.37258.0 12 0zm0 2.4C6.69807 2.4 2.4 6.69807 2.4 12c0 5.3019 4.29807 9.6 9.6 9.6 5.3019.0 9.6-4.2981 9.6-9.6.0-5.30193-4.2981-9.6-9.6-9.6zm3.9515 5.15147L9.6 13.9029 8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515c-.46863.4686-.46863 1.2284.0 1.697l2.4 2.4C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485l7.2-7.19997C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147c-.468599999999999-.46863-1.2284-.46863-1.697.0z" fill="currentcolor"></path> </svg> <p>Tip</p></div> <div class="notice-body"> <p> <p>Don&rsquo;t change <code>GOMACPROCS</code> to a value higher than the available cores to force more parallelism.</p> <p>It will have more effects than just on tests — it will spawn more Go threads than cores. It will lead to more expensive context switching and may slow down CPU-bound tests.</p> </p> </div> </div> <p>Let&rsquo;s see how the same test will behave with the extra <code>-parallel 100</code> flag:</p> <figure class="img-center" role="group" aria-describedby="caption-go test ./&hellip; -json -parallel=100 | vgt"> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-parallel.png" class="glightbox" data-glightbox="description: .caption-go-test---json--parallel100--vgt"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="2492" height="1444" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-parallel_hu3ffbddc5f30f178cd5f094933617fcd3_568984_2492x1444_resize_q80_h2_lanczos_3.webp" alt="vgt cli tool" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fgo-test-parallelism%5C%2Fvgt-parallel_hu3ffbddc5f30f178cd5f094933617fcd3_568984_2492x1444_resize_lanczos_3.png"" /> </a> <figcaption id="caption-go test ./&hellip; -json -parallel=100 | vgt" class="caption-go-test---json--parallel100--vgt"> go test ./&hellip; -json -parallel=100 | vgt </figcaption> </figure> <p>We achieved our goal - all tests run in parallel. <strong>Our tests were not CPU-bound, so the overall execution time can be as long as the slowest executed test.</strong></p> <div class="notice tip"> <div class="notice-head"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillrule="evenodd" cliprule="evenodd" d="M12 0c6.6274.0 12 5.37258 12 12 0 6.6274-5.3726 12-12 12C5.37258 24 0 18.6274.0 12 0 5.37258 5.37258.0 12 0zm0 2.4C6.69807 2.4 2.4 6.69807 2.4 12c0 5.3019 4.29807 9.6 9.6 9.6 5.3019.0 9.6-4.2981 9.6-9.6.0-5.30193-4.2981-9.6-9.6-9.6zm3.9515 5.15147L9.6 13.9029 8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515c-.46863.4686-.46863 1.2284.0 1.697l2.4 2.4C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485l7.2-7.19997C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147c-.468599999999999-.46863-1.2284-.46863-1.697.0z" fill="currentcolor"></path> </svg> <p>Tip</p></div> <div class="notice-body"> <p> <p>If you are not changing the test code and want to test their performance, Go may cache them.</p> <p>To avoid caching, run them with the <code>-count=1</code> flag, for example <code>go test ./... -json -count=1 -parallel=100 | vgt</code>.</p> </p> </div> </div> <h2 id="tests-in-multiple-packages">Tests in multiple packages</h2> <p>Using the <code>-parallel</code> flag is not the only thing we can do to speed up tests. It&rsquo;s not uncommon to store tests in multiple packages. <strong>By default, Go limits how many packages can run simultaneously to the number of cores.</strong> Let&rsquo;s look at the example project structure:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ ls ./tests/* </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">./tests/package_1: </span></span><span class="line"><span class="cl">api_test.go </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">./tests/package_2: </span></span><span class="line"><span class="cl">api_test.go </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">./tests/package_3: </span></span><span class="line"><span class="cl">api_test.go </span></span></code></pre></div><p>Usually, we may have many more packages with tests for more complex projects. For readability, let&rsquo;s simulate how tests will run on one CPU (for example, in a CI runner with 1 core):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">GOMAXPROCS</span><span class="o">=</span><span class="m">1</span> go <span class="nb">test</span> ./tests/... -parallel <span class="m">100</span> -json <span class="p">|</span> vgt </span></span></code></pre></div> <figure class="img-center" role="group" aria-describedby="caption-Running tests on a runner with 1 core with 3 packages. . Output from vgt."> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-multi-pkg.png" class="glightbox" data-glightbox="description: .caption-Running-tests-on-a-runner-with-1-core-with-3-packages--Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="2716" height="1444" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-multi-pkg_hu2eb304ff1ae41954eb2f22c62533bb11_254571_2716x1444_resize_q80_h2_lanczos_3.webp" alt="vgt cli tool" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fgo-test-parallelism%5C%2Fvgt-multi-pkg_hu2eb304ff1ae41954eb2f22c62533bb11_254571_2716x1444_resize_lanczos_3.png"" /> </a> <figcaption id="caption-Running tests on a runner with 1 core with 3 packages. . Output from vgt." class="caption-Running-tests-on-a-runner-with-1-core-with-3-packages--Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> Running tests on a runner with 1 core with 3 packages. . Output from <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target='_blank'>vgt</a>. </figcaption> </figure> <p>You can see that every package runs separately. We can fix this with the <code>-p</code> flag. <strong>It may not be a problem if your tests run on machines with multiple cores and you don&rsquo;t have many packages with long-running tests.</strong> But in our scenario, with CI and one core, we need to specify the <code>-p</code> flag. It will allow up to 16 packages to run in parallel.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">GOMAXPROCS</span><span class="o">=</span><span class="m">1</span> go <span class="nb">test</span> ./tests/... -parallel <span class="m">128</span> -p <span class="m">16</span> -json <span class="p">|</span> vgt </span></span></code></pre></div> <figure class="img-center" role="group" aria-describedby="caption-Running tests on a runner with 1 core with 3 packages. Output from vgt."> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-multi-pkg-p.png" class="glightbox" data-glightbox="description: .caption-Running-tests-on-a-runner-with-1-core-with-3-packages-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="2716" height="1444" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-multi-pkg-p_hu2eb304ff1ae41954eb2f22c62533bb11_256669_2716x1444_resize_q80_h2_lanczos_3.webp" alt="vgt cli tool" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fgo-test-parallelism%5C%2Fvgt-multi-pkg-p_hu2eb304ff1ae41954eb2f22c62533bb11_256669_2716x1444_resize_lanczos_3.png"" /> </a> <figcaption id="caption-Running tests on a runner with 1 core with 3 packages. Output from vgt." class="caption-Running-tests-on-a-runner-with-1-core-with-3-packages-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> Running tests on a runner with 1 core with 3 packages. Output from <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target='_blank'>vgt</a>. </figcaption> </figure> <p>Now, the entire execution time is very close to the longest test duration.</p> <div class="notice tip"> <div class="notice-head"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillrule="evenodd" cliprule="evenodd" d="M12 0c6.6274.0 12 5.37258 12 12 0 6.6274-5.3726 12-12 12C5.37258 24 0 18.6274.0 12 0 5.37258 5.37258.0 12 0zm0 2.4C6.69807 2.4 2.4 6.69807 2.4 12c0 5.3019 4.29807 9.6 9.6 9.6 5.3019.0 9.6-4.2981 9.6-9.6.0-5.30193-4.2981-9.6-9.6-9.6zm3.9515 5.15147L9.6 13.9029 8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515c-.46863.4686-.46863 1.2284.0 1.697l2.4 2.4C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485l7.2-7.19997C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147c-.468599999999999-.46863-1.2284-.46863-1.697.0z" fill="currentcolor"></path> </svg> <p>Tip</p></div> <div class="notice-body"> <p> <p>It&rsquo;s hard to give <code>-parallel</code> and <code>-p</code> values that will work for all projects. It depends a lot on your types of tests and how they are structured. The default value will work fine for many lightweight unit tests or CPU-bound tests that efficiently use multiple cores.</p> <p>The best way to find the correct <code>-parallel</code> flag is to experiment with different values. <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target="_blank"><code>vgt</code></a> may be helpful in understanding how different values affect test execution.</p> </p> </div> </div> <h3 id="parallelism-with-sub-tests-and-test-tables">Parallelism with sub-tests and test tables</h3> <p>Using test tables for tests in Go is very useful when you need to test many input parameters for a function. On the other hand, they have a couple of dangers: creating test tables may sometimes be more complex than just copying the test body multiple times.</p> <p><strong>Using test tables can also affect the performance of our tests a lot if we forget to add <code>t.Parallel()</code>.</strong> This is especially visible for test tables with a lot of slow test cases. Even one use of test table can make our tests considerably slower.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestApi_with_test_table</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">testCases</span> <span class="o">:=</span> <span class="p">[]</span><span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">API</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="p">}{</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;1&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/1&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;2&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/2&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;3&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/3&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;4&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/4&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;5&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/5&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;6&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/6&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;7&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/7&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;8&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/8&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;9&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/9&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;10&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/9&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;11&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/1&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;12&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/2&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;13&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/3&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;14&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/4&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;15&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/5&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;16&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/6&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;17&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/7&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;18&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/8&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;19&#34;</span><span class="p">,</span> <span class="nx">API</span><span class="p">:</span> <span class="s">&#34;/api/9&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">testCases</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">tc</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nf">simulateSlowCall</span><span class="p">(</span><span class="mi">1</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div> <figure class="img-center" role="group" aria-describedby="caption-Using a test table without t.Parallel(). Output from vgt."> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-test-table.png" class="glightbox" data-glightbox="description: .caption-Using-a-test-table-without-tParallel-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="2534" height="1444" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-test-table_huc1c09de7476ad6cf6416ad99d72c35cf_165401_2534x1444_resize_q80_h2_lanczos_3.webp" alt="vgt cli tool" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fgo-test-parallelism%5C%2Fvgt-test-table_huc1c09de7476ad6cf6416ad99d72c35cf_165401_2534x1444_resize_lanczos_3.png"" /> </a> <figcaption id="caption-Using a test table without t.Parallel(). Output from vgt." class="caption-Using-a-test-table-without-tParallel-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> Using a test table without t.Parallel(). Output from <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target='_blank'>vgt</a>. </figcaption> </figure> <p>The solution is simple: add <code>t.Parallel()</code> to the test table. But it&rsquo;s easy to forget about it. We can be careful when using test tables. But being careful doesn&rsquo;t always work in the real world when you&rsquo;re in a hurry. We need an automated way to ensure that <code>t.Parallel()</code> is not missed.</p> <h3 id="linting-if-tparallel-is-used">Linting if <code>t.Parallel()</code> is used</h3> <p>In most projects, we use <a href="proxy.php?url=https%3A%2F%2Fgolangci-lint.run%2F" target="_blank"><code>golangci-lint</code></a>. It allows you to configure multiple linters and set them up for the entire project.</p> <p>We can configure which linter should be enabled based on the file name. An example configuration will ensure all tests in files ending with <code>_api_test.go</code> or <code>_integ_test.go</code> are using <code>t.Parallel()</code>. Unfortunately, as a downside, it requires keeping a convention in naming test files.</p> <div class="notice tip"> <div class="notice-head"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillrule="evenodd" cliprule="evenodd" d="M12 0c6.6274.0 12 5.37258 12 12 0 6.6274-5.3726 12-12 12C5.37258 24 0 18.6274.0 12 0 5.37258 5.37258.0 12 0zm0 2.4C6.69807 2.4 2.4 6.69807 2.4 12c0 5.3019 4.29807 9.6 9.6 9.6 5.3019.0 9.6-4.2981 9.6-9.6.0-5.30193-4.2981-9.6-9.6-9.6zm3.9515 5.15147L9.6 13.9029 8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515c-.46863.4686-.46863 1.2284.0 1.697l2.4 2.4C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485l7.2-7.19997C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147c-.468599999999999-.46863-1.2284-.46863-1.697.0z" fill="currentcolor"></path> </svg> <p>Tip</p></div> <div class="notice-body"> <p> Alternatively, you can group your tests by type in multiple packages. So, one package can contain API tests, and another can contain integration tests. </p> </div> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> As mentioned earlier, it&rsquo;s not only pointless, <strong>but even slower to use <code>t.Parallel()</code> for all kinds of tests.</strong> Avoid requiring <code>t.Parallel()</code> for all types of tests. </p></div> </div> <p>This is an example <code>.golangci.yml</code> configuration:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">run</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">linters</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">enable</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># ...</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">paralleltest</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">issues</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">exclude-rules</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># ...</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">path-except</span><span class="p">:</span><span class="w"> </span><span class="l">_api_test\.go|_integ_test\.go</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">linters</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">paralleltest</span><span class="w"> </span></span></span></code></pre></div><p>With this config, we can run the linter:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ golangci-lint run </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">package_1/some_api_test.go:9:1: Function TestApi_with_test_table missing the call to method parallel <span class="o">(</span>paralleltest<span class="o">)</span> </span></span><span class="line"><span class="cl">func TestApi_with_test_table<span class="o">(</span>t *testing.T<span class="o">)</span> <span class="o">{</span> </span></span><span class="line"><span class="cl">^ </span></span></code></pre></div><p>You can find a reference for configuration in <a href="proxy.php?url=https%3A%2F%2Fgolangci-lint.run%2Fusage%2Fconfiguration%2F" target="_blank">golangci-lint docs</a>.</p> <div class="notice tip"> <div class="notice-head"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillrule="evenodd" cliprule="evenodd" d="M12 0c6.6274.0 12 5.37258 12 12 0 6.6274-5.3726 12-12 12C5.37258 24 0 18.6274.0 12 0 5.37258 5.37258.0 12 0zm0 2.4C6.69807 2.4 2.4 6.69807 2.4 12c0 5.3019 4.29807 9.6 9.6 9.6 5.3019.0 9.6-4.2981 9.6-9.6.0-5.30193-4.2981-9.6-9.6-9.6zm3.9515 5.15147L9.6 13.9029 8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515c-.46863.4686-.46863 1.2284.0 1.697l2.4 2.4C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485l7.2-7.19997C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147c-.468599999999999-.46863-1.2284-.46863-1.697.0z" fill="currentcolor"></path> </svg> <p>Tip</p></div> <div class="notice-body"> <p> <p>Not all tests can always run in parallel. In this case, you can disable the linter for this specific test with <code>//nolint:paralleltest</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">//nolint:paralleltest </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="nf">SomeTest</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">//nolint:paralleltest </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="s">&#34;some_sub_test&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div> </p> </div> </div> <h2 id="parallelism-quirks-does-grouping-tests-with-trun-affect-performance">Parallelism quirks: does grouping tests with <code>t.Run()</code> affect performance?</h2> <p>I&rsquo;ve often seen in many projects a convention of grouping tests with <code>t.Run()</code>. Have you ever wondered if it affects performance in any way?</p> <p>To check this hypothesis, I wrote 50 tests like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestApi1</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="s">&#34;1&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="s">&#34;1&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="s">&#34;1&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">simulateSlowCall</span><span class="p">(</span><span class="mi">1</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="s">&#34;2&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="s">&#34;2&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="s">&#34;2&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">simulateSlowCall</span><span class="p">(</span><span class="mi">1</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">simulateSlowCall</span><span class="p">(</span><span class="nx">sleepTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// for more reliable results I&#39;m using constant time here </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="nx">sleepTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>To compare, I also wrote 50 tests without using <code>t.Run()</code> for subtests:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestApi1</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"><span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nf">simulateSlowCall</span><span class="p">(</span><span class="mi">1</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nf">simulateSlowCall</span><span class="p">(</span><span class="mi">1</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">simulateSlowCall</span><span class="p">(</span><span class="nx">sleepTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"><span class="c1">// for more reliable results I&#39;m using constant time here </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="nx">sleepTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Does it affect test performance by affecting parallelism in any way? Let&rsquo;s see what <code>vgt</code> will show us.</p> <figure class="img-center" role="group" aria-describedby="caption-Tests without using r.Run() for subtests. Output from vgt."> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-no-subtests.png" class="glightbox" data-glightbox="description: .caption-Tests-without-using-rRun-for-subtests-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="2524" height="1444" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-no-subtests_hub25189b661e397d4d4a812c604fb3bce_366417_2524x1444_resize_q80_h2_lanczos_3.webp" alt="vgt cli tool" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fgo-test-parallelism%5C%2Fvgt-no-subtests_hub25189b661e397d4d4a812c604fb3bce_366417_2524x1444_resize_lanczos_3.png"" /> </a> <figcaption id="caption-Tests without using r.Run() for subtests. Output from vgt." class="caption-Tests-without-using-rRun-for-subtests-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> Tests without using r.Run() for subtests. Output from <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target='_blank'>vgt</a>. </figcaption> </figure> <figure class="img-center" role="group" aria-describedby="caption-Tests using r.Run() for subtests. Output from vgt."> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-subtests.png" class="glightbox" data-glightbox="description: .caption-Tests-using-rRun-for-subtests-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> <img title="" loading="lazy" decoding="async" class="img img-wide" width="2524" height="1444" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-test-parallelism%2Fvgt-subtests_hub25189b661e397d4d4a812c604fb3bce_422518_2524x1444_resize_q80_h2_lanczos_3.webp" alt="vgt cli tool" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fgo-test-parallelism%5C%2Fvgt-subtests_hub25189b661e397d4d4a812c604fb3bce_422518_2524x1444_resize_lanczos_3.png"" /> </a> <figcaption id="caption-Tests using r.Run() for subtests. Output from vgt." class="caption-Tests-using-rRun-for-subtests-Output-from-a-hrefhttpsgithubcomroblaszczakvgt-targetblankvgta"> Tests using r.Run() for subtests. Output from <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target='_blank'>vgt</a>. </figcaption> </figure> <p>Despite the chart looking a bit uglier, execution times are the same for grouped and non-grouped tests. <strong>In other words, grouping tests with <code>t.Run()</code> does not affect performance.</strong></p> <h2 id="summary">Summary</h2> <p><strong>Having fast and reliable tests for efficient development is crucial. But over the years, I&rsquo;ve learned that reality is not always so simple. There is always more work to do, and tests are not something that our product&rsquo;s users or boss directly see.</strong> On the other hand, a small investment in tests can save a lot of time in the future. Reliable and fast tests are one of the best investments in your project&rsquo;s return on investment (ROI).</p> <p>Knowing some tactics to convince your team leader or manager to find time to improve tests is useful. <strong>It&rsquo;s helpful to think in terms that your manager or boss uses. You should consider the benefits they care about.</strong> Reducing delivery time - you can even calculate how many minutes per month the entire team wastes by waiting for tests or retrying them. It may be useful to multiply this by the average developer&rsquo;s salary. It&rsquo;s also helpful to track bugs shipped to production because tests were so flaky that nobody noticed them.</p> <p>There are more ways to make your tests more useful. I&rsquo;ve also observed that people often struggle with naming different types of tests &ndash; we will give you some hints about that, too. If you are interested in more articles about testing, check out:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-integration-testing%2F">4 practical principles of high-quality database integration tests in Go</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-test-architecture%2F">Microservices test architecture</a></li> </ul> <p><strong>Is <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fvgt" target="_blank"><code>vgt</code></a> useful for you? Don&rsquo;t forget to give it a star on GitHub and share it with your friends or social media!</strong></p>Distributed Transactions in Go: Read Before You Tryhttps://threedots.tech/post/distributed-transactions-in-go/Wed, 02 Oct 2024 00:00:00 +0200https://threedots.tech/post/distributed-transactions-in-go/<p><em>In the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2F">previous post</a>, I looked into running transactions in a layered architecture. Now, let&rsquo;s consider transactions that need to span more than one service.</em></p> <p>If you work with microservices, a time may come when you need a transaction running across them. Especially if the way they are split was an afterthought (the unfortunate but likely scenario). Service A calls service B, which calls service C, and if something goes wrong at the end, the system becomes inconsistent. It would be helpful to have a way to roll back the changes in all the services.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1396" height="301" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdistributed-transactions-in-go%2Fimages%2F5-microservices_hub5f05658aab835b55e17359be976c631_39617_1396x301_resize_q80_h2_lanczos_3.webp" alt="Microservices" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdistributed-transactions-in-go%5C%2Fimages%5C%2F5-microservices_hub5f05658aab835b55e17359be976c631_39617_1396x301_resize_lanczos_3.png"" /> <p>Now, you&rsquo;re looking at <em>distributed transactions</em> or the <em>saga</em> pattern. It&rsquo;s a solved problem. Sometimes, there are good reasons to use them. <strong>But more often, it&rsquo;s overkill. If you go down this path, your architecture quickly becomes much more complex than you&rsquo;d like.</strong> (I learned this the hard way.)</p> <p>If you need things to be consistent, why split them in the first place? The whole idea of using microservices is to keep independent concepts separate.</p> <p><strong>Let&rsquo;s face it: your microservice boundaries are likely wrong if you consider using a distributed transaction.</strong> This is super common and not easy to solve. But it can get much worse if you apply the wrong pattern to the mess you already have.</p> <p><strong>Before you try it, I want to show you an alternative that might be simpler.</strong> It&rsquo;s not a silver bullet, but consider it before committing to distributed transactions.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Distributed Transactions</p> </div> <div class="notice-body"><p> Stay away from transactions that span more than one service, except when there's no other way. <br><br> They are hard to test, debug, and maintain. Your system becomes tightly coupled and more challenging to extend. </p></div> </div> <h2 id="the-example">The Example</h2> <p>The snippets below continue the example from the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2F">previous post</a>. Here&rsquo;s a quick refresher: we work with an e-commerce web app where users get virtual points. They can use these points as a discount for their next order. The challenge is keeping the user&rsquo;s points and the applied discount consistent.</p> <p>Let&rsquo;s assume we work with two services: <code>users</code> for authentication and <code>orders</code> for placing orders and tracking users&rsquo; discounts. Initially, the split seemed like a good idea because they are separate areas. Later, we realized we couldn&rsquo;t update users and their discounts within one transaction.</p> <p>The <code>orders</code> service exposes an HTTP endpoint for adding a discount. In the <code>users</code> service, the handler for using points as a discount looks like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">UsePointsAsDiscountHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">UsePointsAsDiscount</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">userRepository</span><span class="p">.</span><span class="nf">UpdateByID</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">user</span><span class="p">.</span><span class="nf">UsePoints</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">false</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">true</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not update user: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">ordersService</span><span class="p">.</span><span class="nf">AddDiscount</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not add discount: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F01-distributed-monolith%2Fusers-svc%2Fapp.go%23L36" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/05-distributed-transactions/01-distributed-monolith/users-svc/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F01-distributed-monolith%2Fusers-svc%2Fapp.go%23L36" target="_blank">Full source</a> </div> <p>We first take the user&rsquo;s points. After committing the transaction, we call the <code>orders</code> service to add a discount. It works well in a happy path scenario, but the HTTP call works outside the transaction. If anything goes wrong, the points are gone, but we add no discount. The best we can do is log the error and fire an alert so someone on-call can manually add the discount for the annoyed customer.</p> <p>Welcome to the distributed monolith.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1050" height="432" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdistributed-transactions-in-go%2Fimages%2F1-distributed-monolith_hu9c541f083a1498e44f6a598271ebac34_61537_1050x432_resize_q80_h2_lanczos_3.webp" alt="Distributed Monolith" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdistributed-transactions-in-go%5C%2Fimages%5C%2F1-distributed-monolith_hu9c541f083a1498e44f6a598271ebac34_61537_1050x432_resize_lanczos_3.png"" /> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: The Distributed Monolith</p> </div> <div class="notice-body"><p> Be extra careful when designing the boundaries of microservices. What seems like a minor decision in the beginning can have terrible consequences in the future. If in doubt, stick to a single service and decouple your code using modules — <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-or-monolith-its-detail%2F" target="_blank">the modular monolith approach</a>. </p></div> </div> <p>One way out of this mess is to accept past mistakes and merge the microservices into one service. Depending on the approach used and the size of the services, it may not be a quick task. And if your product backlog is full and people who designed the microservices are still around, it&rsquo;s unlikely to happen.</p> <p>If you see the problem as &ldquo;we miss consistency between services&rdquo;, running a distributed transaction seems like a good solution. But the real problem is &ldquo;the boundaries are wrong&rdquo;. It&rsquo;s easy to make the project much more complicated because of this misinterpretation.</p> <p>One alternative is to <strong>embrace eventual consistency</strong>.</p> <h2 id="eventual-consistency">Eventual Consistency</h2> <p>The idea is that the data (the user&rsquo;s points and applied discount in our example) stays consistent but no longer within a single transaction. In practice, there&rsquo;s a slight delay — usually milliseconds — during which the system is inconsistent but then catches up.</p> <p>The system may become out of sync for longer if something goes wrong. But the important part is that it <em>eventually</em> becomes consistent again. <strong>We don&rsquo;t accept inconsistency — we&rsquo;re just okay with waiting for it to happen.</strong> Think about sending a bank transfer. The money disappears from your account immediately but isn&rsquo;t visible on the other account right away. You know it will eventually happen within a few hours or days, so it&rsquo;s not an issue.</p> <p>Whether it&rsquo;s acceptable in a given scenario is often a business decision. Since business stakeholders generally don&rsquo;t consider this, engineers must explain it and suggest solutions.</p> <p>Having a transaction boundary sounds reasonable in the &ldquo;using points as a discount&rdquo; scenario. But since we already have to work with a distributed monolith, choosing eventual consistency might be the next best thing instead of implementing a distributed transaction.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> I could have picked another example that would be more fit for eventual consistency. In some cases, data doesn't need to be consistent at all. <br><br> But this scenario is closer to what you can see in your daily work. Using a distributed transaction here may be tempting because it's not obvious if the data can be eventually consistent. </p></div> </div> <h3 id="events">Events</h3> <p>Eventual consistency usually means working with events.</p> <p>Going this path, we no longer call the <code>orders</code> service directly. We take the user&rsquo;s points and <em>publish an event</em>, like <code>PointsUsedForDiscount</code>, to record that it happened.</p> <p>The event is stored on a message queue (AKA a Pub/Sub), a reliable, highly-available infrastructure for messages. The <code>orders</code> service receives this event and reacts by applying a discount for the next order. We can then update the discount value on the website (for example, with <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2F" target="_blank">Server-Sent Events</a>). Most of the time, it takes milliseconds, and the customers can&rsquo;t tell a difference.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1029" height="925" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdistributed-transactions-in-go%2Fimages%2F2-event-driven_hu5f9ac72418618ecf7b66abe49ef949d0_122934_1029x925_resize_q80_h2_lanczos_3.webp" alt="Event Driven" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdistributed-transactions-in-go%5C%2Fimages%5C%2F2-event-driven_hu5f9ac72418618ecf7b66abe49ef949d0_122934_1029x925_resize_lanczos_3.png"" /> <p>It&rsquo;s sometimes better to retry the operation internally instead of showing a scary error message to the user. After all, the issue might be temporary, and retrying is often a good enough fix. However, you need to consider it separately for each scenario. It&rsquo;s often crucial that the user knows an error has occurred.</p> <p>If the <code>orders</code> service goes down, it won&rsquo;t interrupt the process. <strong>The event will be delivered again and again until it&rsquo;s processed successfully.</strong> Once the service comes up, the discount will be applied as expected.</p> <p>It doesn&rsquo;t fix all the issues. For example, the customer won&rsquo;t see the discount if the service is down for a few hours. But it gives us a solid way of retrying the operation if it&rsquo;s a short issue. In many scenarios, retrying for a few hours is also acceptable (generating reports, issuing invoices, etc.).</p> <p>In most cases, <strong>we don&rsquo;t need to manually investigate what data needs to be backfilled, and no one needs to wake up in the middle of the night.</strong> The system auto-heals with time.</p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Embrace Eventual Consistency</p> </div> <div class="notice-body"><p> Not all operations need to be strongly consistent, even if it initially seems like it. <br><br> Keep eventual consistency in mind when designing distributed systems. It's often simpler than going the distributed transactions way. </p></div> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <b>Events or messages?</b> <br><br> While sometimes used as synonyms, there's a difference between events and messages. <br><br> A message is a transport unit you publish to the Pub/Sub or receive from it. It's similar to an HTTP request with a method, path, headers, and body. <br><br> An event is a specific message. It represents a fact about something that has already happened in the system. The event is represented by the payload held by the message (encoded as JSON or another format). It's what you would include in the HTTP request's body. </p></div> </div> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/distributed-transactions-in-go/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;web-applications;anti-patterns;transactions;databases;watermill;event-driven"> <input type="hidden" name="fields[blog_series]" value=""> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h3 id="implementation">Implementation</h3> <p>To start working with messages, you must pick a broker (the infrastructure part), just like when choosing a database. You publish messages to the broker, which asynchronously pushes them to all subscribers. In this post, I&rsquo;m going to use Redis.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1687" height="659" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdistributed-transactions-in-go%2Fimages%2F7-pubsub_hua694b087d006714260b29106c5ca2ee6_101093_1687x659_resize_q80_h2_lanczos_3.webp" alt="Pub/Sub" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdistributed-transactions-in-go%5C%2Fimages%5C%2F7-pubsub_hua694b087d006714260b29106c5ca2ee6_101093_1687x659_resize_lanczos_3.png"" /> <p>Most popular brokers (Pub/Subs) have a Go library (SDK) that lets you interact with it. But they usually offer a low-level API. As much as I like Go&rsquo;s <code>net/http</code>, I still prefer using Echo or Chi for their high-level API. Similarly, I will use <a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">Watermill</a> to work with messages.</p> <p>Watermill is a Go library we maintain. I won&rsquo;t explain how it works here in detail. See the <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fgetting-started%2F" target="_blank">Getting Started page</a> for an overview. It supports <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2F" target="_blank">many Pub/Sub backends</a>, like Kafka, AMQP, NATS, or Redis.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> All the ideas below are universal and can be implemented with any other messaging library. <br><br> <b>Watermill is not a framework, so it's easy to add or remove from a project.</b> It's one of the core design ideas. <br><br> You can see that I won't include Watermill-specific code in the logic layer. Feel free to use anything else if it fits your needs better. </p></div> </div> <p>We&rsquo;ll use the pair of <code>EventBus</code> and <code>EventProcessor</code> components from Watermill&rsquo;s <code>cqrs</code> package.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1324" height="716" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdistributed-transactions-in-go%2Fimages%2F9-cqrs_hu416387cd7ea843a1e39f99730af141f4_95655_1324x716_resize_q80_h2_lanczos_3.webp" alt="CQRS" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdistributed-transactions-in-go%5C%2Fimages%5C%2F9-cqrs_hu416387cd7ea843a1e39f99730af141f4_95655_1324x716_resize_lanczos_3.png"" /> <p>First, let&rsquo;s replace the HTTP call with publishing an event. The event bus lets us publish messages with a JSON payload. The setup should be easy to grasp even if you don&rsquo;t know Watermill yet.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">client</span> <span class="o">:=</span> <span class="nx">redis</span><span class="p">.</span><span class="nf">NewClient</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">redis</span><span class="p">.</span><span class="nx">Options</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Addr</span><span class="p">:</span> <span class="nx">redisAddr</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">logger</span> <span class="o">:=</span> <span class="nx">watermill</span><span class="p">.</span><span class="nf">NewStdLogger</span><span class="p">(</span><span class="kc">false</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">publisher</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">redisstream</span><span class="p">.</span><span class="nf">NewPublisher</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">redisstream</span><span class="p">.</span><span class="nx">PublisherConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Client</span><span class="p">:</span> <span class="nx">client</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">eventBus</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nf">NewEventBusWithConfig</span><span class="p">(</span><span class="nx">publisher</span><span class="p">,</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">EventBusConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">GeneratePublishTopic</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">params</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">GenerateEventPublishTopicParams</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">params</span><span class="p">.</span><span class="nx">EventName</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">Marshaler</span><span class="p">:</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">JSONMarshaler</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">Logger</span><span class="p">:</span> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F02-eventual-consistency%2Fusers-svc%2Fevents.go%23L10" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/05-distributed-transactions/02-eventual-consistency/users-svc/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F02-eventual-consistency%2Fusers-svc%2Fevents.go%23L10" target="_blank">Full source</a> </div> <p>We create a Redis publisher and an <code>EventBus</code>. It&rsquo;s going to use the event&rsquo;s name as a topic. (A topic is what you subscribe to to receive messages.) It&rsquo;s going to marshal events into JSON.</p> <p>In the command handler, we replace the synchronous call with publishing an event.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line hl"><span class="cl"><span class="kd">type</span> <span class="nx">EventPublisher</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line hl"><span class="cl"> <span class="nf">Publish</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="nx">any</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line hl"><span class="cl"><span class="p">}</span> </span></span><span class="line hl"><span class="cl"> </span></span><span class="line hl"><span class="cl"><span class="kd">type</span> <span class="nx">PointsUsedForDiscount</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line hl"><span class="cl"> <span class="nx">UserID</span> <span class="kt">int</span> <span class="s">`json:&#34;user_id&#34;`</span> </span></span><span class="line hl"><span class="cl"> <span class="nx">Points</span> <span class="kt">int</span> <span class="s">`json:&#34;points&#34;`</span> </span></span><span class="line hl"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">UsePointsAsDiscountHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">UsePointsAsDiscount</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">userRepository</span><span class="p">.</span><span class="nf">UpdateByID</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">user</span><span class="p">.</span><span class="nf">UsePoints</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">false</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">true</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not update user: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line hl"><span class="cl"> <span class="nx">event</span> <span class="o">:=</span> <span class="nx">PointsUsedForDiscount</span><span class="p">{</span> </span></span><span class="line hl"><span class="cl"> <span class="nx">UserID</span><span class="p">:</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> </span></span><span class="line hl"><span class="cl"> <span class="nx">Points</span><span class="p">:</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">,</span> </span></span><span class="line hl"><span class="cl"> <span class="p">}</span> </span></span><span class="line hl"><span class="cl"> </span></span><span class="line hl"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">eventPublisher</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">)</span> </span></span><span class="line hl"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line hl"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not publish event: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line hl"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F02-eventual-consistency%2Fusers-svc%2Fapp.go%23L22" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/05-distributed-transactions/02-eventual-consistency/users-svc/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F02-eventual-consistency%2Fusers-svc%2Fapp.go%23L22" target="_blank">Full source</a> </div> <p>In the <code>orders</code> service, we replace the HTTP handler with an event handler.</p> <p>Let&rsquo;s set up an <code>EventProcessor</code> for subscribing, just like we set up the event bus for publishing.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">client</span> <span class="o">:=</span> <span class="nx">redis</span><span class="p">.</span><span class="nf">NewClient</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">redis</span><span class="p">.</span><span class="nx">Options</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Addr</span><span class="p">:</span> <span class="nx">redisAddr</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">logger</span> <span class="o">:=</span> <span class="nx">watermill</span><span class="p">.</span><span class="nf">NewStdLogger</span><span class="p">(</span><span class="kc">false</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">router</span> <span class="o">:=</span> <span class="nx">message</span><span class="p">.</span><span class="nf">NewDefaultRouter</span><span class="p">(</span><span class="nx">logger</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">eventProcessor</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nf">NewEventProcessorWithConfig</span><span class="p">(</span><span class="nx">router</span><span class="p">,</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">EventProcessorConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">GenerateSubscribeTopic</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">params</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">EventProcessorGenerateSubscribeTopicParams</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">params</span><span class="p">.</span><span class="nx">EventName</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">SubscriberConstructor</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">params</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">EventProcessorSubscriberConstructorParams</span><span class="p">)</span> <span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">Subscriber</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">redisstream</span><span class="p">.</span><span class="nf">NewSubscriber</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">redisstream</span><span class="p">.</span><span class="nx">SubscriberConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Client</span><span class="p">:</span> <span class="nx">client</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">ConsumerGroup</span><span class="p">:</span> <span class="s">&#34;orders-svc&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">Marshaler</span><span class="p">:</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">JSONMarshaler</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">Logger</span><span class="p">:</span> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F02-eventual-consistency%2Forders-svc%2Fevents.go%23L31" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/05-distributed-transactions/02-eventual-consistency/orders-svc/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F02-eventual-consistency%2Forders-svc%2Fevents.go%23L31" target="_blank">Full source</a> </div> <p>The setup is almost identical to the publishing side. The main difference is that we provide a constructor instead of a single subscriber. I won&rsquo;t dive into the reasons, as they are irrelevant to what we want to do. To receive messages correctly, we must use the same marshaler and topic configuration as in the event bus config.</p> <p>Next, we need an event handler. It simply maps the event payload to the command and executes it, just like the HTTP handler did.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">OnPointsUsedForDiscountHandler</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">addDiscountHandler</span> <span class="nx">AddDiscountHandler</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">OnPointsUsedForDiscountHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="o">*</span><span class="nx">PointsUsedForDiscount</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span> <span class="o">:=</span> <span class="nx">AddDiscount</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserID</span><span class="p">:</span> <span class="nx">event</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Discount</span><span class="p">:</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Points</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">.</span><span class="nx">addDiscountHandler</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F02-eventual-consistency%2Forders-svc%2Fevents.go%23L18" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/05-distributed-transactions/02-eventual-consistency/orders-svc/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F02-eventual-consistency%2Forders-svc%2Fevents.go%23L18" target="_blank">Full source</a> </div> <p>We add it to the event processor.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">err</span> <span class="p">=</span> <span class="nx">eventProcessor</span><span class="p">.</span><span class="nf">AddHandlers</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">cqrs</span><span class="p">.</span><span class="nf">NewEventHandler</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;OnPointsUsedForDiscountHandler&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">onPointsUsedForDiscountHandler</span><span class="p">.</span><span class="nx">Handle</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F02-eventual-consistency%2Forders-svc%2Fevents.go%23L67" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/05-distributed-transactions/02-eventual-consistency/orders-svc/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F02-eventual-consistency%2Forders-svc%2Fevents.go%23L67" target="_blank">Full source</a> </div> <p>The behavior is exactly the same as that of the HTTP endpoint. The message is just another transport, and the handler is another entry point to the application. The main difference is that it&rsquo;s asynchronous, so no client waits for it to complete.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1291" height="320" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdistributed-transactions-in-go%2Fimages%2F4-message_hub2e1ab57fca11f9bff26d40cd16bd166_52376_1291x320_resize_q80_h2_lanczos_3.webp" alt="Message" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdistributed-transactions-in-go%5C%2Fimages%5C%2F4-message_hub2e1ab57fca11f9bff26d40cd16bd166_52376_1291x320_resize_lanczos_3.png"" /> <p>That&rsquo;s it! With a few changes, we replaced the synchronous HTTP call with a message handler.</p> <p>Of course, there&rsquo;s the hard part of running a production-grade Pub/Sub. I used Redis, but you can pick what you feel comfortable with or what your cloud provider offers. You can even start with an SQL database if you don&rsquo;t want to set up new infrastructure.</p> <div class="text-center"> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F%3Futm_source%3Dblog-content"> <img src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fsidebar%2Fcourse.svg" loading="lazy" decoding="async" alt="Go In One Evening" class=" img" width="500" height="500" /> </a> <span class="h5 inline-block"> Are you experienced engineer who wants to learn Go basics?<br> You don't become an engineer by watching videos. <br> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F%3Futm_source%3Dblog-content"> Learn Go hands-on by building real projects. </a> </span> </div> <h2 id="the-outbox-pattern">The Outbox Pattern</h2> <p>There&rsquo;s still one scenario when things can go wrong. Since publishing the message works over the network, it can fail, just like the HTTP call to another service. Pub/Subs are usually highly available, but issues still happen (never assume the network is reliable!). We could lose the messages after the transaction is committed.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1154" height="476" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdistributed-transactions-in-go%2Fimages%2F6-outbox_hud59d468c91eecd1c43714b738b9852b1_68591_1154x476_resize_q80_h2_lanczos_3.webp" alt="Outbox" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdistributed-transactions-in-go%5C%2Fimages%5C%2F6-outbox_hud59d468c91eecd1c43714b738b9852b1_68591_1154x476_resize_lanczos_3.png"" /> <p><strong>It&rsquo;s one of the most common mistakes we&rsquo;ve seen: ignoring this scenario with the hope it will never happen.</strong> It&rsquo;s unlikely, sure, but you don&rsquo;t want to take this risk. Imagine figuring out what events you lost and manually correcting the system&rsquo;s state.</p> <p>We need to store changes in the database and publish the event within the same transaction. Is this possible?</p> <p><strong>Yes, we can use the outbox pattern</strong>. The idea is to save the event in the same database as the regular data within the same transaction. Then, <strong>we asynchronously publish it to the Pub/Sub.</strong> This way, the event and the stored data will always be consistent, and we will not rely on the Pub/Sub to be around. The events can be simply stored in a dedicated SQL table.</p> <p>The main challenge is waiting for new events and moving them to the Pub/Sub. Depending on the database, you can use some custom software, although configuring it is often quite complex. You can implement it from scratch, but it&rsquo;s not trivial.</p> <p>In this example, I&rsquo;ll use Watermill&rsquo;s <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fforwarder%2F" target="_blank">Forwarder</a> component to achieve this. Its behavior is straightforward, thanks to the heavy lifting done by the Pub/Sub implementation. It listens to messages from the given Pub/Sub and publishes them to another Pub/Sub.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1727" height="464" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdistributed-transactions-in-go%2Fimages%2F8-forwarder_huc37b28c94bd820a36b7a45be5b620d9b_88702_1727x464_resize_q80_h2_lanczos_3.webp" alt="Forwarder" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdistributed-transactions-in-go%5C%2Fimages%5C%2F8-forwarder_huc37b28c94bd820a36b7a45be5b620d9b_88702_1727x464_resize_lanczos_3.png"" /> <p>Watermill supports Postgres as one of the Pub/Sub implementations, so adding it to the existing setup is easy. (Yes, you can simply use an SQL database as your messaging infrastructure.)</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1305" height="1235" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdistributed-transactions-in-go%2Fimages%2F3-outbox_hu206b4751c136e729e48d74c36add6942_184047_1305x1235_resize_q80_h2_lanczos_3.webp" alt="Outbox" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdistributed-transactions-in-go%5C%2Fimages%5C%2F3-outbox_hu206b4751c136e729e48d74c36add6942_184047_1305x1235_resize_lanczos_3.png"" /> <p>We face another design dilemma. How can the publisher and repository work together without mixing the logic and the database code?</p> <p><strong>We&rsquo;ll extend the <code>UpdateFn</code> function so it decides what events should be published.</strong> (For more on how this pattern works, see the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2F">post on database transactions</a>.)</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">UsePointsAsDiscountHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">UsePointsAsDiscount</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line hl"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">.</span><span class="nx">userRepository</span><span class="p">.</span><span class="nf">UpdateByID</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="p">[]</span><span class="nx">any</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">user</span><span class="p">.</span><span class="nf">UsePoints</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">false</span><span class="p">,</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line hl"><span class="cl"> <span class="nx">event</span> <span class="o">:=</span> <span class="nx">PointsUsedForDiscount</span><span class="p">{</span> </span></span><span class="line hl"><span class="cl"> <span class="nx">UserID</span><span class="p">:</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> </span></span><span class="line hl"><span class="cl"> <span class="nx">Points</span><span class="p">:</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">,</span> </span></span><span class="line hl"><span class="cl"> <span class="p">}</span> </span></span><span class="line hl"><span class="cl"> </span></span><span class="line hl"><span class="cl"> <span class="k">return</span> <span class="kc">true</span><span class="p">,</span> <span class="p">[]</span><span class="nx">any</span><span class="p">{</span><span class="nx">event</span><span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F03-outbox%2Fusers-svc%2Fapp.go%23L28" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/05-distributed-transactions/03-outbox/users-svc/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F03-outbox%2Fusers-svc%2Fapp.go%23L28" target="_blank">Full source</a> </div> <p>The repository publishes the returned events.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line hl"><span class="cl"><span class="nx">updated</span><span class="p">,</span> <span class="nx">events</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">updateFn</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">!</span><span class="nx">updated</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">ExecContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;UPDATE users SET email = $1, points = $2 WHERE id = $3&#34;</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nf">Email</span><span class="p">(),</span> <span class="nx">user</span><span class="p">.</span><span class="nf">Points</span><span class="p">(),</span> <span class="nx">user</span><span class="p">.</span><span class="nf">ID</span><span class="p">())</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line hl"><span class="cl"><span class="nx">eventBus</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">NewWatermillEventBus</span><span class="p">(</span><span class="nx">tx</span><span class="p">)</span> </span></span><span class="line hl"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line hl"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line hl"><span class="cl"><span class="p">}</span> </span></span><span class="line hl"><span class="cl"> </span></span><span class="line hl"><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">event</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">events</span> <span class="p">{</span> </span></span><span class="line hl"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">eventBus</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">)</span> </span></span><span class="line hl"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line hl"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line hl"><span class="cl"> <span class="p">}</span> </span></span><span class="line hl"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F03-outbox%2Fusers-svc%2Frepository.go%23L31" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/05-distributed-transactions/03-outbox/users-svc/repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F03-outbox%2Fusers-svc%2Frepository.go%23L31" target="_blank">Full source</a> </div> <p>Note how a new event bus is created using the <code>tx</code> variable. This is how we ensure publishing happens within the same transaction.</p> <p>We must amend how the event bus is created in the <code>users</code> service. We will no longer publish directly to Redis. We need to change the original publisher from Redis to Postgres.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">publisher</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">watermillSQL</span><span class="p">.</span><span class="nf">NewPublisher</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">watermillSQL</span><span class="p">.</span><span class="nx">PublisherConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">SchemaAdapter</span><span class="p">:</span> <span class="nx">watermillSQL</span><span class="p">.</span><span class="nx">DefaultPostgreSQLSchema</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F03-outbox%2Fusers-svc%2Fevents.go%23L23" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/05-distributed-transactions/03-outbox/users-svc/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F03-outbox%2Fusers-svc%2Fevents.go%23L23" target="_blank">Full source</a> </div> <p>Next, we <em>wrap</em> this publisher with the forwarder&rsquo;s <code>Publisher</code>. It introduces an <em>envelope</em> around the message so the forwarder knows how to handle it. Its config is minimal.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">publisher</span> <span class="p">=</span> <span class="nx">forwarder</span><span class="p">.</span><span class="nf">NewPublisher</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">publisher</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">forwarder</span><span class="p">.</span><span class="nx">PublisherConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ForwarderTopic</span><span class="p">:</span> <span class="nx">forwarderTopic</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F03-outbox%2Fusers-svc%2Fevents.go%23L34" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/05-distributed-transactions/03-outbox/users-svc/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F03-outbox%2Fusers-svc%2Fevents.go%23L34" target="_blank">Full source</a> </div> <p>The last part is running the forwarder. While it could be a standalone service, keeping it running as another goroutine within the <code>users</code> service is also fine.</p> <p>We need an SQL subscriber to receive the stored events from the database, and a Redis publisher to publish the messages where the <code>orders</code> service expects them.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">sqlSubscriber</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">watermillSQL</span><span class="p">.</span><span class="nf">NewSubscriber</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">watermillSQL</span><span class="p">.</span><span class="nx">SubscriberConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">SchemaAdapter</span><span class="p">:</span> <span class="nx">watermillSQL</span><span class="p">.</span><span class="nx">DefaultPostgreSQLSchema</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">OffsetsAdapter</span><span class="p">:</span> <span class="nx">watermillSQL</span><span class="p">.</span><span class="nx">DefaultPostgreSQLOffsetsAdapter</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">InitializeSchema</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">client</span> <span class="o">:=</span> <span class="nx">redis</span><span class="p">.</span><span class="nf">NewClient</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">redis</span><span class="p">.</span><span class="nx">Options</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Addr</span><span class="p">:</span> <span class="nx">redisAddr</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">redisPublisher</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">redisstream</span><span class="p">.</span><span class="nf">NewPublisher</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">redisstream</span><span class="p">.</span><span class="nx">PublisherConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Client</span><span class="p">:</span> <span class="nx">client</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">fwd</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">forwarder</span><span class="p">.</span><span class="nf">NewForwarder</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">sqlSubscriber</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">redisPublisher</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">forwarder</span><span class="p">.</span><span class="nx">Config</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ForwarderTopic</span><span class="p">:</span> <span class="nx">forwarderTopic</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F03-outbox%2Fusers-svc%2Fevents.go%23L55" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/05-distributed-transactions/03-outbox/users-svc/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2F03-outbox%2Fusers-svc%2Fevents.go%23L55" target="_blank">Full source</a> </div> <p>Note that <strong>the forwarder topic needs to match between the forwarder publisher and the forwarder itself</strong>. In this case, it&rsquo;s the same <code>forwarderTopic</code> constant (can be any string).</p> <p>Finally, we run the forwarder.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">forwarder</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}()</span> </span></span></code></pre></div><p>No changes are necessary on the <code>orders</code> service side. As far as it&rsquo;s concerned, it receives events from Redis, as it used to.</p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: The Outbox Pattern</p> </div> <div class="notice-body"><p> Use the Outbox pattern to publish events within a database transaction. Don&rsquo;t leave it to chance whether all events will be successfully published. </p></div> </div> <h2 id="simplifying-or-complicating">Simplifying or Complicating?</h2> <p>There are many moving parts here. Event-driven patterns are not trivial, and you need to understand what you&rsquo;re doing to some degree. If you never used these concepts before, it can seem complex.</p> <p>I used to have these concerns as well. The system seems simpler with just the synchronous APIs and a database. Why complicate it with adding all this background processing?</p> <p>As counterintuitive as it is, you can simplify your distributed system using these ideas. Because you work with facts that happened (events), you don&rsquo;t need to consider rollbacks, which terribly complicate your flow.</p> <p>A rollback can interfere with other flows, so you need distributed locks to protect the state. Rollbacks can also fail. What do you do when this happens?</p> <p>As the snippets show, little code is needed to start working with events, and the handlers are also straightforward. This is where Watermill helps by removing the low-level boilerplate. You can focus on setting up the handlers and publishing messages with an API that&rsquo;s easier to use than HTTP endpoints.</p> <p>Once you grasp the basics, your mental model will start changing, and eventually, it will click. You&rsquo;ll get the intuition on how to design event-driven systems.</p> <h3 id="events--coupling">Events &amp; Coupling</h3> <p>Make no mistake, events are still a form of a contract. The fact you use them doesn&rsquo;t magically make your services decoupled. Incorrectly designed events are another trap you can fall into.</p> <p>What I like about the pattern is that it limits what the services know about each other. But it&rsquo;s easy to fail at this.</p> <p>The example above shows that the event is designed quite poorly. The <code>UsePointsAsDiscount</code> name makes the <code>users</code> service aware of what happens in the <code>orders</code> service after it receives it. It&rsquo;s too late to escape this now, though. The service boundaries are just wrong from the beginning.</p> <p>Well-designed events state a fact that happened in this service&rsquo;s domain. The service publishing the event should not know or care what happens when another service consumes it.</p> <h3 id="testing">Testing</h3> <p>Testing event-driven services is not much different, but here are some tips.</p> <p>Run your Pub/Sub locally in a Docker container and test against it. Cloud-based Pub/Subs usually offer an emulator (but often with no feature parity, so watch out).</p> <p>Testing the event handler code makes little sense. Event handlers should just execute your application logic with the data from the event. Instead, use <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-test-architecture%2F">component tests</a> to test the behavior of the service using the public API.</p> <p>A test scenario can be something like:</p> <ul> <li>Once this event is published, an entity read over HTTP should change.</li> <li>Once this HTTP endpoint is called, an event should be published.</li> <li>Once this event is published, another event should be published.</li> </ul> <p>If you use <code>testify</code>, the <code>Eventually</code> and <code>EventuallyWithT</code> functions are helpful. (The second one allows using asserts inside). They run the assert code periodically until it&rsquo;s successful or until it times out. Use these instead of plain sleeps, as they slow down your tests.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">assert</span><span class="p">.</span><span class="nf">EventuallyWithT</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">assert</span><span class="p">.</span><span class="nx">CollectT</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">row</span> <span class="o">:=</span> <span class="nx">discountDB</span><span class="p">.</span><span class="nf">QueryRowContext</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="s">&#34;SELECT next_order_discount FROM user_discounts WHERE user_id = $1&#34;</span><span class="p">,</span> <span class="nx">userID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">discount</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">row</span><span class="p">.</span><span class="nf">Scan</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">discount</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">assert</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">expectedDiscount</span><span class="p">,</span> <span class="nx">discount</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">},</span> <span class="mi">2</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">,</span> <span class="mi">100</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2Ftests%2Fall_test.go%23L129" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/05-distributed-transactions/tests/all_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fc7ceec7a004c90d7477e588f8ba2461ab35f1793%2F05-distributed-transactions%2Ftests%2Fall_test.go%23L129" target="_blank">Full source</a> </div> <p>When reading events, watch out for others that may interfere. With many tests running in parallel, it&rsquo;s common to have many events of the same type published. You need to filter them based on the ID or some metadata. Using UUIDs for this is best so your tests are completely independent.</p> <p>Finally, run your tests in parallel to speed them up. With lots of time spent on I/O and waiting, this quickly adds up. There are some quirks to running Go tests in parallel, though. Robert is currently working on an article on how to do it right — <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fnewsletter">join our newsletter</a>, and we&rsquo;ll let you know once it&rsquo;s ready.</p> <h3 id="monitoring">Monitoring</h3> <p>Observability is a long topic, and there&rsquo;s much to be said about metrics and tracing for event-driven systems. This is beyond the scope of this post, but I want to mention monitoring the queue of waiting messages.</p> <p>If a message fails to be processed (i.e., the handler returns an error for whatever reason), it will be delivered again. Depending on the Pub/Sub you picked and its configuration, it can block other messages from being delivered.</p> <p>This is your key metric to monitor. Having unprocessed messages for a long time usually means something went wrong, and it&rsquo;s not a temporary issue. You need manual intervention, or your system will be out of sync.</p> <h2 id="more-examples">More Examples</h2> <p>This scratches the surface of switching to eventual consistency, but it should be enough to get you started. To learn about different use cases, check <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%3Ftab%3Dreadme-ov-file%23examples" target="_blank">Watermill examples</a>. There are some basic ones, but also more advanced and interesting examples.</p> <p>As with most programming topics, reading about something is rarely enough to fully get it. For me, building something on my own is what makes me fully understand it. But it&rsquo;s often hard to come up with an idea for a project to build. We wanted to recreate this way of learning, so we designed a hands-on <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fevent-driven%2F%3Futm_source%3Ddistributed-transactions" target="_blank">event-driven training</a>. Consider joining us if you like learning like this.</p>Database Transactions in Go with Layered Architecturehttps://threedots.tech/post/database-transactions-in-go/Thu, 05 Sep 2024 00:00:00 +0200https://threedots.tech/post/database-transactions-in-go/<p>As I join a new company, I often feel like an impostor. After all the interviews, they really seem to know what they&rsquo;re doing. I&rsquo;m humbled and ready to learn from the best.</p> <p>On one such occasion, a few days in, I dealt with a production outage and asked the most senior engineer for help. They came to the rescue and casually flipped a value in the database with a manual update. 🤯 The root cause was that a set of SQL updates were not done within a transaction. Suddenly, I regretted not asking for higher compensation right away.</p> <p>Onboarding is fun. What I learned this way is that even if something seems like a fundamental concept (e.g., SQL transactions), it may often be overlooked.</p> <p>SQL seems like something we all know well, and there are few surprises. (It&rsquo;s 50 years old!) Perhaps it&rsquo;s a good time to reconsider, as we&rsquo;re past the &ldquo;NoSQL is cool&rdquo; hype phase and back to &ldquo;just use Postgres&rdquo;, or even &ldquo;SQLite is good enough&rdquo;.</p> <p>I want to focus here on how to keep transactions in the code rather than on their technical complexity. Once you need to organize a bigger project, perhaps by using layers, splitting the logic from the database code is not always that obvious. It&rsquo;s easy to end up with a mess leading to obscure bugs.</p> <p><strong>The key idea of layers is to keep the critical parts of code (the logic) out of the implementation details (like the SQL queries).</strong> One way to achieve this separation is <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F" target="_blank">the Repository pattern</a>. The hard part is handling transactions in this setup. When using transactions, you usually fetch data from the database, run some logic, update the data, and commit the transaction.</p> <p>How do we model this in the code so we don&rsquo;t end up with a mess across the layers? I often see people asking about this, and I know the pain from my experience. We had to figure this out, and the ideas evolved over time. Here&rsquo;s a list of various approaches you can consider and everything I&rsquo;ve learned along the way.</p> <h2 id="sql-transactions-101">SQL Transactions 101</h2> <p>Let&rsquo;s quickly reiterate why we need transactions at all.</p> <p>Consider an e-commerce web app where users receive virtual points for purchasing products. The points can later be spent on discounts.</p> <p>The user decides how many points they want to spend on a discount. We have to check if the user has enough points, then decrease them and apply a proper discount for the next order.</p> <p>Let&rsquo;s say a user wants to use 100 points. It could look roughly like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">points</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">19</span><span class="p">;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- in the code: check if points &gt;= 100 </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">points</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">points</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">19</span><span class="p">;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">user_discounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">next_order_discount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">next_order_discount</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">19</span><span class="p">;</span><span class="w"> </span></span></span></code></pre></div><p>We should consider what can go wrong here, even if it&rsquo;s unlikely.</p> <p>Our application can suddenly exit after the first <code>UPDATE</code> statement. Or, a network outage could prevent the second <code>UPDATE</code> from being executed. In this case, we would decrease the user&rsquo;s points without giving them a discount.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1235" height="296" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2Fimages%2F1-broken-updates_hud59b0b8b153a28fe8e882eb7778a6885_61103_1235x296_resize_q80_h2_lanczos_3.webp" alt="Broken Updates" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdatabase-transactions-in-go%5C%2Fimages%5C%2F1-broken-updates_hud59b0b8b153a28fe8e882eb7778a6885_61103_1235x296_resize_lanczos_3.png"" /> <p>Or a different scenario: If the user sends two (or more) requests very quickly, they could be processed concurrently. If the requests execute the <code>SELECT</code> query before any updates, the check for the points balance won&rsquo;t work correctly, and the user could end up with -100 points and a 200 discount value.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1256" height="532" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2Fimages%2F2-parallel-requests_hu6706b1183317b321413ba80204b8a007_85789_1256x532_resize_q80_h2_lanczos_3.webp" alt="Parallel Requests" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdatabase-transactions-in-go%5C%2Fimages%5C%2F2-parallel-requests_hu6706b1183317b321413ba80204b8a007_85789_1256x532_resize_lanczos_3.png"" /> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Skipping transactions</p> </div> <div class="notice-body"><p> <p>Use transactions for sets of queries that rely on each other.</p> <p>Even if unlikely, things can go wrong mid-request. You&rsquo;ll be left with inconsistencies in the system that need manual effort to be understood and fixed.</p> <p><strong>If something can theoretically happen, it will likely happen in production at some point.</strong> Don&rsquo;t accept solutions that <em>almost always</em> work. The 1% chance can turn into a nightmare investigation scenario in which you can&rsquo;t understand what happened in your application and how to fix it.</p> </p></div> </div> <p>We can easily improve this by introducing a transaction.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line hl"><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w"> </span></span></span><span class="line hl"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">points</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">where</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">19</span><span class="w"> </span><span class="k">FOR</span><span class="w"> </span><span class="k">UPDATE</span><span class="p">;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- in the code: check if points &gt;= 100 </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">points</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">points</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">19</span><span class="p">;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">user_discounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">next_order_discount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">next_order_discount</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">19</span><span class="p">;</span><span class="w"> </span></span></span><span class="line hl"><span class="cl"><span class="w"></span><span class="k">COMMIT</span><span class="p">;</span><span class="w"> </span></span></span></code></pre></div><p>Now, either all of the updates get saved or none.</p> <p>Besides the <code>BEGIN</code> and <code>COMMIT</code> statements that manage the transaction, note the <code>FOR UPDATE</code> clause added to the <code>SELECT</code>. It locks the row so that further <code>SELECTS</code> have to wait until this transaction is done. This lets us correctly process parallel requests.</p> <div class="notice warning"> <div class="notice-head"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20" > <path d="M10 1L1 18h18L10 1zM10 7v6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <circle cx="10" cy="15.5" r="1" fill="currentColor"/> </svg> <p>Warning</p> </div> <div class="notice-body"><p> <p><code>FOR UPDATE</code> works well for simple uses cases, but it may cause performance issues. It locks the entire row, so if many concurrent updates are competing for it, they will all have to wait. On a bigger scale, this can lead to a bottleneck.</p> <p>An alternative is to use another isolation level, like <code>REPEATABLE READ</code>. This is out of the scope of this post but you can check out <a href="proxy.php?url=https%3A%2F%2Fmkdev.me%2Fposts%2Ftransaction-isolation-levels-with-postgresql-as-an-example" target="_blank">this article</a> for details.</p> <p>Whatever you choose, consider running stress tests to see how your code behaves under load. It will help you avoid surprises in production when a traffic spike hits.</p> </p></div> </div> <h2 id="layers">Layers</h2> <p>Splitting code into layers is pretty common, no matter how many you use or what you call the approach (<em>clean</em>, <em>hexagonal</em>, <em>ports &amp; adapters</em>, <em>onion</em>, etc.). It makes sense for many reasons, like separation of concerns, enabling parallel work, and easier testing. We wrote about these concepts in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Ftags%2Fclean-architecture%2F" target="_blank">our other articles</a>.</p> <p>For the rest of this post, I assume you keep your SQL code in a place (struct, file, package, whatever) separate from your application logic and inject one into the other. In the example snippets, I keep the &ldquo;layers&rdquo; in different files but in the same package for simplicity.</p> <ul> <li><code>http.go</code> — the HTTP handler (we don&rsquo;t care about this one much).</li> <li><code>app.go</code> — the application logic.</li> <li><code>repository.go</code> — the repository (the database storage).</li> </ul> <img title="" loading="lazy" decoding="async" class="img img-center" width="1180" height="316" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2Fimages%2F12-layers_huecda7135195598134196654b5604bb7e_44333_1180x316_resize_q80_h2_lanczos_3.webp" alt="Layers" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdatabase-transactions-in-go%5C%2Fimages%5C%2F12-layers_huecda7135195598134196654b5604bb7e_44333_1180x316_resize_lanczos_3.png"" /> <p>In the application logic, I define the <code>UsePointsAsDiscount</code> command and a handler for it (see <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbasic-cqrs-in-go%2F" target="_blank">Robert&rsquo;s CQRS article</a> for more details on this). You may prefer using something slightly different, like a <em>service</em> struct with methods or <em>use cases</em>. That&rsquo;s all fine. The key is that this part of the code knows nothing about the database used. The repository is injected inside the command handler using an interface defined close to the handler&rsquo;s definition.</p> <p>Here&rsquo;s how the code can look like with no transactions.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">UsePointsAsDiscount</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserID</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="nx">Points</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">UsePointsAsDiscountHandler</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">userRepository</span> <span class="nx">UserRepository</span> </span></span><span class="line"><span class="cl"> <span class="nx">discountRepository</span> <span class="nx">DiscountRepository</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">UserRepository</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">GetPoints</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">TakePoints</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">points</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">DiscountRepository</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">AddDiscount</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">discount</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewUsePointsAsDiscountHandler</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">userRepository</span> <span class="nx">UserRepository</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">discountRepository</span> <span class="nx">DiscountRepository</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="nx">UsePointsAsDiscountHandler</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">UsePointsAsDiscountHandler</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">userRepository</span><span class="p">:</span> <span class="nx">userRepository</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">discountRepository</span><span class="p">:</span> <span class="nx">discountRepository</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">UsePointsAsDiscountHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">UsePointsAsDiscount</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;points must be greater than 0&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">currentPoints</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">userRepository</span><span class="p">.</span><span class="nf">GetPoints</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not get points: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">currentPoints</span> <span class="p">&lt;</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;not enough points&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">userRepository</span><span class="p">.</span><span class="nf">TakePoints</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not take points: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">discountRepository</span><span class="p">.</span><span class="nf">AddDiscount</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not add discount: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F01-no-tx%2Fapp.go%23L9" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/01-no-tx/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F01-no-tx%2Fapp.go%23L9" target="_blank">Full source</a> </div> <p>There are two repositories: one for the user and one for the discounts. The handler&rsquo;s logic is rather easy to grasp: we validate the command (points must be positive), check if the user has enough points, and subtract them from their account while adding a discount.</p> <p>The repositories are trivial:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">PostgresDiscountRepository</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DB</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewPostgresDiscountRepository</span><span class="p">(</span><span class="nx">db</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DB</span><span class="p">)</span> <span class="o">*</span><span class="nx">PostgresDiscountRepository</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">PostgresDiscountRepository</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">:</span> <span class="nx">db</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">PostgresDiscountRepository</span><span class="p">)</span> <span class="nf">AddDiscount</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">discount</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">ExecContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;UPDATE user_discounts SET next_order_discount = next_order_discount + $1 WHERE user_id = $2&#34;</span><span class="p">,</span> <span class="nx">discount</span><span class="p">,</span> <span class="nx">userID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F01-no-tx%2Frepository.go%23L52" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/01-no-tx/repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F01-no-tx%2Frepository.go%23L52" target="_blank">Full source</a> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> In your project, you may call the repository a <em>storage</em> or something else. It&rsquo;s a similar concept, although the Repository pattern is not only about database access. See <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F" target="_blank">Robert&rsquo;s post</a> for more details. </p></div> </div> <p>We&rsquo;d like all of this to run within a single transaction, but we have a bunch of logic mixed with two repositories. Let&rsquo;s see how to approach this.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1633" height="1108" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2Fimages%2F3-no-tx_hu7b1578eb8b5bf6a11bb3dfb279bf30dc_145957_1633x1108_resize_q80_h2_lanczos_3.webp" alt="No Transaction" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdatabase-transactions-in-go%5C%2Fimages%5C%2F3-no-tx_hu7b1578eb8b5bf6a11bb3dfb279bf30dc_145957_1633x1108_resize_lanczos_3.png"" /> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> All examples are available in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns" target="_blank">go-web-app-antipatterns repository</a>. There&rsquo;s a Docker Compose definition that lets you run all of them locally. They use Postgres as the database. </p></div> </div> <h2 id="transactions-in-the-logic-layer-avoid-if-you-can">Transactions in the logic layer (avoid if you can)</h2> <p>The first idea for dealing with transactions across repositories is passing a transaction object around.</p> <p>For running code in a transaction, we can use a helper function like the <code>runInTx</code> below.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">runInTx</span><span class="p">(</span><span class="nx">db</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DB</span><span class="p">,</span> <span class="nx">fn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">tx</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">Tx</span><span class="p">)</span> <span class="kt">error</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">tx</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">Begin</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nf">fn</span><span class="p">(</span><span class="nx">tx</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Commit</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">rollbackErr</span> <span class="o">:=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">rollbackErr</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">rollbackErr</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F02-tx-in-logic%2Fapp.go%23L71" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/02-tx-in-logic/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F02-tx-in-logic%2Fapp.go%23L71" target="_blank">Full source</a> </div> <p>It takes a function (you&rsquo;ll see many anonymous functions in this post) that receives the transaction object. Whatever happens within the <code>fn</code> function doesn&rsquo;t know or care how the transaction is started, committed, or rolled back.</p> <p>Here&rsquo;s how it works when used inside the command handler.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">UsePointsAsDiscountHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">UsePointsAsDiscount</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line hl"><span class="cl"> <span class="k">return</span> <span class="nf">runInTx</span><span class="p">(</span><span class="nx">h</span><span class="p">.</span><span class="nx">db</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">tx</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">Tx</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;points must be greater than 0&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">currentPoints</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">userRepository</span><span class="p">.</span><span class="nf">GetPoints</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">tx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not get points: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">currentPoints</span> <span class="p">&lt;</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;not enough points&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">userRepository</span><span class="p">.</span><span class="nf">TakePoints</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">tx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not take points: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">discountRepository</span><span class="p">.</span><span class="nf">AddDiscount</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">tx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not add discount: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F02-tx-in-logic%2Fapp.go%23L42" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/02-tx-in-logic/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F02-tx-in-logic%2Fapp.go%23L42" target="_blank">Full source</a> </div> <p>The good part is that this approach works. But we can&rsquo;t ignore the fact that it mixes the application logic with implementation details (the SQL transaction). It may not seem like a big deal: we will likely not change the database, so why be concerned about it?</p> <p>For one, <strong>handling the transaction with the commit/rollback sequence complicates the flow</strong>. Instead of working with plain application logic, you now have to consider this additional behavior, which has nothing to do with what your application does. As the logic grows, you must carefully consider whether something should work within the transaction or outside of it. A rollback will affect everything you put inside the function.</p> <p>Not to mention the weird <code>tx</code> argument you must pass to the repository methods. <strong>Testing becomes awkward</strong> because you need to pass the transaction even if the method doesn&rsquo;t need it at the time (the <code>GetPoints</code> method could also work without transaction in another context). You can&rsquo;t mock the SQL connection to test the command handler, so it becomes an integration test instead of a simple unit test checking the logic.</p> <p>You could try to work around this with an abstract <code>Transaction</code> object or perhaps pass it through the <code>Context</code>, but it only masks the root issue of mixing the implementation details with the logic.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Transactions mixed with logic</p> </div> <div class="notice-body"><p> Avoid mixing transactions with your application logic. It&rsquo;s challenging to grasp how it works, test the logic, and debug issues. </p></div> </div> <img title="" loading="lazy" decoding="async" class="img img-center" width="1630" height="1529" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2Fimages%2F4-tx-in-app_hu55afc0bbcc7419e42ac6996c7aa45bb8_194017_1630x1529_resize_q80_h2_lanczos_3.webp" alt="Transaction in app" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdatabase-transactions-in-go%5C%2Fimages%5C%2F4-tx-in-app_hu55afc0bbcc7419e42ac6996c7aa45bb8_194017_1630x1529_resize_lanczos_3.png"" /> <h2 id="transactions-inside-the-repository-better-but-far-from-perfect">Transactions inside the repository (better, but far from perfect)</h2> <p>If the transaction belongs to the database layer, why not keep it there? The hard part is that we deal with two repositories, so they must somehow share the transaction object. Sometimes, it&rsquo;s really the case (and I&rsquo;ll show you how to do it in a bit). But first, we should consider if we really need two repositories.</p> <p>It&rsquo;s common to think of the database tables as your entities. This way, you end up with one repository per table and the application logic orchestrating them. Often, this split of repositories has no practical value.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1549" height="358" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2Fimages%2F5a-table-driven-design_hu205cfe7af515c2e04c2f74fd29311386_75358_1549x358_resize_q80_h2_lanczos_3.webp" alt="Table Driven Design" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdatabase-transactions-in-go%5C%2Fimages%5C%2F5a-table-driven-design_hu205cfe7af515c2e04c2f74fd29311386_75358_1549x358_resize_lanczos_3.png"" /> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: One repository per database table</p> </div> <div class="notice-body"><p> Don&rsquo;t create a repository for each database table. Instead, think of the data that needs to be transactionally stored together. </p></div> </div> <p>Data that should be transactionally consistent should also be coherent and kept as one. Domain-Driven Design proposes the idea of an <em>aggregate</em> — a set of data that must always be consistent. If you follow this idea, <strong>you don&rsquo;t keep a repository per SQL table, but a repository per aggregate.</strong> (It&rsquo;s the short version of the whole idea. We have a proper article coming up on aggregates soon.)</p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/database-transactions-in-go/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;web-applications;anti-patterns;transactions;databases;clean-architecture"> <input type="hidden" name="fields[blog_series]" value=""> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <p>We think of the <em>user</em> and the <em>discounts</em> as separate concepts (entities, structs, etc.). And it makes sense, as conceptually, they are different things. The user is used for identity and authentication. Placing orders with discounts is just one of many things a user can do on the website. But we also want to keep the user&rsquo;s points and discounts consistent. We can consider them part of the same <strong>aggregate — a set of objects stored together transactionally</strong>.</p> <p>In practice, we can consider discounts part of the <code>User</code> aggregate, even if we store them in another SQL table. In that case, we need just one repository. And this lets us move the transaction handling there.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1561" height="401" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2Fimages%2F5b-aggregate_hue35b2cdd1f77d324d9cbb96b89943f54_77598_1561x401_resize_q80_h2_lanczos_3.webp" alt="Aggregate" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdatabase-transactions-in-go%5C%2Fimages%5C%2F5b-aggregate_hue35b2cdd1f77d324d9cbb96b89943f54_77598_1561x401_resize_lanczos_3.png"" /> <p>The application logic becomes trivial now.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">UsePointsAsDiscountHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">UsePointsAsDiscount</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;points must be greater than 0&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">userRepository</span><span class="p">.</span><span class="nf">UsePointsForDiscount</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not use points as discount: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F03-tx-in-repo%2Fapp.go%23L30" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/03-tx-in-repo/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F03-tx-in-repo%2Fapp.go%23L30" target="_blank">Full source</a> </div> <p>And the repository exposes a method specific to this one operation.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">PostgresUserRepository</span><span class="p">)</span> <span class="nf">UsePointsForDiscount</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">points</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">runInTx</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">db</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">tx</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">Tx</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">row</span> <span class="o">:=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">QueryRowContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;SELECT points FROM users WHERE id = $1 FOR UPDATE&#34;</span><span class="p">,</span> <span class="nx">userID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">currentPoints</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">row</span><span class="p">.</span><span class="nf">Scan</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">currentPoints</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">currentPoints</span> <span class="p">&lt;</span> <span class="nx">points</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;not enough points&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">ExecContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;UPDATE users SET points = points - $1 WHERE id = $2&#34;</span><span class="p">,</span> <span class="nx">points</span><span class="p">,</span> <span class="nx">userID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">ExecContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;UPDATE user_discounts SET next_order_discount = next_order_discount + $1 WHERE user_id = $2&#34;</span><span class="p">,</span> <span class="nx">points</span><span class="p">,</span> <span class="nx">userID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F9ca762d3a321726a9f9b95ca609973dec236bf36%2F04-transactions%2F03-tx-in-repo%2Frepository.go%23L36" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/03-tx-in-repo/repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F9ca762d3a321726a9f9b95ca609973dec236bf36%2F04-transactions%2F03-tx-in-repo%2Frepository.go%23L36" target="_blank">Full source</a> </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Aggregates</p> </div> <div class="notice-body"><p> Keep the data that needs to be strongly consistent within the same aggregate. Keep a repository for the aggregate. </p></div> </div> <img title="" loading="lazy" decoding="async" class="img img-center" width="1173" height="1397" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2Fimages%2F6-tx-in-repository_hu2491433a23c97e14e4debce027c94fe6_163946_1173x1397_resize_q80_h2_lanczos_3.webp" alt="Transaction in repository" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdatabase-transactions-in-go%5C%2Fimages%5C%2F6-tx-in-repository_hu2491433a23c97e14e4debce027c94fe6_163946_1173x1397_resize_lanczos_3.png"" /> <p>This approach still has some downsides. One is that we had to move the &ldquo;has enough points&rdquo; logic to the repository. The method name (<code>UsePointsForDiscount</code>) makes it obvious the repository knows something about the logic, and occasionally it may be fine. But ideally, we&rsquo;d have the logic in the command handler.</p> <p>The other issue is that with many methods like this, the repository&rsquo;s interface grows and gets difficult to maintain and test. This is against the layers split we wanted to achieve.</p> <p>While useful, this approach doesn&rsquo;t scale well. It seems we could use a more generic <code>Update</code> method.</p> <h2 id="the-updatefn-pattern-our-go-to-solution">The UpdateFn Pattern (our go-to solution)</h2> <p>A universal <code>Update</code> method should load the user from the database, allow us to make any changes, and store the result. The challenge, again, is how to make it work with transactions.</p> <p>First, we need a model that combines the user with discounts (our aggregate). I use encapsulation to hide the fields, as we want the model to be always valid in memory. The only way to change the state is by calling exported methods.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">id</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="nx">email</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">points</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="nx">discounts</span> <span class="o">*</span><span class="nx">Discounts</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">u</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="nf">ID</span><span class="p">()</span> <span class="kt">int</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">u</span><span class="p">.</span><span class="nx">id</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">u</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="nf">Email</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">u</span><span class="p">.</span><span class="nx">email</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">u</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="nf">Points</span><span class="p">()</span> <span class="kt">int</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">u</span><span class="p">.</span><span class="nx">points</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">u</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="nf">Discounts</span><span class="p">()</span> <span class="o">*</span><span class="nx">Discounts</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">u</span><span class="p">.</span><span class="nx">discounts</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Discounts</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">nextOrderDiscount</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="o">*</span><span class="nx">Discounts</span><span class="p">)</span> <span class="nf">NextOrderDiscount</span><span class="p">()</span> <span class="kt">int</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">c</span><span class="p">.</span><span class="nx">nextOrderDiscount</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F04-update-func-closure%2Fmodels.go%23L5" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/04-update-func-closure/models.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F04-update-func-closure%2Fmodels.go%23L5" target="_blank">Full source</a> </div> <p>Adding a discount works by calling <code>UsePointsAsDiscount</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">u</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="nf">UsePointsAsDiscount</span><span class="p">(</span><span class="nx">points</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">points</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;points must be greater than 0&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">u</span><span class="p">.</span><span class="nx">points</span> <span class="p">&lt;</span> <span class="nx">points</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;not enough points&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">u</span><span class="p">.</span><span class="nx">points</span> <span class="o">-=</span> <span class="nx">points</span> </span></span><span class="line"><span class="cl"> <span class="nx">u</span><span class="p">.</span><span class="nx">discounts</span><span class="p">.</span><span class="nx">nextOrderDiscount</span> <span class="o">+=</span> <span class="nx">points</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F04-update-func-closure%2Fmodels.go%23L12" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/04-update-func-closure/models.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F04-update-func-closure%2Fmodels.go%23L12" target="_blank">Full source</a> </div> <p>Easy to grasp, trivial to test. No need to think about database transactions when reading it. In fact, no need to think about database tables, too. That&rsquo;s the kind of code I want to work with.</p> <p>Now, let&rsquo;s introduce the update method interface.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">UserRepository</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">UpdateByID</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">))</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F04-update-func-closure%2Fapp.go%23L16" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/04-update-func-closure/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F04-update-func-closure%2Fapp.go%23L16" target="_blank">Full source</a> </div> <p>The <code>updateFn</code> argument is how we keep the logic separate from the database details. The function receives the <code>User</code> model that can be modified. Any changes made to it are saved to the storage if no error is returned.</p> <p>The <code>updateFn</code> returns an <code>updated</code> boolean value indicating whether the user has been updated. If <code>true</code> is returned, the repository saves the user. We don&rsquo;t have a use case for returning <code>false</code> now, but it&rsquo;s often helpful when updating multiple values (like in a generic <code>PATCH</code> update request). The update may be unnecessary and can be skipped then.</p> <p>The command handler looks like this now.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">UsePointsAsDiscountHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">UsePointsAsDiscount</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">.</span><span class="nx">userRepository</span><span class="p">.</span><span class="nf">UpdateByID</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">user</span><span class="p">.</span><span class="nf">UsePointsAsDiscount</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">false</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">true</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F04-update-func-closure%2Fapp.go%23L28" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/04-update-func-closure/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F04-update-func-closure%2Fapp.go%23L28" target="_blank">Full source</a> </div> <p>Let&rsquo;s take a look at the repository implementation.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">PostgresUserRepository</span><span class="p">)</span> <span class="nf">UpdateByID</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">))</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">runInTx</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">db</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">tx</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">Tx</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">row</span> <span class="o">:=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">QueryRowContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;SELECT email, points FROM users WHERE id = $1 FOR UPDATE&#34;</span><span class="p">,</span> <span class="nx">userID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">email</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">currentPoints</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">row</span><span class="p">.</span><span class="nf">Scan</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">email</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">currentPoints</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">row</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">QueryRowContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;SELECT next_order_discount FROM user_discounts WHERE user_id = $1 FOR UPDATE&#34;</span><span class="p">,</span> <span class="nx">userID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">discount</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">row</span><span class="p">.</span><span class="nf">Scan</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">discount</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">discounts</span> <span class="o">:=</span> <span class="nf">UnmarshalDiscounts</span><span class="p">(</span><span class="nx">discount</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">user</span> <span class="o">:=</span> <span class="nf">UnmarshalUser</span><span class="p">(</span><span class="nx">userID</span><span class="p">,</span> <span class="nx">email</span><span class="p">,</span> <span class="nx">currentPoints</span><span class="p">,</span> <span class="nx">discounts</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">updated</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">updateFn</span><span class="p">(</span><span class="nx">user</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">updated</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">ExecContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;UPDATE users SET email = $1, points = $2 WHERE id = $3&#34;</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nf">Email</span><span class="p">(),</span> <span class="nx">user</span><span class="p">.</span><span class="nf">Points</span><span class="p">(),</span> <span class="nx">user</span><span class="p">.</span><span class="nf">ID</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">ExecContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;UPDATE user_discounts SET next_order_discount = $1 WHERE user_id = $2&#34;</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nf">Discounts</span><span class="p">().</span><span class="nf">NextOrderDiscount</span><span class="p">(),</span> <span class="nx">user</span><span class="p">.</span><span class="nf">ID</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F04-update-func-closure%2Frepository.go%23L36" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/04-update-func-closure/repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F04-update-func-closure%2Frepository.go%23L36" target="_blank">Full source</a> </div> <p>The repository has no logic at all. It fetches the complete data from the user and discounts tables, translates them to application models (with the <code>Unmarshal</code> functions), and calls the <code>updateFn</code> function. Then, it stores everything back in the database. All of it happens within a transaction.</p> <div class="notice tip"> <div class="notice-head"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillrule="evenodd" cliprule="evenodd" d="M12 0c6.6274.0 12 5.37258 12 12 0 6.6274-5.3726 12-12 12C5.37258 24 0 18.6274.0 12 0 5.37258 5.37258.0 12 0zm0 2.4C6.69807 2.4 2.4 6.69807 2.4 12c0 5.3019 4.29807 9.6 9.6 9.6 5.3019.0 9.6-4.2981 9.6-9.6.0-5.30193-4.2981-9.6-9.6-9.6zm3.9515 5.15147L9.6 13.9029 8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515c-.46863.4686-.46863 1.2284.0 1.697l2.4 2.4C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485l7.2-7.19997C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147c-.468599999999999-.46863-1.2284-.46863-1.697.0z" fill="currentcolor"></path> </svg> <p>Tip</p></div> <div class="notice-body"> <p> The repository is quite verbose now. Using an ORM can help you get rid of the boilerplate. But be careful what library you choose, as some ORMs can do more harm than good. Check the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flist-of-recommended-libraries%2F" target="_blank">libraries that never failed us</a>. </p> </div> </div> <p>With this, we achieved the goal: the logic stays in the command handler, and the transaction remains in the repository.</p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: The UpdateFn pattern</p> </div> <div class="notice-body"><p> Use an <code>Update</code> method that loads and stores the aggregate. Keep the logic in the <code>updateFn</code> closure. </p></div> </div> <img title="" loading="lazy" decoding="async" class="img img-center" width="1190" height="1620" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2Fimages%2F7-update-fn_hu3204e6752050999e502e7f8c3a1a16d4_212571_1190x1620_resize_q80_h2_lanczos_3.webp" alt="UpdateFn" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdatabase-transactions-in-go%5C%2Fimages%5C%2F7-update-fn_hu3204e6752050999e502e7f8c3a1a16d4_212571_1190x1620_resize_lanczos_3.png"" /> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p><strong>Isn&rsquo;t this inefficient?</strong></p> <p>A common concern about the <code>Update</code> method is that it needlessly updates all the fields even if they didn&rsquo;t change (for example, the <code>email</code> in the example above).</p> <p>Unless you&rsquo;re saving huge datasets or dealing with massive scale, it shouldn&rsquo;t really matter. You could track which fields changed and update only these, but it will likely only complicate your architecture with no actual performance gains. Be pragmatic and avoid premature optimization. If in doubt, run stress tests.</p> </p></div> </div> <h2 id="the-transaction-provider-for-the-edge-cases">The Transaction Provider (for the edge cases)</h2> <p>I mentioned that sometimes, keeping a transaction across two repositories makes sense. While your first reaction should be, &ldquo;Why isn&rsquo;t this a single repository?&rdquo; there may be a valid use case for it.</p> <p>It usually works well for things that are not strictly related to your application&rsquo;s domain but are more technical or platform-related. For example, consider keeping an audit log of the users&rsquo; actions. Saving it within the same transaction makes sense, but keeping it inside the <code>User</code> aggregate seems like overkill.</p> <p><strong>We can work around having an explicit transaction object mixed with logic using the Transaction Provider pattern.</strong> It works similarly to the first approach, but the application code is easier to read with the transaction mechanism working in the background.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">UsePointsAsDiscountHandler</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">txProvider</span> <span class="nx">txProvider</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">txProvider</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">Transact</span><span class="p">(</span><span class="nx">txFunc</span> <span class="kd">func</span><span class="p">(</span><span class="nx">adapters</span> <span class="nx">Adapters</span><span class="p">)</span> <span class="kt">error</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Adapters</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserRepository</span> <span class="nx">UserRepository</span> </span></span><span class="line"><span class="cl"> <span class="nx">AuditLogRepository</span> <span class="nx">AuditLogRepository</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F05-tx-provider%2Fapp.go%23L13" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/05-tx-provider/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F05-tx-provider%2Fapp.go%23L13" target="_blank">Full source</a> </div> <p>The transaction provider has a <code>Transact</code> method that takes a function as an argument. Within it, we can access <code>adapters</code> — a struct of all the dependencies we need, working within the same transaction.</p> <p>Here&rsquo;s how it looks in the command handler.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">UsePointsAsDiscountHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">UsePointsAsDiscount</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line hl"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">.</span><span class="nx">txProvider</span><span class="p">.</span><span class="nf">Transact</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">adapters</span> <span class="nx">Adapters</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line hl"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">adapters</span><span class="p">.</span><span class="nx">UserRepository</span><span class="p">.</span><span class="nf">UpdateByID</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">user</span> <span class="o">*</span><span class="nx">User</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">user</span><span class="p">.</span><span class="nf">UsePointsAsDiscount</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">false</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">true</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not use points as discount: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">log</span> <span class="o">:=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;used %d points as discount for user %d&#34;</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Points</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">UserID</span><span class="p">)</span> </span></span><span class="line hl"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">adapters</span><span class="p">.</span><span class="nx">AuditLogRepository</span><span class="p">.</span><span class="nf">StoreAuditLog</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">log</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;could not store audit log: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F05-tx-provider%2Fapp.go%23L42" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/05-tx-provider/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F05-tx-provider%2Fapp.go%23L42" target="_blank">Full source</a> </div> <p>Note that both <code>UserRepository</code> and <code>AuditLogRepository</code> come from <code>adapters</code>, not the handler.</p> <p>Also, we pass no transaction object to the repository methods. The repositories now take the <code>tx</code> in the constructor. It&rsquo;s handy not to depend on <code>*sql.Tx</code> directly but to use an interface. It also lets you pass an <code>*sql.DB</code> (for example, in tests).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">db</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">QueryRowContext</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">query</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">args</span> <span class="o">...</span><span class="kd">interface</span><span class="p">{})</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">Row</span> </span></span><span class="line"><span class="cl"> <span class="nf">ExecContext</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">query</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">args</span> <span class="o">...</span><span class="kd">interface</span><span class="p">{})</span> <span class="p">(</span><span class="nx">sql</span><span class="p">.</span><span class="nx">Result</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">PostgresUserRepository</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span> <span class="nx">db</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewPostgresUserRepository</span><span class="p">(</span><span class="nx">db</span> <span class="nx">db</span><span class="p">)</span> <span class="o">*</span><span class="nx">PostgresUserRepository</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">PostgresUserRepository</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">:</span> <span class="nx">db</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F05-tx-provider%2Frepository.go%23L31" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/05-tx-provider/repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F05-tx-provider%2Frepository.go%23L31" target="_blank">Full source</a> </div> <p>The <code>TransactionProvider</code> implementation looks like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">TransactionProvider</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DB</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewTransactionProvider</span><span class="p">(</span><span class="nx">db</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">DB</span><span class="p">)</span> <span class="o">*</span><span class="nx">TransactionProvider</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">TransactionProvider</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">:</span> <span class="nx">db</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="o">*</span><span class="nx">TransactionProvider</span><span class="p">)</span> <span class="nf">Transact</span><span class="p">(</span><span class="nx">txFunc</span> <span class="kd">func</span><span class="p">(</span><span class="nx">adapters</span> <span class="nx">Adapters</span><span class="p">)</span> <span class="kt">error</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">runInTx</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">db</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">tx</span> <span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">Tx</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">adapters</span> <span class="o">:=</span> <span class="nx">Adapters</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserRepository</span><span class="p">:</span> <span class="nf">NewPostgresUserRepository</span><span class="p">(</span><span class="nx">tx</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">AuditLogRepository</span><span class="p">:</span> <span class="nf">NewPostgresAuditLogRepository</span><span class="p">(</span><span class="nx">tx</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">txFunc</span><span class="p">(</span><span class="nx">adapters</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F05-tx-provider%2Fapp.go%23L43" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/04-transactions/05-tx-provider/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F27beeb3f4602de5ae915b7e731c53e5dc09dacf5%2F04-transactions%2F05-tx-provider%2Fapp.go%23L43" target="_blank">Full source</a> </div> <p>This approach seems clean enough, although you have to be careful. With a set of transaction adapters, it&rsquo;s easy to go too far and start calling methods from different repositories in a single handler. It shouldn&rsquo;t become your go-to tool. Stick to the <code>UpdateFn</code> pattern instead.</p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: The Transaction Provider</p> </div> <div class="notice-body"><p> Sometimes, it makes sense to share a transaction across repositories. In such a scenario, use the transaction provider pattern. Be careful not to over-use it. </p></div> </div> <img title="" loading="lazy" decoding="async" class="img img-center" width="1638" height="1530" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2Fimages%2F8-tx-provider_hue97e9cc6abbf4d11a1cdc7b6e252eacf_174542_1638x1530_resize_q80_h2_lanczos_3.webp" alt="Transaction Provider" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fdatabase-transactions-in-go%5C%2Fimages%5C%2F8-tx-provider_hue97e9cc6abbf4d11a1cdc7b6e252eacf_174542_1638x1530_resize_lanczos_3.png"" /> <div class="notice warning"> <div class="notice-head"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="20" height="20" > <path d="M10 1L1 18h18L10 1zM10 7v6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <circle cx="10" cy="15.5" r="1" fill="currentColor"/> </svg> <p>Warning</p> </div> <div class="notice-body"><p> <p>The transaction provider is a risky pattern. If you use it together with <code>FOR UPDATE</code>, you need to be extra careful, as all the <code>SELECT</code>s need to include the <code>FOR UPDATE</code> clause to work correctly.</p> <p>This can quickly get out of control when working within a team with rapid changes.</p> <p>Once again, consider using a different isolation level, like <code>REPEATABLE READ</code>.</p> </p></div> </div> <h2 id="distributed-transactions">Distributed Transactions</h2> <p>This wraps up our way of working with SQL transactions within a single service. But there&rsquo;s a related topic that often comes up and that we should consider: <strong>transactions across services</strong>. I cover this in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdistributed-transactions-in-go">Distributed Transactions in Go: Read Before You Try</a>.</p> <h2 id="check-the-source-code">Check the Source Code</h2> <p>You can see the complete source code on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns" target="_blank">GitHub</a>. There are basic tests that verify that all examples work in the same way. You can run it all locally with Docker Compose.</p>Live website updates with Go, SSE, and htmxhttps://threedots.tech/post/live-website-updates-go-sse-htmx/Thu, 13 Jun 2024 00:00:00 +0200https://threedots.tech/post/live-website-updates-go-sse-htmx/<p>In case you missed the memo, the Single Page Application hype period is over, and we&rsquo;re now back to <del>PHP and jQuery,</del> I mean rendering HTML on the server. I&rsquo;m excited! It brings me back to the early 2000s when we were all web developers, not frontend or backend engineers.</p> <p>But there&rsquo;s one thing I would miss from the SPA era: <strong>live updates</strong>. The classic websites often relied on the &ldquo;refresh&rdquo; button, which wasn&rsquo;t that great. While polling for updates periodically is a solution, it&rsquo;s inefficient. It&rsquo;s much better to push updates to the client once they happen.</p> <p>This post shows how to push live updates to your website using Go, Server-Sent Events (SSE), and htmx. As the example project, I use a tiny microblogging website where you can react to posts.</p> <p>Below, you can see the embedded example in two &ldquo;windows&rdquo;. You can click on reactions and see them update in real-time in the other window. You will also see the reactions and views counters update as other readers interact with the posts. (You can open the example in a new tab <a href="proxy.php?url=https%3A%2F%2Fgo-sse-example.threedots.tech" target="_blank">here</a>).</p> <div class="app-windows"> <div class="browser-mockup with-url"> <iframe loading="lazy" src="proxy.php?url=https%3A%2F%2Fgo-sse-example.threedots.tech" scrolling="no" width="524" height="492"></iframe> </div> <div class="browser-mockup with-url"> <iframe loading="lazy" src="proxy.php?url=https%3A%2F%2Fgo-sse-example.threedots.tech" scrolling="no" width="524" height="492"></iframe> </div> </div> <h2 id="server-sent-events">Server-Sent Events</h2> <p>WebSockets seem like the most popular option for pushing live updates from the HTTP server to the browser. Meanwhile, Server-Sent Events (SSE) is a great alternative. It is simple to set up and good enough for many use cases. It uses standard HTTP connections, so you don&rsquo;t need a custom protocol. All modern browsers support SSE.</p> <p>SSE endpoints work just like standard HTTP endpoints with a slight twist. You set the <code>Content-Type</code> header to <code>text/event-stream</code> and keep writing data in a text format like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">event</span><span class="p">:</span> <span class="nx">notifications</span> </span></span><span class="line"><span class="cl"><span class="nx">data</span><span class="p">:</span> <span class="p">{</span><span class="s">&#34;unread_messages&#34;</span><span class="p">:</span> <span class="mi">14</span><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">event</span><span class="p">:</span> <span class="nx">message</span> </span></span><span class="line"><span class="cl"><span class="nx">data</span><span class="p">:</span> <span class="p">&lt;</span><span class="nx">h1</span><span class="p">&gt;</span><span class="nx">Hello</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="nx">data</span><span class="p">:</span> <span class="nx">world</span><span class="p">!&lt;</span><span class="o">/</span><span class="nx">h1</span><span class="p">&gt;</span> </span></span></code></pre></div><p>What follows the <code>event:</code> line is an optional event <em>type</em>, which can be any string you want. The multiline <code>data</code> field is the payload sent to the browser. Every &ldquo;event&rdquo; is separated by an extra new line. The specification mentions a few more things, but that&rsquo;s all you need to start.</p> <p>In Go, SSE endpoints are slightly different from <code>net/http</code> endpoints because you don&rsquo;t just write data to the <code>ResponseWriter</code> and return from the function. Instead, you reply with the status code (<code>200 OK</code>) and keep writing the &ldquo;events&rdquo; in the format above. The connection stays alive until the client (the browser) closes it.</p> <p>Here&rsquo;s a complete example of sending back a &ldquo;ping&rdquo; event every 10 seconds.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">http</span><span class="p">.</span><span class="nf">HandleFunc</span><span class="p">(</span><span class="s">&#34;/ping&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Content-Type&#34;</span><span class="p">,</span> <span class="s">&#34;text/event-stream&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusOK</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">select</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">().</span><span class="nf">Done</span><span class="p">():</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="k">default</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprintf</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;data: ping\n\n&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">f</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">w</span><span class="p">.(</span><span class="nx">http</span><span class="p">.</span><span class="nx">Flusher</span><span class="p">);</span> <span class="nx">ok</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">f</span><span class="p">.</span><span class="nf">Flush</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="mi">10</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">http</span><span class="p">.</span><span class="nf">ListenAndServe</span><span class="p">(</span><span class="s">&#34;:8080&#34;</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Two things are worth mentioning here.</p> <ul> <li>To avoid buffering, we use the <code>http.Flusher</code> to send the response immediately.</li> <li>To exit the infinite loop, we check if the request&rsquo;s context is done.</li> </ul> <p>On the client side, you can handle it like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">es</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">EventSource</span><span class="p">(</span><span class="s2">&#34;http://localhost:8080/ping&#34;</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="nx">es</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Received: </span><span class="si">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">data</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span> </span></span><span class="line"><span class="cl"> <span class="p">};</span> </span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span> </span></span></code></pre></div><p>Most online tutorials end here and wish you happy coding. Perhaps it&rsquo;s good enough if you want to create a demo project in one afternoon. But the example is far from something you would use in production. I don&rsquo;t want you to read this post, then go back to your project and think, &ldquo;Uhh, so what do I do now?&rdquo;</p> <p>If you follow our blog, you know we like to focus on real-world examples. Let&rsquo;s see how to use SSE in a more complex scenario — the microblog example you saw at the top of this page. It&rsquo;s not quick and easy, and there are many things to consider, but it should make you comfortable enough to use a similar mechanism in your projects.</p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/live-website-updates-go-sse-htmx/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;htmx;sse;watermill"> <input type="hidden" name="fields[blog_series]" value=""> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h2 id="the-microblog-example">The Microblog Example</h2> <p>The core component of the example is a &ldquo;post&rdquo;. The key feature is that the reactions and views are updated in real-time as other users interact with the post.</p> <div class="app-windows"> <div class="browser-mockup with-url"> <iframe loading="lazy" src="proxy.php?url=https%3A%2F%2Fgo-sse-example.threedots.tech" scrolling="no" width="524" height="492"></iframe> </div> <div class="browser-mockup with-url"> <iframe loading="lazy" src="proxy.php?url=https%3A%2F%2Fgo-sse-example.threedots.tech" scrolling="no" width="524" height="492"></iframe> </div> </div> <p>In this example, we&rsquo;ll render HTML on the server side, using no JavaScript code for interactivity except the htmx library.</p> <p>The complete example is on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx" target="_blank">GitHub</a>. You can run it locally using docker-compose.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h3 id="json-api-example">JSON API Example</h3> <p>For another example featuring a Twitter-like web app, see the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Fserver-sent-events" target="_blank">server-sent-events</a> example on the Watermill repository. It shows a similar approach using a Single Page Application with Vue.js and a JSON API.</p> </p></div> </div> <h3 id="tools-used">Tools used</h3> <p>Here&rsquo;s the stack I use in the example. I won&rsquo;t dive deep into all the components, but I&rsquo;ll mention them briefly here.</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fecho.labstack.com" target="_blank"><strong>Echo</strong></a> — a lightweight HTTP router I like for error handling that is simpler than in <code>net/http</code>.</li> <li><a href="proxy.php?url=https%3A%2F%2Ftempl.guide" target="_blank"><strong>templ</strong></a> — an HTML templates library based on code generation. It can get weird at times, but overall, I&rsquo;m happy with it and like it more than <code>html/template</code>. It&rsquo;s best used together with an IDE plugin.</li> <li><a href="proxy.php?url=https%3A%2F%2Fhtmx.org" target="_blank"><strong>htmx</strong></a> — a library for using AJAX and SSE with no need to write JavaScript.</li> <li><a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank"><strong>Watermill</strong></a> — an event-driven library we maintain for working with messages.</li> <li><strong>PostgreSQL</strong> and <strong>Google Cloud Pub/Sub</strong> for storage and messaging infrastructure. (You can choose a different Pub/Sub for messaging, even Postgres.)</li> </ul> <h2 id="deciding-what-and-when-to-push">Deciding what and when to push</h2> <p>When designing an SSE endpoint, you must decide <em>what</em> the payload should be, <em>when</em> to send an update, and to <em>whom</em> to send it.</p> <h3 id="what">What</h3> <p>The payload is just text, and it&rsquo;s up to you how to encode it. It can be a regular JSON API response or an HTML you would embed directly on the website. Remember that each line should have the <code>data:</code> prefix, and the payload needs to end with two new lines (<code>\n\n</code>).</p> <h3 id="when">When</h3> <p>You need a way to know when something changes in your application so you can push the updates. For example, if the user receives a message, you show a red bubble in the UI.</p> <p>The SSE endpoints are long-running, so you may have hundreds or thousands of goroutines running in the background that you must notify of the change. In reaction, each should send an event to the client. Since you&rsquo;re likely running more than one instance of your service, this can&rsquo;t work in memory.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="508" height="547" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2Fimages%2Fevents-1_hue94f4a6bd5ed2a1c3a38d6ed99cae4ff_88612_508x547_resize_q80_h2_lanczos_3.webp" alt="Event to SSE" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Flive-website-updates-go-sse-htmx%5C%2Fimages%5C%2Fevents-1_hue94f4a6bd5ed2a1c3a38d6ed99cae4ff_88612_508x547_resize_lanczos_3.png"" /> <h3 id="to-whom">To Whom</h3> <p>You often only want to notify some users of something that happened. If I send you a message, I expect a notification to appear on your screen, but not for anyone else. So, you need a way to filter what happens and choose who should get the update (and which SSE endpoints to trigger).</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="505" height="564" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2Fimages%2Fevents-2_hu3889ca070725fe23178ca4697a0575f4_75189_505x564_resize_q80_h2_lanczos_3.webp" alt="Event to single SSE" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Flive-website-updates-go-sse-htmx%5C%2Fimages%5C%2Fevents-2_hu3889ca070725fe23178ca4697a0575f4_75189_505x564_resize_lanczos_3.png"" /> <p>In this example, it goes as follows:</p> <ul> <li><strong>What:</strong> the post &ldquo;stats&rdquo; model, including the numbers of views and reactions as HTML.</li> <li><strong>When:</strong> when someone sees the post or reacts to it.</li> <li><strong>To Whom:</strong> everyone who sees the updated post. Other posts are not updated.</li> </ul> <img title="" loading="lazy" decoding="async" class="img img-center" width="976" height="1030" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2Fimages%2Farchitecture_huc02c536378643281bc081fe58296d1c5_172188_976x1030_resize_q80_h2_lanczos_3.webp" alt="Architecture" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Flive-website-updates-go-sse-htmx%5C%2Fimages%5C%2Farchitecture_huc02c536378643281bc081fe58296d1c5_172188_976x1030_resize_lanczos_3.png"" /> <h2 id="implementing-sse-endpoints">Implementing SSE Endpoints</h2> <p>While you absolutely can create SSE endpoints from scratch (or with a library), and it&rsquo;s not that complex, the hard part is triggering the updates in reaction to something that happened. (And doing this over the network since running a single service instance rarely happens in production.) As with running an HTTP server, <strong>you don&rsquo;t want to reinvent the wheel here.</strong></p> <p>We usually approach anything events-related with <a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">Watermill</a>. It&rsquo;s a Go library we maintain that abstracts away the low-level details of Pub/Subs. (Getting close to 7k GitHub stars ⭐️). You can use it with any existing codebase as it&rsquo;s not a framework but a lightweight library (just like htmx). It supports many Pub/Subs, so it&rsquo;s easy to start with the infrastructure you already have (even an SQL database).</p> <h3 id="watermill-primer">Watermill Primer</h3> <p>(Feel free to skip this part if you&rsquo;re already familiar with Watermill.)</p> <p>The <a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">documentation</a> goes in-depth on how Watermill works. Below is a TL;DR version.</p> <p>First, you need a <strong>Pub/Sub</strong> — a system that lets you work with messages across the network (also known as a &ldquo;message broker&rdquo; or a &ldquo;queue&rdquo;). Common picks are Kafka or RabbitMQ, but it could just as well be an SQL database.</p> <p>Watermill abstracts away all Pub/Subs into two interfaces:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Publisher</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">Publish</span><span class="p">(</span><span class="nx">topic</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">messages</span> <span class="o">...*</span><span class="nx">Message</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"> <span class="nf">Close</span><span class="p">()</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Subscriber</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">Subscribe</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">topic</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">&lt;-</span><span class="kd">chan</span> <span class="o">*</span><span class="nx">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">Close</span><span class="p">()</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>You can <code>Publish</code> messages and <code>Subscribe</code> to them. There&rsquo;s always a <code>topic</code> involved — a string that decides who gets the message.</p> <p>Here&rsquo;s basic Watermill architecture in one picture:</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1309" height="546" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2Fimages%2Fwatermill-1_hub0fa5a7eb204e5c9b5d8bdcd543f227e_105482_1309x546_resize_q80_h2_lanczos_3.webp" alt="Watermill on one picture" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Flive-website-updates-go-sse-htmx%5C%2Fimages%5C%2Fwatermill-1_hub0fa5a7eb204e5c9b5d8bdcd543f227e_105482_1309x546_resize_lanczos_3.png"" /> <p>The core part of Watermill is the <code>Message</code>. It is what the <code>Request</code> is for the <code>net/http</code> package. The simplest message has just an optional ID and a payload. The payload is a slice of bytes, so you can use any marshaling you want (JSON, Protocol Buffers, plain strings, etc.).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">msg</span> <span class="o">:=</span> <span class="nx">message</span><span class="p">.</span><span class="nf">NewMessage</span><span class="p">(</span><span class="nx">watermill</span><span class="p">.</span><span class="nf">NewUUID</span><span class="p">(),</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;Hello, world!&#34;</span><span class="p">))</span> </span></span></code></pre></div><p>While all Watermill&rsquo;s components are based on the <code>Publisher</code> and <code>Subscriber</code> interfaces, using them directly is a relatively low-level API. In this example, we&rsquo;ll use the CQRS component of Watermill, which is a higher-level API. It&rsquo;s based on the same ideas but removes some boilerplate, like serialization and deserialization. We&rsquo;ll use the <code>EventBus</code> to publish events and <code>EventProcessor</code> to subscribe to them.</p> <h2 id="high-level-architecture-overview">High-level architecture overview</h2> <img title="" loading="lazy" decoding="async" class="img img-center" width="976" height="1030" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2Fimages%2Farchitecture_huc02c536378643281bc081fe58296d1c5_172188_976x1030_resize_q80_h2_lanczos_3.webp" alt="Architecture" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Flive-website-updates-go-sse-htmx%5C%2Fimages%5C%2Farchitecture_huc02c536378643281bc081fe58296d1c5_172188_976x1030_resize_lanczos_3.png"" /> <p>We want to publish two events:</p> <ul> <li><code>PostViewed</code> is published when someone sees the post.</li> <li><code>PostReactionAdded</code> is published when someone reacts to a post.</li> </ul> <p>Each will have an event handler that updates the post&rsquo;s stats in the database. (A similar concept as an HTTP handler.) The handlers should also publish the <code>PostStatsUpdated</code> event. We will use it to trigger the SSE updates.</p> <h2 id="publishing-events">Publishing events</h2> <p>First, let&rsquo;s create a publisher. I use Google Cloud Pub/Sub Publisher, but it can be swapped with any other publisher supported by Watermill. All the configuration needs is a project ID.</p> <p><em>(Note that Google Cloud Pub/Sub is just one the Pub/Subs Watermill supports. You could easily change this to <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2F" target="_blank">another supported Pub/Sub</a>. Kind of like an ORM would work with MySQL, PostgreSQL, and SQLite.)</em></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">logger</span> <span class="o">:=</span> <span class="nx">watermill</span><span class="p">.</span><span class="nf">NewStdLogger</span><span class="p">(</span><span class="kc">false</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">publisher</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">googlecloud</span><span class="p">.</span><span class="nf">NewPublisher</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">googlecloud</span><span class="p">.</span><span class="nx">PublisherConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ProjectID</span><span class="p">:</span> <span class="nx">cfg</span><span class="p">.</span><span class="nx">PubSubProjectID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L41" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L41" target="_blank">Full source</a> </div> <p>Publisher works with messages, meaning you must marshal events (structs) into bytes and choose what topics to publish them to. It&rsquo;s very common to use the same marshaling for all events and topics that follow some convention, like the event name being part of them.</p> <p>We will use the <code>EventBus</code> component to simplify the publishing API. You can think of it as a high-level wrapper on the publisher (as you can see, it&rsquo;s the first argument). You pass the configuration options once and then can publish events with a single method call.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">eventBus</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nf">NewEventBusWithConfig</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">publisher</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">EventBusConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">GeneratePublishTopic</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">params</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">GenerateEventPublishTopicParams</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">params</span><span class="p">.</span><span class="nx">EventName</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">Marshaler</span><span class="p">:</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">JSONMarshaler</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">Logger</span><span class="p">:</span> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L51" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L51" target="_blank">Full source</a> </div> <p>The configuration takes a <code>Marshaler</code>, so we use the <code>cqrs.JSONMarshaler{}</code> (all messages will be marshaled to JSON).</p> <p>The <code>GeneratePublishTopic</code> function returns the topic&rsquo;s name based on the available parameters. Instead of passing a topic directly to <code>Publish,</code> we define this function to determine the topic based on the message. The <code>EventBus</code> calls this function every time a message is published. In this case, we&rsquo;ll use the <code>params.EventName</code>. So, if you consider a struct like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">PostViewed</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">PostID</span> <span class="kt">int</span> <span class="s">`json:&#34;post_id&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L17" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L17" target="_blank">Full source</a> </div> <p>It will be published on the <code>PostViewed</code> topic. (The chosen marshaler provides a way to extract the event name).</p> <p>Publishing events using the event bus is trivial. Thanks to the setup of marshaler and <code>GeneratePublishTopic</code>, we pass the event struct to <code>Publish</code>, and the rest happens behind the scenes. In the HTTP handler, we can use something like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">event</span> <span class="o">:=</span> <span class="nx">PostViewed</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">PostID</span><span class="p">:</span> <span class="nx">post</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">err</span> <span class="p">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">eventBus</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fhttp.go%23L62" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fhttp.go%23L62" target="_blank">Full source</a> </div> <h2 id="subscribing-to-events">Subscribing to events</h2> <p>I decided to make the HTTP endpoints just publish the events. The event handlers update the post&rsquo;s stats in the database asynchronously. This way, the client doesn&rsquo;t need to wait for the changes to be applied, and the view will be eventually updated via SSE.</p> <p>We need two event handlers to update the stats in the database. The first updates the views count, and the second updates the reactions count.</p> <p>The CQRS component used for subscribing to events is the <code>EventProcessor</code>. As with the <code>EventBus</code>, some setup needs to be done initially. But thanks to this, writing the handlers later will be very pleasant. The idea behind it is similar to the <code>EventBus</code>, but it&rsquo;s the other way around.</p> <p>First, create a Router. It&rsquo;s a similar concept to the HTTP routers you&rsquo;re familiar with. The component runs in the background and routes messages to handlers.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">router</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">message</span><span class="p">.</span><span class="nf">NewRouter</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">RouterConfig</span><span class="p">{},</span> <span class="nx">logger</span><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L65" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L65" target="_blank">Full source</a> </div> <p>Similarly to HTTP routers, Watermill&rsquo;s router supports middlewares. For example, you can add the <code>Recoverer</code> middleware so panics in handlers don&rsquo;t blow up your server.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">router</span><span class="p">.</span><span class="nf">AddMiddleware</span><span class="p">(</span><span class="nx">middleware</span><span class="p">.</span><span class="nx">Recoverer</span><span class="p">)</span> </span></span></code></pre></div><p>Now we can create the EventProcessor.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">eventProcessor</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nf">NewEventProcessorWithConfig</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">router</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">EventProcessorConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">GenerateSubscribeTopic</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">params</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">EventProcessorGenerateSubscribeTopicParams</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">params</span><span class="p">.</span><span class="nx">EventName</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">SubscriberConstructor</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">params</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">EventProcessorSubscriberConstructorParams</span><span class="p">)</span> <span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">Subscriber</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">googlecloud</span><span class="p">.</span><span class="nf">NewSubscriber</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">googlecloud</span><span class="p">.</span><span class="nx">SubscriberConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ProjectID</span><span class="p">:</span> <span class="nx">cfg</span><span class="p">.</span><span class="nx">PubSubProjectID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">GenerateSubscriptionName</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">topic</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;%v_%v&#34;</span><span class="p">,</span> <span class="nx">topic</span><span class="p">,</span> <span class="nx">params</span><span class="p">.</span><span class="nx">HandlerName</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">Marshaler</span><span class="p">:</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nx">JSONMarshaler</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">Logger</span><span class="p">:</span> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L72" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L72" target="_blank">Full source</a> </div> <p>The first argument is the router. It&rsquo;s similar to how the <code>EventBus</code> &ldquo;wrapped&rdquo; the publisher.</p> <p>Then comes the config. The <code>Marshaler</code> and <code>GenerateSubscribeTopic</code> are the same concepts as in the <code>EventBus</code>. The only difference is they come at the other end of the Pub/Sub. In the <code>EventBus</code>, the marshaler marshals the message, and the function decides to which topic to publish it. Here, the <code>Marshaler</code> unmarshals the message back on the struct, and <code>GenerateSubscribeTopic</code> decides to which topic to subscribe to.</p> <p><code>SubscriberConstructor</code> is what the name says: it returns a new <code>Subscriber</code>. You may wonder, why not use a single subscriber, as we did with the <code>publisher</code> in the <code>EventBus</code>?</p> <p>Publishing messages is straightforward: you marshal a struct, send the bytes to a topic, and you&rsquo;re done. Subscribing is where things get more interesting. For example, you run two replicas of the same service. How do you ensure that only one replica receives a message from the Pub/Sub?</p> <p>The strategy depends on the Pub/Sub. In Google Cloud Pub/Sub, you use a single &ldquo;subscription&rdquo; bound to a topic and share it among the replicas. That&rsquo;s why having a subscriber constructor is helpful in this context. It allows us to specify what subscription to use for each event type. In this example, the subscription joins the topic name with the handler name. For example, <code>PostViewed_UpdateViews</code>.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1317" height="638" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2Fimages%2Frouting-1_hue2d6422d41dfd900a23623d04b2ccb4e_97194_1317x638_resize_q80_h2_lanczos_3.webp" alt="Events Routing" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Flive-website-updates-go-sse-htmx%5C%2Fimages%5C%2Frouting-1_hue2d6422d41dfd900a23623d04b2ccb4e_97194_1317x638_resize_lanczos_3.png"" /> <p>As promised, with the setup done, adding message handlers is quite simple. Note that the functions are generic (with inferred types), so you work with strongly typed events! The handler name is used to generate the subscription name, so it needs to be unique within handlers.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">err</span> <span class="p">=</span> <span class="nx">eventProcessor</span><span class="p">.</span><span class="nf">AddHandlers</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">cqrs</span><span class="p">.</span><span class="nf">NewEventHandler</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;UpdateViews&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="o">*</span><span class="nx">PostViewed</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">UpdatePost</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">PostID</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">post</span> <span class="o">*</span><span class="nx">Post</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">post</span><span class="p">.</span><span class="nx">Views</span><span class="o">++</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">cqrs</span><span class="p">.</span><span class="nf">NewEventHandler</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;UpdateReactions&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="o">*</span><span class="nx">PostReactionAdded</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">UpdatePost</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">PostID</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">post</span> <span class="o">*</span><span class="nx">Post</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">post</span><span class="p">.</span><span class="nx">Reactions</span><span class="p">[</span><span class="nx">event</span><span class="p">.</span><span class="nx">ReactionID</span><span class="p">]</span><span class="o">++</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L97" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L97" target="_blank">Full source</a> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If you&rsquo;re curious about the <code>UpdatePost</code> method, check <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F" target="_blank">Robert&rsquo;s article on the repository pattern</a>. </p></div> </div> <div class="notice tip"> <div class="notice-head"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillrule="evenodd" cliprule="evenodd" d="M12 0c6.6274.0 12 5.37258 12 12 0 6.6274-5.3726 12-12 12C5.37258 24 0 18.6274.0 12 0 5.37258 5.37258.0 12 0zm0 2.4C6.69807 2.4 2.4 6.69807 2.4 12c0 5.3019 4.29807 9.6 9.6 9.6 5.3019.0 9.6-4.2981 9.6-9.6.0-5.30193-4.2981-9.6-9.6-9.6zm3.9515 5.15147L9.6 13.9029 8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515c-.46863.4686-.46863 1.2284.0 1.697l2.4 2.4C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485l7.2-7.19997C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147c-.468599999999999-.46863-1.2284-.46863-1.697.0z" fill="currentcolor"></path> </svg> <p>Tip</p></div> <div class="notice-body"> <p> <h3 id="be-careful-when-refactoring">Be careful when refactoring</h3> <p>The names used for creating the topics and subscriptions are essential. If they were accidentally changed, you could lose messages.</p> <p>You should not change the event or handler names once they&rsquo;re running in production. If you need to, consider creating a new event or handler. For example, <code>PostViewedV2</code>.</p> </p> </div> </div> <p>The last part is running the router, just like you would run an HTTP server.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">router</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}()</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fmain.go%23L44" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fmain.go%23L44" target="_blank">Full source</a> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> To learn more about message routing, see the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Fconsumer-groups" target="_blank">Consumer Groups Example</a>. </p></div> </div> <h3 id="publishing-poststatsupdated">Publishing PostStatsUpdated</h3> <p>We&rsquo;ll use one more event to trigger the SSE updates: <code>PostStatsUpdated</code>. It includes the post&rsquo;s ID and a record of what has been updated (views or the reaction ID).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">PostStatsUpdated</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">PostID</span> <span class="kt">int</span> <span class="s">`json:&#34;post_id&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">ViewsUpdated</span> <span class="kt">bool</span> <span class="s">`json:&#34;views_updated&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">ReactionUpdated</span> <span class="o">*</span><span class="kt">string</span> <span class="s">`json:&#34;reaction_updated&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L26" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L26" target="_blank">Full source</a> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>Since the release of this post, I realized the naive approach of getting the post from the database on each update doesn&rsquo;t scale well (hundreds of <code>SELECT</code> queries on each update, depending on how many visitors we have). I&rsquo;ve updated the example so the <code>PostStatsUpdated</code> event includes all stats. This way, the SSE endpoint doesn&rsquo;t need to query the database at all, except for the initial call. You can see how big was the impact on the database load.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1228" height="646" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2Fimages%2Fcpu_hu2f252e2cda18585e8ba58d3dddf4827a_89125_1228x646_resize_q80_h2_lanczos_3.webp" alt="CPU load" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Flive-website-updates-go-sse-htmx%5C%2Fimages%5C%2Fcpu_hu2f252e2cda18585e8ba58d3dddf4827a_89125_1228x646_resize_lanczos_3.png"" /> <p>I left the original version in the code snippets for simplicity. You can see the changes on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fcommit%2F0ea2d2de47d9c83ef85791a17822cc058ea54de2" target="_blank">GitHub</a>.</p> </p></div> </div> <p>After updating the post, both handlers should publish the <code>PostStatsUpdated</code> event.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">err</span> <span class="p">=</span> <span class="nx">eventProcessor</span><span class="p">.</span><span class="nf">AddHandlers</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">cqrs</span><span class="p">.</span><span class="nf">NewEventHandler</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;UpdateViews&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="o">*</span><span class="nx">PostViewed</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">UpdatePost</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">PostID</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">post</span> <span class="o">*</span><span class="nx">Post</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">post</span><span class="p">.</span><span class="nx">Views</span><span class="o">++</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">statsUpdated</span> <span class="o">:=</span> <span class="nx">PostStatsUpdated</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">PostID</span><span class="p">:</span> <span class="nx">event</span><span class="p">.</span><span class="nx">PostID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">ViewsUpdated</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">eventBus</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">statsUpdated</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">cqrs</span><span class="p">.</span><span class="nf">NewEventHandler</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;UpdateReactions&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="o">*</span><span class="nx">PostReactionAdded</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">UpdatePost</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">PostID</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">post</span> <span class="o">*</span><span class="nx">Post</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">post</span><span class="p">.</span><span class="nx">Reactions</span><span class="p">[</span><span class="nx">event</span><span class="p">.</span><span class="nx">ReactionID</span><span class="p">]</span><span class="o">++</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">statsUpdated</span> <span class="o">:=</span> <span class="nx">PostStatsUpdated</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">PostID</span><span class="p">:</span> <span class="nx">event</span><span class="p">.</span><span class="nx">PostID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">ReactionUpdated</span><span class="p">:</span> <span class="o">&amp;</span><span class="nx">event</span><span class="p">.</span><span class="nx">ReactionID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">eventBus</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">statsUpdated</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L97" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L97" target="_blank">Full source</a> </div> <h2 id="sse-router">SSE Router</h2> <p>It&rsquo;s time to implement the SSE endpoints. Watermill also provides an SSE component that works well with other internals.</p> <p>The main component is called SSE Router, and the idea behind it is pretty simple. When you call its <code>AddHandler</code> method, it subscribes to the given topic in the configured subscriber. The method returns a regular HTTP handler you can use with any HTTP router you want. Whenever a message appears in the chosen topic, it will be propagated in a fan-out fashion to all running SSE endpoints within.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="852" height="639" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2Fimages%2Fsse-router_hu54e0885851217062437a9788e35cec19_56751_852x639_resize_q80_h2_lanczos_3.webp" alt="SSE Router" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Flive-website-updates-go-sse-htmx%5C%2Fimages%5C%2Fsse-router_hu54e0885851217062437a9788e35cec19_56751_852x639_resize_lanczos_3.png"" /> <p>First, let&rsquo;s create the <code>SSERouter</code> (it comes from the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-http" target="_blank">watermill-http</a> package — use the <code>v2</code> version!).</p> <p>The config requires an <code>UpstreamSubscriber</code>: you need to subscribe to a Pub/Sub that contains the events. We use Google Cloud Pub/Sub again. I&rsquo;ll explain the details of the configuration a bit later.</p> <p>We use a string marshaler as we&rsquo;re going to return plain HTML.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">subscriber</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">googlecloud</span><span class="p">.</span><span class="nf">NewSubscriber</span><span class="p">(</span><span class="nx">googlecloud</span><span class="p">.</span><span class="nx">SubscriberConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">},</span> <span class="nx">logger</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">sseRouter</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">http</span><span class="p">.</span><span class="nf">NewSSERouter</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">SSERouterConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">UpstreamSubscriber</span><span class="p">:</span> <span class="nx">subscriber</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Marshaler</span><span class="p">:</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StringSSEMarshaler</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"><span class="p">},</span> <span class="nx">logger</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L139" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L139" target="_blank">Full source</a> </div> <p>Then, you need to implement the <code>StreamAdapter</code> interface:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">StreamAdapter</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">InitialStreamResponse</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">(</span><span class="nx">response</span> <span class="kd">interface</span><span class="p">{},</span> <span class="nx">ok</span> <span class="kt">bool</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">NextStreamResponse</span><span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">,</span> <span class="nx">msg</span> <span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="p">(</span><span class="nx">response</span> <span class="kd">interface</span><span class="p">{},</span> <span class="nx">ok</span> <span class="kt">bool</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-http%2Fblob%2Fmaster%2Fpkg%2Fhttp%2Fsse.go%23L15" target="_blank">github.com/ThreeDotsLabs/watermill-http/pkg/http/sse.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-http%2Fblob%2Fmaster%2Fpkg%2Fhttp%2Fsse.go%23L15" target="_blank">Full source</a> </div> <p>These two methods are very similar. The first one is how you respond to the initial HTTP request. If needed, it lets you return an error and write it to the <code>ResponseWriter</code>. This is important because as soon as you write any data, it&rsquo;s too late to change the response code or the headers. So, <code>InitialStreamResponse</code> is where you handle things like validation or authentication. If any errors happen, return <code>ok</code> equal <code>false</code> to stop the handler. Otherwise, what you return becomes the first event sent to the client.</p> <p><code>NextStreamResponse</code> is called for each incoming <code>Message</code>. You can return a <code>response</code> to be sent to the SSE clients that use this endpoint. Or you can skip the message (again, return <code>ok</code> equal <code>false</code>).</p> <p>By default, whatever you return as the <code>response</code> is marshaled to JSON with event type <code>data</code>. You can override this with a custom marshaler, as we did here. You can also return the <code>ServerSentEvent</code> struct, which lets you explicitly specify the <code>Event</code> and <code>Data</code> fields.</p> <p>In our case, <code>InitialStreamResponse</code> simply returns the post&rsquo;s response.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">statsStream</span><span class="p">)</span> <span class="nf">InitialStreamResponse</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">(</span><span class="nx">response</span> <span class="kd">interface</span><span class="p">{},</span> <span class="nx">ok</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">postIDStr</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">PathValue</span><span class="p">(</span><span class="s">&#34;id&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">postID</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">strconv</span><span class="p">.</span><span class="nf">Atoi</span><span class="p">(</span><span class="nx">postIDStr</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusBadRequest</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">w</span><span class="p">.</span><span class="nf">Write</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;invalid post ID&#34;</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">resp</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">s</span><span class="p">.</span><span class="nf">getResponse</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">(),</span> <span class="nx">postID</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusInternalServerError</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">w</span><span class="p">.</span><span class="nf">Write</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nf">Error</span><span class="p">()))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">resp</span><span class="p">,</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fhttp.go%23L115" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fhttp.go%23L115" target="_blank">Full source</a> </div> <p><code>NextStreamResponse</code> is similar, but it also checks if the post&rsquo;s ID in the event matches the one in the URL. If not, it skips the message. It means a post has been updated, but not the one this endpoint returns.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">statsStream</span><span class="p">)</span> <span class="nf">NextStreamResponse</span><span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">,</span> <span class="nx">msg</span> <span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="p">(</span><span class="nx">response</span> <span class="kd">interface</span><span class="p">{},</span> <span class="nx">ok</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">postIDStr</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">PathValue</span><span class="p">(</span><span class="s">&#34;id&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">postID</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">strconv</span><span class="p">.</span><span class="nf">Atoi</span><span class="p">(</span><span class="nx">postIDStr</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;invalid post ID&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">event</span> <span class="nx">PostStatsUpdated</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Unmarshal</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">Payload</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">event</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;cannot unmarshal: &#34;</span> <span class="o">+</span> <span class="nx">err</span><span class="p">.</span><span class="nf">Error</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="s">&#34;&#34;</span><span class="p">,</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">event</span><span class="p">.</span><span class="nx">PostID</span> <span class="o">!=</span> <span class="nx">postID</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="s">&#34;&#34;</span><span class="p">,</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">resp</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">s</span><span class="p">.</span><span class="nf">getResponse</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">(),</span> <span class="nx">postID</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">event</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;could not get response: &#34;</span> <span class="o">+</span> <span class="nx">err</span><span class="p">.</span><span class="nf">Error</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">resp</span><span class="p">,</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fhttp.go%23L134" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fhttp.go%23L134" target="_blank">Full source</a> </div> <p>As you can see, there&rsquo;s no usual error handling here. The best we can do is log the error and return <code>false</code> to skip the message. The handler already replied with <code>200 OK</code>, so it&rsquo;s too late to change the status code. Alternatively, we could return a custom response with the error message to be displayed to the user.</p> <p>With the stream adapter ready, we can create a handler on the SSE Router:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">statsHandler</span> <span class="o">:=</span> <span class="nx">sseRouter</span><span class="p">.</span><span class="nf">AddHandler</span><span class="p">(</span><span class="s">&#34;PostStatsUpdated&#34;</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">statsStream</span><span class="p">{</span><span class="nx">repo</span><span class="p">:</span> <span class="nx">repo</span><span class="p">})</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fhttp.go%23L32" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fhttp.go%23L32" target="_blank">Full source</a> </div> <p>The first argument here is the <code>topic</code> to listen to. The returned value is a ready-to-use <code>http.HandlerFunc</code>.</p> <p>Most HTTP libraries and frameworks are compatible with <code>net/http</code>, so you can use it with whatever HTTP router you like. I use echo, so there&rsquo;s a small conversion of the path value needed:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">e</span><span class="p">.</span><span class="nf">GET</span><span class="p">(</span><span class="s">&#34;/posts/:id/stats&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">c</span> <span class="nx">echo</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">postID</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nf">Param</span><span class="p">(</span><span class="s">&#34;id&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">c</span><span class="p">.</span><span class="nf">Request</span><span class="p">().</span><span class="nf">SetPathValue</span><span class="p">(</span><span class="s">&#34;id&#34;</span><span class="p">,</span> <span class="nx">postID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nf">statsHandler</span><span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nf">Response</span><span class="p">(),</span> <span class="nx">c</span><span class="p">.</span><span class="nf">Request</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fhttp.go%23L40" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fhttp.go%23L40" target="_blank">Full source</a> </div> <p>Finally, you need to run the SSE router in a separate goroutine:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">sseRouter</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}()</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fmain.go%23L51" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fmain.go%23L51" target="_blank">Full source</a> </div> <p>And that&rsquo;s it! Whenever a message is published on the <code>PostStatsUpdated</code> topic, the SSE Router propagates it to all clients listening to the <code>GET /posts/:id/stats</code> endpoint.</p> <p>The handlers created by the SSE Router already handle all implementation details, so you don&rsquo;t need to worry about setting the headers.</p> <p>When called without an <code>Accept</code> header or with a value other than <code>text/event-stream</code>, the handlers act as regular GET handlers, returning the response from <code>InitialStreamResponse</code>. Creating an <code>EventStream</code> in JavaScript automatically passes the header for you, but keep this in mind when debugging your endpoints with a tool like <code>curl</code>!</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Regular HTTP response</span> </span></span><span class="line"><span class="cl">curl localhost:8080/posts/1/stats </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># SSE response</span> </span></span><span class="line"><span class="cl">curl -H <span class="s2">&#34;Accept: text/event-stream&#34;</span> localhost:8080/posts/1/stats </span></span></code></pre></div><h3 id="configuring-the-subscriber">Configuring the Subscriber</h3> <p>Remember the part that we want each event to be processed only by one service replica at a time? In the case of events used for SSE, you need a counterintuitive approach: all subscribers need to process each event, as the SSE endpoints will be running across all of your service instances.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1463" height="588" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flive-website-updates-go-sse-htmx%2Fimages%2Frouting-2_hua29ca1401efa2fade3c6aea16f52bdb6_108232_1463x588_resize_q80_h2_lanczos_3.webp" alt="Events Routing" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Flive-website-updates-go-sse-htmx%5C%2Fimages%5C%2Frouting-2_hua29ca1401efa2fade3c6aea16f52bdb6_108232_1463x588_resize_lanczos_3.png"" /> <p>In other words, in this case, each replica should have its own subscription that&rsquo;s not shared with anyone. For Google Cloud Pub/Sub, an easy way to do it is to generate a unique subscription name on the service&rsquo;s startup. For example, using a &ldquo;short UUID&rdquo; would generate subscription names like <code>PostStatsUpdated_lkcNowPZ99M123xPwqcxp1</code>.</p> <p>Keep in mind this can have some impact on your Pub/Sub. For example, when using Google Cloud Pub/Sub, it&rsquo;s best to set the expiration policy for such subscriptions for one day, so they&rsquo;re deleted when no longer used. There&rsquo;s a hard limit of 10,000 subscriptions; you could quickly hit it this way.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">subscriber</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">googlecloud</span><span class="p">.</span><span class="nf">NewSubscriber</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">googlecloud</span><span class="p">.</span><span class="nx">SubscriberConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ProjectID</span><span class="p">:</span> <span class="nx">cfg</span><span class="p">.</span><span class="nx">PubSubProjectID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">GenerateSubscriptionName</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">topic</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;%v_%v&#34;</span><span class="p">,</span> <span class="nx">topic</span><span class="p">,</span> <span class="nx">watermill</span><span class="p">.</span><span class="nf">NewShortUUID</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">SubscriptionConfig</span><span class="p">:</span> <span class="nx">pubsub</span><span class="p">.</span><span class="nx">SubscriptionConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ExpirationPolicy</span><span class="p">:</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Hour</span> <span class="o">*</span> <span class="mi">24</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L139" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/events.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fevents.go%23L139" target="_blank">Full source</a> </div> <p>If that sounds like a lot to consider, it&rsquo;s because there is! Running production-grade Event-Driven systems comes with many advantages, but it&rsquo;s not trivial. That&rsquo;s why I go into detail here, so you know what to expect in production, not just in toy examples. (If that sounds like something you&rsquo;d like to learn in-depth, see our <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fevent-driven%2F" target="_blank">Go Event-Driven training</a>.)</p> <h2 id="htmx">htmx</h2> <p>The last piece of the puzzle is the client-side code.</p> <p>In the example, we use htmx, a library that lets you make AJAX requests with HTML attributes. It also supports SSE with an extension. The templating in the snippets below comes from templ.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">hx-ext</span><span class="o">=</span><span class="s">&#34;sse&#34;</span> <span class="na">sse-connect</span><span class="o">=</span><span class="s">{</span> <span class="err">&#34;/</span><span class="na">posts</span><span class="err">/&#34;</span> <span class="err">+</span> <span class="na">post</span><span class="err">.</span><span class="na">ID</span> <span class="err">+</span> <span class="err">&#34;/</span><span class="na">stats</span><span class="err">&#34;</span> <span class="err">}</span> <span class="na">sse-swap</span><span class="o">=</span><span class="s">&#34;data&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fviews%2Fpages.templ%23L50" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/views/pages.templ</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fviews%2Fpages.templ%23L50" target="_blank">Full source</a> </div> <p>The <code>sse-swap</code> attribute is the event type to look for from the stream. By default, Watermill&rsquo;s SSE endpoints use <code>data</code>, so that&rsquo;s what we use. Every time an event is received from the <code>/posts/:id/stats</code> endpoint, its payload will be injected inside the div. (Remember, our events are HTML.)</p> <p>We also use htmx to send the reaction form asynchronously (a classic POST AJAX request). In this case, we use <code>hx-swap=&quot;outerHTML&quot;</code>, which replaces the entire form with the response from the server. It&rsquo;s a button with a ✅ &ldquo;check&rdquo; suggesting that the reaction has been added. The SSE will eventually update the stats. (Although there might be a slight delay. If you care about UX, returning a &ldquo;fake&rdquo; number increase could make sense here.)</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">form</span> <span class="na">hx-post</span><span class="o">=</span><span class="s">{</span> <span class="err">&#34;/</span><span class="na">posts</span><span class="err">/&#34;</span> <span class="err">+</span> <span class="na">postID</span> <span class="err">+</span> <span class="err">&#34;/</span><span class="na">reactions</span><span class="err">&#34;}</span> <span class="na">hx-swap</span><span class="o">=</span><span class="s">&#34;outerHTML&#34;</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;reaction_id&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">{</span> <span class="na">reaction</span><span class="err">.</span><span class="na">ID</span> <span class="err">}</span> <span class="p">/&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">button</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;submit&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">{&#34;btn&#34;,</span> <span class="err">&#34;</span><span class="na">btn-outline-secondary</span><span class="err">&#34;,</span> <span class="err">&#34;</span><span class="na">m-1</span><span class="err">&#34;,</span> <span class="na">templ</span><span class="err">.</span><span class="na">KV</span><span class="err">(&#34;</span><span class="na">animated</span><span class="err">&#34;,</span> <span class="na">reaction</span><span class="err">.</span><span class="na">JustChanged</span><span class="err">)}</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;emoji&#34;</span><span class="p">&gt;</span>{ reaction.Label }<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;counter&#34;</span><span class="p">&gt;</span>{ reaction.Count }<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fviews%2Fpages.templ%23L71" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/views/pages.templ</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fviews%2Fpages.templ%23L71" target="_blank">Full source</a> </div> <h3 id="animations">Animations</h3> <p>If you&rsquo;re used to working with Single Page Applications, using htmx might initially feel weird. For example, consider animating an element that has just been updated. In a classic SPA, you would get the JSON response from the SSE endpoint, compare the values with what&rsquo;s in the &ldquo;model&rdquo;, and decide whether to animate the element.</p> <p>While htmx allows for &ldquo;hooks&rdquo; after the request is done, it&rsquo;s probably not what you want. Instead, you need to adjust your mental model a bit. The server code is the source of truth in this setup. There is no &ldquo;client&rdquo; that decides how to display things.</p> <p>In this example, I use a CSS class to mark the updated element. The class includes an animation that pops up the element for a moment. The server code decides whether to add the class or not (based on the data in the incoming event). (<code>templ.KV</code> is how you add a class conditionally using templ. It will be present if <code>stats.Views.JustChanged</code> is true.)</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">{</span> <span class="err">&#34;</span><span class="na">d-flex</span><span class="err">&#34;,</span> <span class="err">&#34;</span><span class="na">align-items-center</span><span class="err">&#34;,</span> <span class="na">templ</span><span class="err">.</span><span class="na">KV</span><span class="err">(&#34;</span><span class="na">animated</span><span class="err">&#34;,</span> <span class="na">stats</span><span class="err">.</span><span class="na">Views</span><span class="err">.</span><span class="na">JustChanged</span><span class="err">)}</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;me-1&#34;</span><span class="p">&gt;</span>👁️<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"> <span class="p">&lt;</span><span class="nt">small</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;text-muted&#34;</span><span class="p">&gt;</span>{ stats.Views.Count + &#34; views&#34; }<span class="p">&lt;/</span><span class="nt">small</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fviews%2Fpages.templ%23L56" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/views/pages.templ</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fviews%2Fpages.templ%23L56" target="_blank">Full source</a> </div> <h2 id="other-things-to-consider">Other things to consider</h2> <h3 id="two-kinds-of-sse-endpoints">Two kinds of SSE endpoints</h3> <p>How you return events from SSE endpoints is totally up to you. Here are two ways that make sense in different scenarios.</p> <ol> <li>An endpoint that returns the same data model initially and on every update. With each triggered update, you kind of &ldquo;refresh&rdquo; the model, perhaps embedded on the website. This is what we use in the example above.</li> <li>An endpoint that returns nothing initially and then keeps sending unique updates as they happen. You can append each new event to some list, for example. It&rsquo;s how you would implement notifications or a web chat.</li> </ol> <h3 id="at-least-once-delivery">At-least-once delivery</h3> <p>When working with virtually any Pub/Sub, you must be aware of the &ldquo;at-least-once&rdquo; delivery guarantee. You may receive the same message twice because of network issues or your server going down at the wrong moment.</p> <p>Don&rsquo;t try to work around this. Instead, embrace that this can happen and design your handlers to be <em>idempotent</em>. It means that processing the message twice (or more) has the same effect as processing it once.</p> <p>In the example above, we don&rsquo;t guard against it. If the same message was processed twice, it would add an extra view or reaction in the database. It&rsquo;s not a big deal in this case, and we can live with it. One way to prevent it would be to store the processed message IDs in the database and check it on each update.</p> <h3 id="watch-out-for-http11">Watch out for HTTP/1.1</h3> <p>In modern browsers, there&rsquo;s a limit of 6 open connections per server over HTTP/1.1, which can be a big issue when using SSE. Your website won&rsquo;t work well if someone opens it in several tabs.</p> <p>For best results, use SSE with HTTP/2 where this limit doesn&rsquo;t apply. Most modern web servers support HTTP/2, so make sure you enable it.</p> <h2 id="local-environment-tricks">Local environment tricks</h2> <p>Here are two tips unrelated to SSE that might be useful for running your app locally.</p> <h3 id="mounting-gopkg-and-go-cache">Mounting /go/pkg and go cache</h3> <p>In the docker-compose definition, you can mount the <code>/go/pkg</code> and <code>/go-cache</code> directories to speed up the build process. This way, you don&rsquo;t have to download the dependencies whenever you rebuild the container.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">server</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># ...</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go_pkg:/go/pkg</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go_cache:/go-cache</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">volumes</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">go_pkg</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">go_cache</span><span class="p">:</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fdocker-compose.yml%23L8" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/docker-compose.yml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fdocker-compose.yml%23L8" target="_blank">Full source</a> </div> <h3 id="reflex-for-regenerating-templ-and-rebuilding-the-server">Reflex for regenerating templ and rebuilding the server</h3> <p><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fcespare%2Freflex" target="_blank">Reflex</a> is my go-to tool for live code reloading. (See <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-docker-dev-environment-with-go-modules-and-live-code-reloading%2F" target="_blank">my post on the dev environment setup</a>.) When working with templ, you can use a configuration like the one below to regenerate the templates and rebuild the server after every change.</p> <p>It&rsquo;s a great way to speed up your development process, so you don&rsquo;t need to keep switching between code, the terminal, and the browser.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-s" data-lang="s"><span class="line"><span class="cl"><span class="o">-</span><span class="n">r</span> <span class="s">&#39;(\.go$|go\.mod$)&#39;</span> <span class="o">-</span><span class="n">s</span> <span class="n">go</span> <span class="n">run</span> <span class="n">. </span></span></span><span class="line"><span class="cl"><span class="n"></span><span class="o">-</span><span class="n">r</span> <span class="s">&#39;\.templ$&#39;</span> <span class="n">templ</span> <span class="n">generate</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fdocker%2Freflex.conf" target="_blank">github.com/ThreeDotsLabs/watermill/_examples/real-world-examples/server-sent-events-htmx/docker/reflex.conf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F57df62163289e02aef999279f80301f8756a8d57%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx%2Fdocker%2Freflex.conf" target="_blank">Full source</a> </div> <h2 id="go-build-something">Go build something!</h2> <p>That should be all the theory you need to build something cool. If you have any questions, let me know in the comments.</p> <p>Once again, the complete source code is on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Fserver-sent-events-htmx" target="_blank">GitHub</a> You can run it locally with <code>docker-compose up</code>.</p> <p>Give this stack a try; I had lots of fun working with it. Good luck!</p>Making Games in Go for Absolute Beginnershttps://threedots.tech/post/making-games-in-go/Fri, 24 Nov 2023 00:00:00 +0100https://threedots.tech/post/making-games-in-go/<p>Here&rsquo;s a rant I often see in developer communities:</p> <blockquote> <p>I used to love programming because I like building stuff. But my full-time job killed my passion. I spend more time in meetings, fighting over deadlines, and arguing in reviews than working with code. Am I burned out? Is there hope, or do I need a new hobby?</p> </blockquote> <p>Sounds familiar? No wonder we keep looking forward to using a new framework or database — we&rsquo;re bored. So here&rsquo;s my tip to cure some of your burnout: <strong>Start coding for fun again and reclaim your hobby.</strong> And what is more fun than creating your own world with code?</p> <p>Wanting to make my own video game pushed me into programming 20 years ago. I didn&rsquo;t make a career out of it, even though I created some small games using different tech stacks. I keep returning to it, and for me, it&rsquo;s the most fun activity related to coding. Especially if done with friends over a weekend for a <a href="proxy.php?url=https%3A%2F%2Fldjam.com%2Fusers%2Fm110%2Fgames" target="_blank">game jam</a>.</p> <p>This post is my attempt at getting you to write games from scratch. (And if you want to pick up a new language too, check <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F%3Futm_source%3Dmaking-games" target="_blank">Go in One Evening</a>!)</p> <p>Why Go? I like it, and <a href="proxy.php?url=https%3A%2F%2Febitengine.org" target="_blank">Ebitengine</a>, the library I will show here, is great to work with. Like many Go libraries, it helps you do what you need and stays out of the way — it&rsquo;s not a framework.</p> <p>Of course, a bigger game engine simplifies many things but also comes with bloat and a specific approach. <strong>A big part of why making games is fun is you get to do things your way and don&rsquo;t need to care if it&rsquo;s the right way.</strong> And it&rsquo;s a great exercise to learn how games work.</p> <p>First, a warning. Once you understand how games work, some of the magic is gone when playing them. You will start wondering, &ldquo;How does it work?&rdquo; instead of enjoying the game. Thankfully, making games compensates for this.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Disclaimer: I&rsquo;m a hobbyist game developer. I do it as it&rsquo;s tons of fun, but I never worked on a professional game. Expect personal experience here. I might show some code that would be dumb when used in a &ldquo;real&rdquo; game (performance-wise, for example). Let me know in the comments if you find any errors! </p></div> </div> <h2 id="the-basics">The basics</h2> <p>I remember having a hard time understanding how exactly games work. How do you make the objects in the game do what they do? It&rsquo;s more obvious if you use a game engine, but what if you start from scratch?</p> <p>You probably know that a movie is a very long collection of images, displayed one after the other so fast that our brains can&rsquo;t tell the difference. Usually, 24 or 60 images (frames) per second (FPS).</p> <p>A video game is the same idea, but the images (frames) don&rsquo;t exist beforehand, so you must generate (draw) them on the fly. And you can press some buttons to change how the next frame looks.</p> <p>This may seem too simplistic, right? &ldquo;What? But how do you make a character jump?&rdquo; You move the image up the screen over time and then back down. And you stop it from moving when it &ldquo;touches&rdquo; other images. 2D games are basically made of images drawn on top of each other.</p> <p>I was surprised to learn that games are basically infinite loops. You keep drawing images on the screen.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">DrawFrame</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Besides drawing, you need to update the logic of the game. It includes things like checking buttons pressed, updating the player&rsquo;s health, and checking collisions. The logic decides what images to draw and where, and the Draw function does it. Each time the logic is updated is known as a <em>tick</em>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// One tick </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">UpdateLogic</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nf">DrawFrame</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>I know this looks hardly exciting, but let this idea sink in: your game does these two things over and over, 60 times per second. Just a single thread with this infinite loop (for now, forget goroutines exist).</p> <p>Even though the idea is simple, keeping the loop running at the same speed on all systems is not trivial. Ebitengine takes care of this for you, running the game at 60 ticks per second.</p> <p>Logic is the part you should be familiar with: it&rsquo;s regular Go code. Drawing images is where things get more complex, and that&rsquo;s why you use Ebitengine. It helps with drawing images, playing sounds, and checking inputs. How the logic works is mainly on you.</p> <p>Ebitengine provides a <code>Game</code> interface you need to implement.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Game</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">Update</span><span class="p">()</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"> <span class="nf">Draw</span><span class="p">(</span><span class="nx">screen</span> <span class="o">*</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">Image</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">Layout</span><span class="p">(</span><span class="nx">outsideWidth</span><span class="p">,</span> <span class="nx">outsideHeight</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="nx">screenWidth</span><span class="p">,</span> <span class="nx">screenHeight</span> <span class="kt">int</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>The <code>Update</code> and <code>Draw</code> functions work as in the loop above. <code>Layout</code> is used for scaling the game screen — you don&rsquo;t need to worry about it now.</p> <p><code>Update</code> is called every tick and updates the game logic. If an error is returned, the game quits.</p> <p><code>Draw</code> is called every frame. The <code>screen</code> argument is an image you draw on, like a canvas.</p> <h2 id="setting-up-the-project">Setting up the project</h2> <p>First, let&rsquo;s kick off the boring stuff: create a module and add Ebitengine (formerly called Ebiten, hence the package name).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">go</span> <span class="nx">mod</span> <span class="nx">init</span> <span class="nx">game</span> </span></span><span class="line"><span class="cl"><span class="k">go</span> <span class="nx">get</span> <span class="nx">github</span><span class="p">.</span><span class="nx">com</span><span class="o">/</span><span class="nx">hajimehoshi</span><span class="o">/</span><span class="nx">ebiten</span><span class="o">/</span><span class="nx">v2</span> </span></span></code></pre></div><p>Here&rsquo;s the minimal code you need to start a game. It&rsquo;s just an empty implementation of the Game interface. Running it should show a blank black window.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">main</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="s">&#34;github.com/hajimehoshi/ebiten/v2&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Game</span> <span class="kd">struct</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Update</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Draw</span><span class="p">(</span><span class="nx">screen</span> <span class="o">*</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">Image</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Layout</span><span class="p">(</span><span class="nx">outsideWidth</span><span class="p">,</span> <span class="nx">outsideHeight</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="nx">screenWidth</span><span class="p">,</span> <span class="nx">screenHeight</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">outsideWidth</span><span class="p">,</span> <span class="nx">outsideHeight</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">Game</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">RunGame</span><span class="p">(</span><span class="nx">g</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> I&rsquo;m pretty liberal with error handling in these examples. We&rsquo;re just prototyping and having fun, so I&rsquo;m okay with raising a <code>panic</code>. With most errors, you can either crash the game or log it and continue. </p></div> </div> <h2 id="loading-assets">Loading assets</h2> <p>If your graphic skills are similar to mine (the term &ldquo;programmer art&rdquo; exists for a reason 🙃), you may prefer using ready assets when prototyping.</p> <p>Unless you want to create them yourself, I encourage you to play with free-to-use assets. Personally, I love the collection from <a href="proxy.php?url=https%3A%2F%2Fkenney.nl" target="_blank">Kenney</a>. Every time I look for inspiration, I browse the 2D assets there and find something that looks like a cool game to make. I&rsquo;ll use the <a href="proxy.php?url=https%3A%2F%2Fkenney.nl%2Fassets%2Fspace-shooter-extension" target="_blank">space </a><a href="proxy.php?url=https%3A%2F%2Fkenney.nl%2Fassets%2Fspace-shooter-redux" target="_blank">shooter</a> pack below to make a trivial Asteroids clone.</p> <p>To load the assets in code, use <code>embed</code> — it&rsquo;s like it was meant for making games. You know the paths are right on compile time. You also don&rsquo;t need to care about the distribution, as the binary contains the assets. It works even for mobile apps!</p> <p>You will likely have many assets, so consider embedding the entire directory:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span> <span class="s">&#34;embed&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">//go:embed assets/* </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">assets</span> <span class="nx">embed</span><span class="p">.</span><span class="nx">FS</span> </span></span></code></pre></div><p>You can now load the images from the <code>assets</code> collection. I&rsquo;m using the <code>must</code> pattern here and keep sprites as global variables for simplicity. (A sprite is just another name for a 2D graphic.)</p> <p>Note the empty <code>&quot;image/png&quot;</code> import used for decoding.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;image&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">_</span> <span class="s">&#34;image/png&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/hajimehoshi/ebiten/v2&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">PlayerSprite</span> <span class="p">=</span> <span class="nf">mustLoadImage</span><span class="p">(</span><span class="s">&#34;assets/player.png&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">mustLoadImage</span><span class="p">(</span><span class="nx">name</span> <span class="kt">string</span><span class="p">)</span> <span class="o">*</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">Image</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">f</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">assets</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="nx">f</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">img</span><span class="p">,</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">image</span><span class="p">.</span><span class="nf">Decode</span><span class="p">(</span><span class="nx">f</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">NewImageFromImage</span><span class="p">(</span><span class="nx">img</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h3 id="drawing-images">Drawing Images</h3> <p>All the drawing happens in the <code>Draw</code> method.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Draw</span><span class="p">(</span><span class="nx">screen</span> <span class="o">*</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">Image</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>The <code>screen</code> argument is the image displayed every frame (after the method returns). Your job is to draw other images (or text) on it.</p> <p>The key method of <code>ebiten.Image</code> is <code>DrawImage</code>, which draws another image onto it.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">screen</span><span class="p">.</span><span class="nf">DrawImage</span><span class="p">(</span><span class="nx">PlayerSprite</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span> </span></span></code></pre></div><p>The first argument is the image you want to draw. The second is the <code>ebiten.DrawImageOptions</code> struct that decides where and how the image is drawn. Passing <code>nil</code> keeps the default options.</p> <p>Here&rsquo;s the result: the player&rsquo;s spaceship is drawn in the screen&rsquo;s top-left corner.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1504" height="1240" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_1_hu39bee006aae0616c1ecd27a764714ef2_315735_1504x1240_fill_q80_h2_lanczos_smart1_3.webp" alt="Image 1" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_1_hu39bee006aae0616c1ecd27a764714ef2_315735_1504x1240_fill_lanczos_smart1_3.png"" /> <p>The drawing options struct has a few fields; the one to focus on first is the <code>GeoM</code> matrix. The <a href="proxy.php?url=https%3A%2F%2Febitengine.org%2Fen%2Fdocuments%2Fmatrix.html" target="_blank">docs</a> go into detail on the math behind it, and math is super helpful in making games. But if I just wanted to make a game, the last thing I&rsquo;d like to read about is algebra, so I&rsquo;ll try to simplify it here.</p> <p>Do you know how when you copy and paste an image in a graphics editor, the pasted part stays selected, and you can keep moving and transforming it? That&rsquo;s how I see <code>DrawImage</code>. You can think of <code>DrawImageOptions</code> as a &ldquo;cursor&rdquo; you can use to position and change the image.</p> <p><strong>Drawing uses X and Y coordinates, with the Y axis going downward.</strong> By default, everything is drawn at the (0, 0) point, so the top-left corner of the base image.</p> <p>To move the image, use <code>GeoM.Translate</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">op</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">DrawImageOptions</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="mi">150</span><span class="p">,</span> <span class="mi">200</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">screen</span><span class="p">.</span><span class="nf">DrawImage</span><span class="p">(</span><span class="nx">PlayerSprite</span><span class="p">,</span> <span class="nx">op</span><span class="p">)</span> </span></span></code></pre></div><p>This <code>Translate</code> call moves the image 150 pixels right and 200 pixels down. You can use negative numbers to go left and up, respectively. (If you move too far away, you may not see the image on the screen).</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1504" height="1240" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_2_hu39bee006aae0616c1ecd27a764714ef2_242812_1504x1240_fill_q80_h2_lanczos_smart1_3.webp" alt="Image.png" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_2_hu39bee006aae0616c1ecd27a764714ef2_242812_1504x1240_fill_lanczos_smart1_3.png"" /> <p>Use <code>GeoM.Rotate</code> to rotate the image:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">op</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">DrawImageOptions</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Rotate</span><span class="p">(</span><span class="mf">45.0</span> <span class="o">*</span> <span class="nx">math</span><span class="p">.</span><span class="nx">Pi</span> <span class="o">/</span> <span class="mf">180.0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">screen</span><span class="p">.</span><span class="nf">DrawImage</span><span class="p">(</span><span class="nx">PlayerSprite</span><span class="p">,</span> <span class="nx">op</span><span class="p">)</span> </span></span></code></pre></div><p>This rotates the image 45° clockwise. (The unit is radians. If you prefer working with degrees, use the <code>degrees * math.Pi / 180.0</code> formula to get them.)</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1504" height="1240" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_3_hu39bee006aae0616c1ecd27a764714ef2_234171_1504x1240_fill_q80_h2_lanczos_smart1_3.webp" alt="Image.png" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_3_hu39bee006aae0616c1ecd27a764714ef2_234171_1504x1240_fill_lanczos_smart1_3.png"" /> <p>Can you see how the image is partially off-screen now? That&rsquo;s because the pivot point (the point &ldquo;around&rdquo; which the image is being rotated) is the top-left corner of the image. I&rsquo;ll show how to work around this in a bit.</p> <p>Using negative values rotates the image counter-clockwise.</p> <p>Finally, there&rsquo;s <code>GeoM.Scale</code>, which lets you draw smaller or bigger images:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">op</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">DrawImageOptions</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Scale</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">screen</span><span class="p">.</span><span class="nf">DrawImage</span><span class="p">(</span><span class="nx">PlayerSprite</span><span class="p">,</span> <span class="nx">op</span><span class="p">)</span> </span></span></code></pre></div><p>This <code>Scale</code> call makes the image twice as big horizontally and vertically.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1504" height="1240" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_4_hua1aa2cfb7523147da1c9fc35763a7dd7_320395_1504x1240_fill_q80_h2_lanczos_smart1_3.webp" alt="Image.png" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_4_hua1aa2cfb7523147da1c9fc35763a7dd7_320395_1504x1240_fill_lanczos_smart1_3.png"" /> <p>Passing negative values flips the image horizontally or vertically. For example, to draw the image upside-down, use:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Scale</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> </span></span></code></pre></div><p><strong>You now know the basic tools to control images in a game: their position, rotation, and scale.</strong></p> <p>You can combine all of them in a single <code>DrawImage</code> call. There&rsquo;s a caveat, though: the order matters. If you use <code>Translate</code> first and then apply the rotation or scale, you may not see what you expect because the change will be applied to the new position.</p> <p>To illustrate this, let&rsquo;s see how to rotate the image around its center (a common use case).</p> <p>You need to move the pivot point to the image&rsquo;s center. You can find it simply by dividing the image&rsquo;s width and height by half. (Calculating stuff based on the image size is also super common.)</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">width</span> <span class="o">:=</span> <span class="nx">PlayerSprite</span><span class="p">.</span><span class="nf">Bounds</span><span class="p">().</span><span class="nf">Dx</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="nx">height</span> <span class="o">:=</span> <span class="nx">PlayerSprite</span><span class="p">.</span><span class="nf">Bounds</span><span class="p">().</span><span class="nf">Dy</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">halfW</span> <span class="o">:=</span> <span class="nb">float64</span><span class="p">(</span><span class="nx">width</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">halfH</span> <span class="o">:=</span> <span class="nb">float64</span><span class="p">(</span><span class="nx">height</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span> </span></span></code></pre></div><p>You need to move the image by <strong>the negative values</strong> first. This is so the image&rsquo;s center aligns with the origin point (0, 0). Then, apply the rotation and move the image &ldquo;back&rdquo; by the same amount.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="o">-</span><span class="nx">halfW</span><span class="p">,</span> <span class="o">-</span><span class="nx">halfH</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Rotate</span><span class="p">(</span><span class="mf">45.0</span> <span class="o">*</span> <span class="nx">math</span><span class="p">.</span><span class="nx">Pi</span> <span class="o">/</span> <span class="mf">180.0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="nx">halfW</span><span class="p">,</span> <span class="nx">halfH</span><span class="p">)</span> </span></span></code></pre></div> <img title="" loading="lazy" decoding="async" class="img img-center" width="1504" height="1240" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_5_hu39bee006aae0616c1ecd27a764714ef2_317582_1504x1240_fill_q80_h2_lanczos_smart1_3.webp" alt="Image.png" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_5_hu39bee006aae0616c1ecd27a764714ef2_317582_1504x1240_fill_lanczos_smart1_3.png"" /> <p>It might be easier to grasp using this animation of all steps (scaled three times):</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_6.gif" alt="AnimatedImage.gif" onerror="this.onerror='null';this.src=''" /> <h2 id="changing-colors">Changing colors</h2> <p>Another useful option is changing the sprite&rsquo;s color. You can use it to draw an image that looks like the sprite but with a solid color. Of course, this makes the most sense if you use sprites with a transparent background.</p> <p>The syntax is similar, although you need to use the recently added <code>colorm</code> package.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">op</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">colorm</span><span class="p">.</span><span class="nx">DrawImageOptions</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="nx">cm</span> <span class="o">:=</span> <span class="nx">colorm</span><span class="p">.</span><span class="nx">ColorM</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="nx">cm</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">colorm</span><span class="p">.</span><span class="nf">DrawImage</span><span class="p">(</span><span class="nx">screen</span><span class="p">,</span> <span class="nx">PlayerSprite</span><span class="p">,</span> <span class="nx">cm</span><span class="p">,</span> <span class="nx">op</span><span class="p">)</span> </span></span></code></pre></div> <img title="" loading="lazy" decoding="async" class="img img-center" width="1504" height="1240" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_7_hua1aa2cfb7523147da1c9fc35763a7dd7_314456_1504x1240_fill_q80_h2_lanczos_smart1_3.webp" alt="Image.png" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_7_hua1aa2cfb7523147da1c9fc35763a7dd7_314456_1504x1240_fill_lanczos_smart1_3.png"" /> <p>The first three arguments of <code>Translate</code> are <code>red</code>, <code>green</code>, and <code>blue</code> values and go from <code>0.0</code> to <code>1.0</code> (0% to 100%). The last one is <code>alpha</code> (transparency).</p> <p>You can control the <code>alpha</code> field to make sprites transparent, creating cool effects like adding a shadow. To keep the transparent background unchanged, use <code>Scale</code> instead of <code>Translate</code>. Keep the first three arguments at <code>1.0</code>, so they don&rsquo;t change.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">op</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">colorm</span><span class="p">.</span><span class="nx">DrawImageOptions</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="nx">cm</span> <span class="o">:=</span> <span class="nx">colorm</span><span class="p">.</span><span class="nx">ColorM</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="nx">cm</span><span class="p">.</span><span class="nf">Scale</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.5</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">colorm</span><span class="p">.</span><span class="nf">DrawImage</span><span class="p">(</span><span class="nx">screen</span><span class="p">,</span> <span class="nx">PlayerSprite</span><span class="p">,</span> <span class="nx">cm</span><span class="p">,</span> <span class="nx">op</span><span class="p">)</span> </span></span></code></pre></div> <img title="" loading="lazy" decoding="async" class="img img-center" width="1504" height="1240" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_8_hua1aa2cfb7523147da1c9fc35763a7dd7_321867_1504x1240_fill_q80_h2_lanczos_smart1_3.webp" alt="Image.png" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_8_hua1aa2cfb7523147da1c9fc35763a7dd7_321867_1504x1240_fill_lanczos_smart1_3.png"" /> <h2 id="combining-options">Combining options</h2> <p>You can do more with <code>DrawImageOptions</code>, but this should be all you need to start making games.</p> <p>For example, here&rsquo;s an evolution animation I created in <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fm110%2Fairplanes" target="_blank">airplanes</a>:</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_9.gif" alt="AnimatedImage.gif" onerror="this.onerror='null';this.src=''" /> <p>I really like how it turned out. The best part is that I didn&rsquo;t use graphic assets for the animation. It&rsquo;s all done with <code>DrawImageOptions</code> and goes like this:</p> <ul> <li>Change the color of the sprite to white.</li> <li>Create a new sprite (also white) on top of the current one, with the scale set to (0, 0) (invisible).</li> <li>Scale the new sprite to scale (1, 1) over time.</li> <li>Scale the old sprite to scale (0, 0) over time.</li> <li>Remove the white color.</li> </ul> <p>Can you notice how the shadow evolves as well? It&rsquo;s the same idea but using a gray sprite and some transparency.</p> <div class="text-center"> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F%3Futm_source%3Dblog-content"> <img src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fsidebar%2Fcourse.svg" loading="lazy" decoding="async" alt="Go In One Evening" class=" img" width="500" height="500" /> </a> <span class="h5 inline-block"> Are you experienced engineer who wants to learn Go basics?<br> You don't become an engineer by watching videos. <br> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F%3Futm_source%3Dblog-content"> Learn Go hands-on by building real projects. </a> </span> </div> <h2 id="the-logic">The logic</h2> <p>To make the game do anything interesting, you need to fill in the <code>Update</code> method. You keep all logic there while the <code>Draw</code> method draws images (on the screen or on top of each other).</p> <p>The basic idea of the game logic is simple: the <code>Game</code> struct holds some state, and the <code>Update</code> method changes this state. <code>Draw</code> reads the state and draws images based on it.</p> <p>A classic example of a game state is the player&rsquo;s position. In 2D games, this will usually be <strong>a pair of (X, Y) values, also known as a vector.</strong></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Vector</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">X</span> <span class="kt">float64</span> </span></span><span class="line"><span class="cl"> <span class="nx">Y</span> <span class="kt">float64</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Game</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">playerPosition</span> <span class="nx">Vector</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>The zero-value of <code>Vector</code> is (0, 0), like any struct in Go. Let&rsquo;s initialize the game state with a predefined value:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">g</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">Game</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">playerPosition</span><span class="p">:</span> <span class="nx">Vector</span><span class="p">{</span><span class="nx">X</span><span class="p">:</span> <span class="mi">100</span><span class="p">,</span> <span class="nx">Y</span><span class="p">:</span> <span class="mi">100</span><span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>All left to do is to update the <code>Draw</code> method so it draws the player&rsquo;s sprite at the given position. This shouldn&rsquo;t be surprising:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">op</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">DrawImageOptions</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="nx">g</span><span class="p">.</span><span class="nx">playerPosition</span><span class="p">.</span><span class="nx">X</span><span class="p">,</span> <span class="nx">g</span><span class="p">.</span><span class="nx">playerPosition</span><span class="p">.</span><span class="nx">Y</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">screen</span><span class="p">.</span><span class="nf">DrawImage</span><span class="p">(</span><span class="nx">PlayerSprite</span><span class="p">,</span> <span class="nx">op</span><span class="p">)</span> </span></span></code></pre></div><p>Note how we call <code>Translate</code> <strong>always with the current position of the player</strong>. Each time <code>Draw</code> is called, the screen is empty, and the images are drawn from scratch. (Ebitengine might optimize this, but we don&rsquo;t need to care now.)</p> <h2 id="movement">Movement</h2> <p>To move the player to the right, keep increasing the <code>X</code> position in <code>Update</code>.</p> <p>Do you remember that <code>Update</code> is called in an infinite loop? This becomes important now, as you need to decide how fast to move the player.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Update</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">speed</span> <span class="o">:=</span> <span class="mf">5.0</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">playerPosition</span><span class="p">.</span><span class="nx">X</span> <span class="o">+=</span> <span class="nx">speed</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><code>speed</code> is the number of pixels the position changes <strong>in a single tick (one <code>Update</code> call)</strong>. With the default 60 ticks per second, the image would move 300 pixels per second to the right.</p> <p>It may be easier to keep the speed as &ldquo;pixels per second&rdquo; instead of &ldquo;pixels per tick&rdquo;. In this case, calculate it like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Move 300 pixels per second </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">speed</span> <span class="o">:=</span> <span class="nb">float64</span><span class="p">(</span><span class="mi">300</span> <span class="o">/</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">TPS</span><span class="p">())</span> </span></span></code></pre></div><h2 id="controls">Controls</h2> <p>Let&rsquo;s now allow moving the player using the arrow keys. The idea stays the same: we need to update the position. Not every tick, though, but only if a key is pressed.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Update</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">speed</span> <span class="o">:=</span> <span class="mf">5.0</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">IsKeyPressed</span><span class="p">(</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">KeyDown</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">playerPosition</span><span class="p">.</span><span class="nx">Y</span> <span class="o">+=</span> <span class="nx">speed</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">IsKeyPressed</span><span class="p">(</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">KeyUp</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">playerPosition</span><span class="p">.</span><span class="nx">Y</span> <span class="o">-=</span> <span class="nx">speed</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">IsKeyPressed</span><span class="p">(</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">KeyLeft</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">playerPosition</span><span class="p">.</span><span class="nx">X</span> <span class="o">-=</span> <span class="nx">speed</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">IsKeyPressed</span><span class="p">(</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">KeyRight</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">playerPosition</span><span class="p">.</span><span class="nx">X</span> <span class="o">+=</span> <span class="nx">speed</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><code>ebiten.IsKeyPressed</code> returns <code>true</code> if the given key is pressed in the current &ldquo;tick&rdquo; (remember, <code>Update</code> is called about 60 times per second by default).</p> <p>The other function you might need is <code>ebitenutil.IsKeyJustPressed</code>, which returns true just in the tick the player presses the key. It&rsquo;s the difference between &ldquo;hold space to keep jumping&rdquo; and &ldquo;even if you keep holding space, you jump just once&rdquo;. Note it&rsquo;s in the <code>ebitenutil</code> package not <code>ebiten</code>, in contrast to <code>IsKeyPressed</code>.</p> <p>Note how we don&rsquo;t use <code>if-else</code> — thanks to this, the player can move diagonally by holding two arrows at a time.</p> <div class="notice tip"> <div class="notice-head"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillrule="evenodd" cliprule="evenodd" d="M12 0c6.6274.0 12 5.37258 12 12 0 6.6274-5.3726 12-12 12C5.37258 24 0 18.6274.0 12 0 5.37258 5.37258.0 12 0zm0 2.4C6.69807 2.4 2.4 6.69807 2.4 12c0 5.3019 4.29807 9.6 9.6 9.6 5.3019.0 9.6-4.2981 9.6-9.6.0-5.30193-4.2981-9.6-9.6-9.6zm3.9515 5.15147L9.6 13.9029 8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515c-.46863.4686-.46863 1.2284.0 1.697l2.4 2.4C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485l7.2-7.19997C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147c-.468599999999999-.46863-1.2284-.46863-1.697.0z" fill="currentcolor"></path> </svg> <p>Tip</p></div> <div class="notice-body"> <p> <p>Moving diagonally this way is naive and faster than moving in one direction at a time. If that&rsquo;s not what you want, you need to adjust for it.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">delta</span> <span class="nx">Vector</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">IsKeyPressed</span><span class="p">(</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">KeyDown</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">delta</span><span class="p">.</span><span class="nx">Y</span> <span class="p">=</span> <span class="o">-</span><span class="nx">speed</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">IsKeyPressed</span><span class="p">(</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">KeyUp</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">delta</span><span class="p">.</span><span class="nx">Y</span> <span class="p">=</span> <span class="nx">speed</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">IsKeyPressed</span><span class="p">(</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">KeyLeft</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">delta</span><span class="p">.</span><span class="nx">X</span> <span class="p">=</span> <span class="o">-</span><span class="nx">speed</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">IsKeyPressed</span><span class="p">(</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">KeyRight</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">delta</span><span class="p">.</span><span class="nx">X</span> <span class="p">=</span> <span class="nx">speed</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// Check for diagonal movement </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="nx">delta</span><span class="p">.</span><span class="nx">X</span> <span class="o">!=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="nx">delta</span><span class="p">.</span><span class="nx">Y</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">factor</span> <span class="o">:=</span> <span class="nx">speed</span> <span class="o">/</span> <span class="nx">math</span><span class="p">.</span><span class="nf">Sqrt</span><span class="p">(</span><span class="nx">delta</span><span class="p">.</span><span class="nx">X</span><span class="o">*</span><span class="nx">delta</span><span class="p">.</span><span class="nx">X</span><span class="o">+</span><span class="nx">delta</span><span class="p">.</span><span class="nx">Y</span><span class="o">*</span><span class="nx">delta</span><span class="p">.</span><span class="nx">Y</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">delta</span><span class="p">.</span><span class="nx">X</span> <span class="o">*=</span> <span class="nx">factor</span> </span></span><span class="line"><span class="cl"> <span class="nx">delta</span><span class="p">.</span><span class="nx">Y</span> <span class="o">*=</span> <span class="nx">factor</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">g</span><span class="p">.</span><span class="nx">playerPosition</span><span class="p">.</span><span class="nx">X</span> <span class="o">+=</span> <span class="nx">delta</span><span class="p">.</span><span class="nx">X</span> </span></span><span class="line"><span class="cl"><span class="nx">g</span><span class="p">.</span><span class="nx">playerPosition</span><span class="p">.</span><span class="nx">Y</span> <span class="o">+=</span> <span class="nx">delta</span><span class="p">.</span><span class="nx">Y</span> </span></span></code></pre></div> </p> </div> </div> <h2 id="timers">Timers</h2> <p>A very common need in games is changing the logic with time. For example, you want the enemy object to move for two seconds to the right and then back to the left. Or you want the boss to cast a spell every five seconds.</p> <p>You can&rsquo;t use a regular way to measure time, using <code>time.Now()</code> and <code>time.Since()</code> due to how the <code>Update</code> method works (running at a constant speed of 60 ticks per second).</p> <p>The idea is to keep counting ticks that have happened since the timer started. Then, you take some action and reset it.</p> <p>I like to keep the Timer as a separate struct with a nice API.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Timer</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">currentTicks</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="nx">targetTicks</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewTimer</span><span class="p">(</span><span class="nx">d</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Duration</span><span class="p">)</span> <span class="o">*</span><span class="nx">Timer</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">Timer</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">currentTicks</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">targetTicks</span><span class="p">:</span> <span class="nb">int</span><span class="p">(</span><span class="nx">d</span><span class="p">.</span><span class="nf">Milliseconds</span><span class="p">())</span> <span class="o">*</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">TPS</span><span class="p">()</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">Timer</span><span class="p">)</span> <span class="nf">Update</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">t</span><span class="p">.</span><span class="nx">currentTicks</span> <span class="p">&lt;</span> <span class="nx">t</span><span class="p">.</span><span class="nx">targetTicks</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nx">currentTicks</span><span class="o">++</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">Timer</span><span class="p">)</span> <span class="nf">IsReady</span><span class="p">()</span> <span class="kt">bool</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">t</span><span class="p">.</span><span class="nx">currentTicks</span> <span class="o">&gt;=</span> <span class="nx">t</span><span class="p">.</span><span class="nx">targetTicks</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">Timer</span><span class="p">)</span> <span class="nf">Reset</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nx">currentTicks</span> <span class="p">=</span> <span class="mi">0</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>And here&rsquo;s a short example of using it:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">g</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">Game</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">attackTimer</span><span class="p">:</span> <span class="nf">NewTimer</span><span class="p">(</span><span class="mi">5</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">),</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Update</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">attackTimer</span><span class="p">.</span><span class="nf">Update</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">g</span><span class="p">.</span><span class="nx">attackTimer</span><span class="p">.</span><span class="nf">IsReady</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">attackTimer</span><span class="p">.</span><span class="nf">Reset</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Execute the attack! </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h2 id="game-objects">Game Objects</h2> <p>Keeping the player&rsquo;s position directly in the <code>Game</code> struct is like using global variables. It would be challenging to maintain non-trivial games this way.</p> <p>Encapsulation is a good improvement: having building blocks you can use instead of dealing with the details. A simple game object (or entity or whatever you want to call it) can be a struct with a position and sprite.</p> <p>There are many approaches to organizing the game logic, and you can use whatever makes sense to you. An easy way to start is to have each game object expose its own <code>Update</code> and <code>Draw</code> methods, and call them in the <code>Game</code>’s <code>Update</code> and <code>Draw</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Player</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">position</span> <span class="nx">Vector</span> </span></span><span class="line"><span class="cl"> <span class="nx">sprite</span> <span class="o">*</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">Image</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewPlayer</span><span class="p">()</span> <span class="o">*</span><span class="nx">Player</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">Player</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">position</span><span class="p">:</span> <span class="nx">Vector</span><span class="p">{</span><span class="nx">X</span><span class="p">:</span> <span class="mi">100</span><span class="p">,</span> <span class="nx">Y</span><span class="p">:</span> <span class="mi">100</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">sprite</span><span class="p">:</span> <span class="nx">PlayerSprite</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="o">*</span><span class="nx">Player</span><span class="p">)</span> <span class="nf">Update</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="o">*</span><span class="nx">Player</span><span class="p">)</span> <span class="nf">Draw</span><span class="p">(</span><span class="nx">screen</span> <span class="o">*</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">Image</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">op</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">DrawImageOptions</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">X</span><span class="p">,</span> <span class="nx">p</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">Y</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">screen</span><span class="p">.</span><span class="nf">DrawImage</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">sprite</span><span class="p">,</span> <span class="nx">op</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Now the <code>Game</code> methods don&rsquo;t concern about the details of the <code>Player</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Game</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">player</span> <span class="o">*</span><span class="nx">Player</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Update</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">player</span><span class="p">.</span><span class="nf">Update</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Draw</span><span class="p">(</span><span class="nx">screen</span> <span class="o">*</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">Image</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">player</span><span class="p">.</span><span class="nf">Draw</span><span class="p">(</span><span class="nx">screen</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Let&rsquo;s finish the Player implementation for this Asteroids clone.</p> <p>The player should be spawned in the center of the screen. It&rsquo;s easy to calculate if we know the screen&rsquo;s width and height. First, define the screen size as constants.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">const</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ScreenWidth</span> <span class="p">=</span> <span class="mi">800</span> </span></span><span class="line"><span class="cl"> <span class="nx">ScreenHeight</span> <span class="p">=</span> <span class="mi">600</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><p>Now is the time we use the third method of <code>ebiten.Game</code>&rsquo;s interface: the <code>Layout</code>. It should return the size of the game window.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Layout</span><span class="p">(</span><span class="nx">outsideWidth</span><span class="p">,</span> <span class="nx">outsideHeight</span> <span class="kt">int</span><span class="p">)</span> <span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ScreenWidth</span><span class="p">,</span> <span class="nx">ScreenHeight</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Now, back to the <code>NewPlayer</code> constructor to set the initial position. The center of the screen is half of the screen&rsquo;s width and height (X and Y). But remember, the player&rsquo;s sprite top-left corner will be drawn at the given position. To keep it precisely in the center, we must move it left and up by half the sprite&rsquo;s width and height.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewPlayer</span><span class="p">()</span> <span class="o">*</span><span class="nx">Player</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">sprite</span> <span class="o">:=</span> <span class="nx">PlayerSprite</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">bounds</span> <span class="o">:=</span> <span class="nx">sprite</span><span class="p">.</span><span class="nf">Bounds</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nx">halfW</span> <span class="o">:=</span> <span class="nb">float64</span><span class="p">(</span><span class="nx">bounds</span><span class="p">.</span><span class="nf">Dx</span><span class="p">())</span> <span class="o">/</span> <span class="mi">2</span> </span></span><span class="line"><span class="cl"> <span class="nx">halfH</span> <span class="o">:=</span> <span class="nb">float64</span><span class="p">(</span><span class="nx">bounds</span><span class="p">.</span><span class="nf">Dy</span><span class="p">())</span> <span class="o">/</span> <span class="mi">2</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">pos</span> <span class="o">:=</span> <span class="nx">Vector</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">X</span><span class="p">:</span> <span class="nx">ScreenWidth</span><span class="o">/</span><span class="mi">2</span> <span class="o">-</span> <span class="nx">halfW</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Y</span><span class="p">:</span> <span class="nx">ScreenHeight</span><span class="o">/</span><span class="mi">2</span> <span class="o">-</span> <span class="nx">halfH</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">Player</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">position</span><span class="p">:</span> <span class="nx">pos</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">sprite</span><span class="p">:</span> <span class="nx">sprite</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div> <img title="" loading="lazy" decoding="async" class="img img-center" width="1504" height="1240" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_10_hua1aa2cfb7523147da1c9fc35763a7dd7_325139_1504x1240_fill_q80_h2_lanczos_smart1_3.webp" alt="Image.png" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_10_hua1aa2cfb7523147da1c9fc35763a7dd7_325139_1504x1240_fill_lanczos_smart1_3.png"" /> <p>Instead of moving the spaceship, the player should be able to rotate it. We can keep the rotation as <code>float64</code> in the <code>Player</code> struct.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="o">*</span><span class="nx">Player</span><span class="p">)</span> <span class="nf">Update</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">speed</span> <span class="o">:=</span> <span class="nx">math</span><span class="p">.</span><span class="nx">Pi</span> <span class="o">/</span> <span class="nb">float64</span><span class="p">(</span><span class="nx">ebiten</span><span class="p">.</span><span class="nf">TPS</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">IsKeyPressed</span><span class="p">(</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">KeyLeft</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">rotation</span> <span class="o">-=</span> <span class="nx">speed</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">IsKeyPressed</span><span class="p">(</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">KeyRight</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">rotation</span> <span class="o">+=</span> <span class="nx">speed</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Remember, the rotation unit is radians. <code>2π</code> is a full rotation. I&rsquo;ll stick to <code>π / TPS</code>, which means the player can rotate 180° per second.</p> <p>Now, we need to update the <code>Draw</code> struct to take rotation into account (using the trick of rotating the image around its center).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="o">*</span><span class="nx">Player</span><span class="p">)</span> <span class="nf">Draw</span><span class="p">(</span><span class="nx">screen</span> <span class="o">*</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">Image</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">bounds</span> <span class="o">:=</span> <span class="nx">p</span><span class="p">.</span><span class="nx">sprite</span><span class="p">.</span><span class="nf">Bounds</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nx">halfW</span> <span class="o">:=</span> <span class="nb">float64</span><span class="p">(</span><span class="nx">bounds</span><span class="p">.</span><span class="nf">Dx</span><span class="p">())</span> <span class="o">/</span> <span class="mi">2</span> </span></span><span class="line"><span class="cl"> <span class="nx">halfH</span> <span class="o">:=</span> <span class="nb">float64</span><span class="p">(</span><span class="nx">bounds</span><span class="p">.</span><span class="nf">Dy</span><span class="p">())</span> <span class="o">/</span> <span class="mi">2</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">op</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">DrawImageOptions</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="o">-</span><span class="nx">halfW</span><span class="p">,</span> <span class="o">-</span><span class="nx">halfH</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Rotate</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">rotation</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="nx">halfW</span><span class="p">,</span> <span class="nx">halfH</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">X</span><span class="p">,</span> <span class="nx">p</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">Y</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">screen</span><span class="p">.</span><span class="nf">DrawImage</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">sprite</span><span class="p">,</span> <span class="nx">op</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h2 id="spawning-objects">Spawning objects</h2> <p>Following the game object idea, let&rsquo;s create a Meteor object. We can load many assets and pick one randomly to make it more interesting.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">MeteorSprites</span> <span class="p">=</span> <span class="nf">mustLoadImages</span><span class="p">(</span><span class="s">&#34;meteors/*.png&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Meteor</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">position</span> <span class="nx">Vector</span> </span></span><span class="line"><span class="cl"> <span class="nx">sprite</span> <span class="o">*</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">Image</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewMeteor</span><span class="p">()</span> <span class="o">*</span><span class="nx">Meteor</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">sprite</span> <span class="o">:=</span> <span class="nx">MeteorSprites</span><span class="p">[</span><span class="nx">rand</span><span class="p">.</span><span class="nf">Intn</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nx">MeteorSprites</span><span class="p">))]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">Meteor</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">position</span><span class="p">:</span> <span class="nx">Vector</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">sprite</span><span class="p">:</span> <span class="nx">sprite</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>The <code>Draw</code> method is pretty much the same as for <code>Player</code>. (As you can imagine, there&rsquo;s room for improvement here to keep the common code in one place — more on this later).</p> <p>Now, <code>Game</code> needs to keep track of meteors. It&rsquo;s the same idea as with <code>Player</code>, except we now need a slice of objects. Let&rsquo;s also add a spawn timer.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Game</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">player</span> <span class="o">*</span><span class="nx">Player</span> </span></span><span class="line"><span class="cl"> <span class="nx">meteorSpawnTimer</span> <span class="o">*</span><span class="nx">Timer</span> </span></span><span class="line"><span class="cl"> <span class="nx">meteors</span> <span class="p">[]</span><span class="o">*</span><span class="nx">Meteor</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><code>Update</code> and <code>Draw</code> iterate over all meteors and call their respective methods. And every time the timer is ready, a new meteor is added to the slice.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Update</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">player</span><span class="p">.</span><span class="nf">Update</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">meteorSpawnTimer</span><span class="p">.</span><span class="nf">Update</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">g</span><span class="p">.</span><span class="nx">meteorSpawnTimer</span><span class="p">.</span><span class="nf">IsReady</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">meteorSpawnTimer</span><span class="p">.</span><span class="nf">Reset</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">m</span> <span class="o">:=</span> <span class="nf">NewMeteor</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">meteors</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">g</span><span class="p">.</span><span class="nx">meteors</span><span class="p">,</span> <span class="nx">m</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">m</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">g</span><span class="p">.</span><span class="nx">meteors</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">m</span><span class="p">.</span><span class="nf">Update</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Draw</span><span class="p">(</span><span class="nx">screen</span> <span class="o">*</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">Image</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">player</span><span class="p">.</span><span class="nf">Draw</span><span class="p">(</span><span class="nx">screen</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">m</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">g</span><span class="p">.</span><span class="nx">meteors</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">m</span><span class="p">.</span><span class="nf">Draw</span><span class="p">(</span><span class="nx">screen</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="notice tip"> <div class="notice-head"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillrule="evenodd" cliprule="evenodd" d="M12 0c6.6274.0 12 5.37258 12 12 0 6.6274-5.3726 12-12 12C5.37258 24 0 18.6274.0 12 0 5.37258 5.37258.0 12 0zm0 2.4C6.69807 2.4 2.4 6.69807 2.4 12c0 5.3019 4.29807 9.6 9.6 9.6 5.3019.0 9.6-4.2981 9.6-9.6.0-5.30193-4.2981-9.6-9.6-9.6zm3.9515 5.15147L9.6 13.9029 8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515c-.46863.4686-.46863 1.2284.0 1.697l2.4 2.4C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485l7.2-7.19997C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147c-.468599999999999-.46863-1.2284-.46863-1.697.0z" fill="currentcolor"></path> </svg> <p>Tip</p></div> <div class="notice-body"> <p> Note that the order of drawing matters. The entities you draw later in <code>Draw</code> will appear on top of the ones drawn earlier (if their positions overlap). </p> </div> </div> <h3 id="details-of-the-meteor">Details of the meteor</h3> <p>The meteor should spawn at the edge of the screen. Here&rsquo;s one approach to do it.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1698" height="1400" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_15_hu975c30c28bda145169b62c69c0536611_376757_1698x1400_fill_q80_h2_lanczos_smart1_3.webp" alt="Image.png" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_15_hu975c30c28bda145169b62c69c0536611_376757_1698x1400_fill_lanczos_smart1_3.png"" /> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Figure out the target position — the screen center, in this case </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">target</span> <span class="o">:=</span> <span class="nx">Vector</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">X</span><span class="p">:</span> <span class="nx">ScreenWidth</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Y</span><span class="p">:</span> <span class="nx">SreenHeight</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// The distance from the center the meteor should spawn at — half the width </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">r</span> <span class="o">:=</span> <span class="nx">ScreenWidth</span> <span class="o">/</span> <span class="mf">2.0</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// Pick a random angle — 2π is 360° — so this returns 0° to 360° </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">angle</span> <span class="o">:=</span> <span class="nx">rand</span><span class="p">.</span><span class="nf">Float64</span><span class="p">()</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">*</span> <span class="nx">math</span><span class="p">.</span><span class="nx">Pi</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// Figure out the spawn position by moving r pixels from the target at the chosen angle </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">pos</span> <span class="o">:=</span> <span class="nx">Vector</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">X</span><span class="p">:</span> <span class="nx">target</span><span class="p">.</span><span class="nx">X</span> <span class="o">+</span> <span class="nx">math</span><span class="p">.</span><span class="nf">Cos</span><span class="p">(</span><span class="nx">angle</span><span class="p">)</span><span class="o">*</span><span class="nx">r</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Y</span><span class="p">:</span> <span class="nx">target</span><span class="p">.</span><span class="nx">Y</span> <span class="o">+</span> <span class="nx">math</span><span class="p">.</span><span class="nf">Sin</span><span class="p">(</span><span class="nx">angle</span><span class="p">)</span><span class="o">*</span><span class="nx">r</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>As I mentioned, math is often handy for making games! You don&rsquo;t need to know how it works in depth; just know the basic operations like &ldquo;move 100 pixels from position (100, 200) at 30° angle&rdquo;. (There&rsquo;s absolutely no shame in having to consult the internet or AI every time you do it. 😅)</p> <p>Next, the meteor should keep moving toward the player&rsquo;s position (the screen center).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Randomized velocity </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">velocity</span> <span class="o">:=</span> <span class="mf">0.25</span> <span class="o">+</span> <span class="nx">rand</span><span class="p">.</span><span class="nf">Float64</span><span class="p">()</span><span class="o">*</span><span class="mf">1.5</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// Direction is the target minus the current position </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">direction</span> <span class="o">:=</span> <span class="nx">Vector</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">X</span><span class="p">:</span> <span class="nx">target</span><span class="p">.</span><span class="nx">X</span> <span class="o">-</span> <span class="nx">pos</span><span class="p">.</span><span class="nx">X</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Y</span><span class="p">:</span> <span class="nx">target</span><span class="p">.</span><span class="nx">Y</span> <span class="o">-</span> <span class="nx">pos</span><span class="p">.</span><span class="nx">Y</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// Normalize the vector — get just the direction without the length </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">normalizedDirection</span> <span class="o">:=</span> <span class="nx">direction</span><span class="p">.</span><span class="nf">Normalize</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// Multiply the direction by velocity </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">movement</span> <span class="o">:=</span> <span class="nx">Vector</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">X</span><span class="p">:</span> <span class="nx">normalizedDirection</span><span class="p">.</span><span class="nx">X</span> <span class="o">*</span> <span class="nx">velocity</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Y</span><span class="p">:</span> <span class="nx">normalizedDirection</span><span class="p">.</span><span class="nx">Y</span> <span class="o">*</span> <span class="nx">velocity</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><code>NewMeteor</code> can store the <code>movement</code> in the struct, and then the <code>Update</code> becomes trivial:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">Meteor</span><span class="p">)</span> <span class="nf">Update</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">m</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">X</span> <span class="o">+=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">movement</span><span class="p">.</span><span class="nx">X</span> </span></span><span class="line"><span class="cl"> <span class="nx">m</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">Y</span> <span class="o">+=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">movement</span><span class="p">.</span><span class="nx">Y</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>A final touch is adding a random rotation to each meteor. You can calculate it in the constructor:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">rotationSpeed</span> <span class="o">:=</span> <span class="o">-</span><span class="mf">0.02</span> <span class="o">+</span> <span class="nx">rand</span><span class="p">.</span><span class="nf">Float64</span><span class="p">()</span><span class="o">*</span><span class="mf">0.04</span><span class="p">,</span> </span></span></code></pre></div><p>Then, keep moving and rotating the meteor.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">Meteor</span><span class="p">)</span> <span class="nf">Update</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">m</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">X</span> <span class="o">+=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">movement</span><span class="p">.</span><span class="nx">X</span> </span></span><span class="line"><span class="cl"> <span class="nx">m</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">Y</span> <span class="o">+=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">movement</span><span class="p">.</span><span class="nx">Y</span> </span></span><span class="line"><span class="cl"> <span class="nx">m</span><span class="p">.</span><span class="nx">rotation</span> <span class="o">+=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">rotationSpeed</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div> <img title="" loading="lazy" decoding="async" class="img img-center" width="1504" height="1240" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_11_hua1aa2cfb7523147da1c9fc35763a7dd7_371998_1504x1240_fill_q80_h2_lanczos_smart1_3.webp" alt="Image.png" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_11_hua1aa2cfb7523147da1c9fc35763a7dd7_371998_1504x1240_fill_lanczos_smart1_3.png"" /> <h2 id="shooting-bullets">Shooting Bullets</h2> <p>Shooting bullets shouldn&rsquo;t be surprising by now, so I won&rsquo;t describe it in detail. You can see the complete source in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fmeteors" target="_blank">repository</a>.</p> <p>We need to:</p> <ul> <li>Keep a timer with a shooting cooldown.</li> <li>Spawn a new bullet when a button is pressed.</li> <li>Rotate the bullet and keep it moving in the direction it faces.</li> </ul> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">p</span><span class="p">.</span><span class="nx">shootCooldown</span><span class="p">.</span><span class="nf">Update</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">p</span><span class="p">.</span><span class="nx">shootCooldown</span><span class="p">.</span><span class="nf">IsReady</span><span class="p">()</span> <span class="o">&amp;&amp;</span> <span class="nx">ebiten</span><span class="p">.</span><span class="nf">IsKeyPressed</span><span class="p">(</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">KeySpace</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">shootCooldown</span><span class="p">.</span><span class="nf">Reset</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Spawn the bullet </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span></code></pre></div><p>One new thing is having <code>Player</code> spawn bullets instead of <code>Game</code>. But <code>Game</code> needs to keep track of the bullets, similar to meteors.</p> <p>The simplest way is to pass <code>Game</code> to <code>Player</code>&rsquo;s constructor and keep it in the struct. Then, expose an <code>AddBullet</code> method from <code>Game</code>. It&rsquo;s not ideal, as it creates a cross-dependency between <code>Player</code> and <code>Game</code>, but it&rsquo;s good enough for now.</p> <p>I decided to calculate the spawn point on the <code>Player</code> side and make the <code>Bullet</code>&rsquo;s constructor take it as an argument.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">bulletSpawnOffset</span> <span class="o">:=</span> <span class="mf">50.0</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">bounds</span> <span class="o">:=</span> <span class="nx">p</span><span class="p">.</span><span class="nx">sprite</span><span class="p">.</span><span class="nf">Bounds</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="nx">halfW</span> <span class="o">:=</span> <span class="nb">float64</span><span class="p">(</span><span class="nx">bounds</span><span class="p">.</span><span class="nf">Dx</span><span class="p">())</span> <span class="o">/</span> <span class="mi">2</span> </span></span><span class="line"><span class="cl"><span class="nx">halfH</span> <span class="o">:=</span> <span class="nb">float64</span><span class="p">(</span><span class="nx">bounds</span><span class="p">.</span><span class="nf">Dy</span><span class="p">())</span> <span class="o">/</span> <span class="mi">2</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">spawnPos</span> <span class="o">:=</span> <span class="nx">Vector</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">X</span> <span class="o">+</span> <span class="nx">halfW</span> <span class="o">+</span> <span class="nx">math</span><span class="p">.</span><span class="nf">Sin</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">rotation</span><span class="p">)</span><span class="o">*</span><span class="nx">bulletSpawnOffset</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">Y</span> <span class="o">+</span> <span class="nx">halfH</span> <span class="o">+</span> <span class="nx">math</span><span class="p">.</span><span class="nf">Cos</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">rotation</span><span class="p">)</span><span class="o">*-</span><span class="nx">bulletSpawnOffset</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">bullet</span> <span class="o">:=</span> <span class="nf">NewBullet</span><span class="p">(</span><span class="nx">spawnPos</span><span class="p">,</span> <span class="nx">p</span><span class="p">.</span><span class="nx">rotation</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">p</span><span class="p">.</span><span class="nx">game</span><span class="p">.</span><span class="nf">AddBullet</span><span class="p">(</span><span class="nx">bullet</span><span class="p">)</span> </span></span></code></pre></div> <img title="" loading="lazy" decoding="async" class="img img-center" width="1504" height="1240" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_12_hua1aa2cfb7523147da1c9fc35763a7dd7_367309_1504x1240_fill_q80_h2_lanczos_smart1_3.webp" alt="Image.png" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_12_hua1aa2cfb7523147da1c9fc35763a7dd7_367309_1504x1240_fill_lanczos_smart1_3.png"" /> <h2 id="collisions">Collisions</h2> <p>Detecting collisions is a complex topic, although, at the basic level, the concept is as simple as iterating over all objects and checking if they intersect.</p> <p>To make things easier, let&rsquo;s introduce a <code>Rect</code> struct that represents a rectangle and makes it easy to check intersections.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Rect</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">X</span> <span class="kt">float64</span> </span></span><span class="line"><span class="cl"> <span class="nx">Y</span> <span class="kt">float64</span> </span></span><span class="line"><span class="cl"> <span class="nx">Width</span> <span class="kt">float64</span> </span></span><span class="line"><span class="cl"> <span class="nx">Height</span> <span class="kt">float64</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewRect</span><span class="p">(</span><span class="nx">x</span><span class="p">,</span> <span class="nx">y</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span> <span class="kt">float64</span><span class="p">)</span> <span class="nx">Rect</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Rect</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">X</span><span class="p">:</span> <span class="nx">x</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Y</span><span class="p">:</span> <span class="nx">y</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Width</span><span class="p">:</span> <span class="nx">width</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Height</span><span class="p">:</span> <span class="nx">height</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="nx">Rect</span><span class="p">)</span> <span class="nf">MaxX</span><span class="p">()</span> <span class="kt">float64</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">r</span><span class="p">.</span><span class="nx">X</span> <span class="o">+</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Width</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="nx">Rect</span><span class="p">)</span> <span class="nf">MaxY</span><span class="p">()</span> <span class="kt">float64</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Y</span> <span class="o">+</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Height</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="nx">Rect</span><span class="p">)</span> <span class="nf">Intersects</span><span class="p">(</span><span class="nx">other</span> <span class="nx">Rect</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">r</span><span class="p">.</span><span class="nx">X</span> <span class="o">&lt;=</span> <span class="nx">other</span><span class="p">.</span><span class="nf">MaxX</span><span class="p">()</span> <span class="o">&amp;&amp;</span> </span></span><span class="line"><span class="cl"> <span class="nx">other</span><span class="p">.</span><span class="nx">X</span> <span class="o">&lt;=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">MaxX</span><span class="p">()</span> <span class="o">&amp;&amp;</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nx">Y</span> <span class="o">&lt;=</span> <span class="nx">other</span><span class="p">.</span><span class="nf">MaxY</span><span class="p">()</span> <span class="o">&amp;&amp;</span> </span></span><span class="line"><span class="cl"> <span class="nx">other</span><span class="p">.</span><span class="nx">Y</span> <span class="o">&lt;=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">MaxY</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Next, each game object exposes a <code>Collider() Rect</code> method.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="o">*</span><span class="nx">Player</span><span class="p">)</span> <span class="nf">Collider</span><span class="p">()</span> <span class="nx">Rect</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">bounds</span> <span class="o">:=</span> <span class="nx">p</span><span class="p">.</span><span class="nx">sprite</span><span class="p">.</span><span class="nf">Bounds</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">NewRect</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">X</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">position</span><span class="p">.</span><span class="nx">Y</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nb">float64</span><span class="p">(</span><span class="nx">bounds</span><span class="p">.</span><span class="nf">Dx</span><span class="p">()),</span> </span></span><span class="line"><span class="cl"> <span class="nb">float64</span><span class="p">(</span><span class="nx">bounds</span><span class="p">.</span><span class="nf">Dy</span><span class="p">()),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>In the Game&rsquo;s <code>Update</code>, iterate over all objects and check for collisions.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">i</span><span class="p">,</span> <span class="nx">m</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">g</span><span class="p">.</span><span class="nx">meteors</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">j</span><span class="p">,</span> <span class="nx">b</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">g</span><span class="p">.</span><span class="nx">bullets</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">m</span><span class="p">.</span><span class="nf">Collider</span><span class="p">().</span><span class="nf">Intersects</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nf">Collider</span><span class="p">())</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// A meteor collided with a bullet </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">m</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">g</span><span class="p">.</span><span class="nx">meteors</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">m</span><span class="p">.</span><span class="nf">Collider</span><span class="p">().</span><span class="nf">Intersects</span><span class="p">(</span><span class="nx">g</span><span class="p">.</span><span class="nx">player</span><span class="p">.</span><span class="nf">Collider</span><span class="p">())</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// A meteor collided with the player </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>When a bullet collides with a meteor, we want to destroy both objects. In our case, this means just dropping them using slice operations. If they&rsquo;re gone from the list, the game won&rsquo;t call their <code>Update</code> and <code>Draw</code> methods, so they effectively disappear (and the garbage collector handles the rest for us).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">m</span><span class="p">.</span><span class="nf">Collider</span><span class="p">().</span><span class="nf">Intersects</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nf">Collider</span><span class="p">())</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">meteors</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">g</span><span class="p">.</span><span class="nx">meteors</span><span class="p">[:</span><span class="nx">i</span><span class="p">],</span> <span class="nx">g</span><span class="p">.</span><span class="nx">meteors</span><span class="p">[</span><span class="nx">i</span><span class="o">+</span><span class="mi">1</span><span class="p">:]</span><span class="o">...</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">bullets</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">g</span><span class="p">.</span><span class="nx">bullets</span><span class="p">[:</span><span class="nx">j</span><span class="p">],</span> <span class="nx">g</span><span class="p">.</span><span class="nx">bullets</span><span class="p">[</span><span class="nx">j</span><span class="o">+</span><span class="mi">1</span><span class="p">:]</span><span class="o">...</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>A meteor colliding with the player means the game is over. Let&rsquo;s restart the game in this case.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Reset</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">player</span> <span class="p">=</span> <span class="nf">NewPlayer</span><span class="p">(</span><span class="nx">g</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">meteors</span> <span class="p">=</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">bullets</span> <span class="p">=</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h2 id="ui">UI</h2> <p>Drawing UI is similar to drawing other sprites. Two rules of thumb:</p> <ul> <li>It&rsquo;s usually the last layer you draw, so it always stays on top.</li> <li>Sometimes, you want it on a separate section of the screen. In this case, you want to draw the game not directly on the screen but on a smaller canvas that you eventually draw next to the UI. (You can create an empty image with <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fhajimehoshi%2Febiten%2Fv2%23NewImage" target="_blank"><code>ebiten.NewImage</code></a> and use it as a &ldquo;panel&rdquo;).</li> </ul> <p>A classic UI example is the score the player gets after shooting down a meteor. We can keep the <code>score</code> as an integer in the <code>Game</code> struct, increment it when a meteor collides with a bullet, and reset it to zero on the game over.</p> <p>What&rsquo;s left is drawing the score on the screen. First, you need to load a font, which is similar to loading a sprite. (I&rsquo;m using <a href="proxy.php?url=https%3A%2F%2Fwww.kenney.nl%2Fassets%2Fkenney-fonts" target="_blank">Kenney Fonts</a>.)</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">ScoreFont</span> <span class="p">=</span> <span class="nf">mustLoadFont</span><span class="p">(</span><span class="s">&#34;font.ttf&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">mustLoadFont</span><span class="p">(</span><span class="nx">name</span> <span class="kt">string</span><span class="p">)</span> <span class="nx">font</span><span class="p">.</span><span class="nx">Face</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">f</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">assets</span><span class="p">.</span><span class="nf">ReadFile</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">tt</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">opentype</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="nx">f</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">face</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">opentype</span><span class="p">.</span><span class="nf">NewFace</span><span class="p">(</span><span class="nx">tt</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">opentype</span><span class="p">.</span><span class="nx">FaceOptions</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Size</span><span class="p">:</span> <span class="mi">48</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">DPI</span><span class="p">:</span> <span class="mi">72</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Hinting</span><span class="p">:</span> <span class="nx">font</span><span class="p">.</span><span class="nx">HintingVertical</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">face</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Then, call <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fhajimehoshi%2Febiten%2Ftext%23Draw" target="_blank"><code>text.Draw</code></a> in <code>Game</code>&rsquo;s <code>Draw</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">text</span><span class="p">.</span><span class="nf">Draw</span><span class="p">(</span><span class="nx">screen</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;%06d&#34;</span><span class="p">,</span> <span class="nx">g</span><span class="p">.</span><span class="nx">score</span><span class="p">),</span> <span class="nx">ScoreFont</span><span class="p">,</span> <span class="nx">ScreenWidth</span><span class="o">/</span><span class="mi">2</span><span class="o">-</span><span class="mi">100</span><span class="p">,</span> <span class="mi">50</span><span class="p">,</span> <span class="nx">color</span><span class="p">.</span><span class="nx">White</span><span class="p">)</span> </span></span></code></pre></div><p>If you want to be precise with the position, use <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgolang.org%2Fx%2Fimage%2Ffont%23BoundString" target="_blank"><code>font.BoundString</code></a> to get the bounds. In the example above, I just estimated something that looks good enough.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1504" height="1240" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_13_hua1aa2cfb7523147da1c9fc35763a7dd7_366695_1504x1240_fill_q80_h2_lanczos_smart1_3.webp" alt="Image.png" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_13_hua1aa2cfb7523147da1c9fc35763a7dd7_366695_1504x1240_fill_lanczos_smart1_3.png"" /> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> If you like this article, check out our materials on topics other than games.<br>You can start with our e-book on modern Go applications! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/making-games-in-go/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="golang;gamedev"> <input type="hidden" name="fields[blog_series]" value=""> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h2 id="other-concepts">Other concepts</h2> <p>This article is already quite long, but I want to mention some ideas you may find helpful once you start making games.</p> <h3 id="scenes">Scenes</h3> <p>A scene is a standalone part of your game. For example, the welcome screen, the main menu, or the game itself.</p> <p>In the <em>meteors</em> project, there&rsquo;s a single <code>Game</code> scene. This could be extended so it&rsquo;s possible to switch between scenes, and each has its own <code>Update</code> and <code>Draw</code> methods. (And manages its own game objects.)</p> <h3 id="camera">Camera</h3> <p>I was initially intimidated by the camera concept, but it turned out to be a simple idea. Your game &ldquo;world&rdquo; can be a big area, and you often don&rsquo;t want to show all of it at once. With the camera concept, you draw only part of the bigger image. You can move the camera, make it follow the player, or scale it up and down to achieve the zoom effect.</p> <p>A trivial example needs just two things:</p> <ul> <li>A <code>Camera</code> struct that keeps the position (and optionally scale and other fields).</li> <li>An <code>offscreen</code> image (initialized with <code>ebiten.NewImage</code>).</li> </ul> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Game</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">camera</span> <span class="o">*</span><span class="nx">Camera</span> </span></span><span class="line"><span class="cl"> <span class="nx">offscreen</span> <span class="o">*</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">Image</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">player</span> <span class="o">*</span><span class="nx">Player</span> </span></span><span class="line"><span class="cl"> <span class="nx">enemies</span> <span class="p">[]</span><span class="o">*</span><span class="nx">Enemy</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="o">*</span><span class="nx">Game</span><span class="p">)</span> <span class="nf">Draw</span><span class="p">(</span><span class="nx">screen</span> <span class="o">*</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">Image</span><span class="p">){</span> </span></span><span class="line"><span class="cl"> <span class="nx">g</span><span class="p">.</span><span class="nx">offscreen</span><span class="p">.</span><span class="nf">Clear</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Draw the game on the offscreen image </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">g</span><span class="p">.</span><span class="nx">player</span><span class="p">.</span><span class="nf">Draw</span><span class="p">(</span><span class="nx">g</span><span class="p">.</span><span class="nx">offscreen</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">e</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">g</span><span class="p">.</span><span class="nx">enemies</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">e</span><span class="p">.</span><span class="nf">Draw</span><span class="p">(</span><span class="nx">g</span><span class="p">.</span><span class="nx">offscreen</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Draw the offscreen image on the screen </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">op</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">ebiten</span><span class="p">.</span><span class="nx">DrawImageOptions</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="nx">op</span><span class="p">.</span><span class="nx">GeoM</span><span class="p">.</span><span class="nf">Translate</span><span class="p">(</span><span class="o">-</span><span class="nx">g</span><span class="p">.</span><span class="nx">camera</span><span class="p">.</span><span class="nx">X</span><span class="p">,</span> <span class="o">-</span><span class="nx">g</span><span class="p">.</span><span class="nx">camera</span><span class="p">.</span><span class="nx">Y</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">screen</span><span class="p">.</span><span class="nf">DrawImage</span><span class="p">(</span><span class="nx">g</span><span class="p">.</span><span class="nx">offscreen</span><span class="p">,</span> <span class="nx">op</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>As you update the camera&rsquo;s position, the part of the offscreen image will also change.</p> <h3 id="animations">Animations</h3> <p>An animation means changing the sprite of an object over time. Sometimes, just two or three sprites can create an animation that makes the game look more alive.</p> <p>To implement it, create a slice of sprites, a timer, and a &ldquo;current sprite index&rdquo;. Then, increase the index as the timer resets.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="o">*</span><span class="nx">Player</span><span class="p">)</span> <span class="nf">Update</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">timer</span><span class="p">.</span><span class="nf">Update</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">p</span><span class="p">.</span><span class="nx">timer</span><span class="p">.</span><span class="nf">IsReady</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">timer</span><span class="p">.</span><span class="nf">Reset</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">index</span><span class="o">++</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">p</span><span class="p">.</span><span class="nx">index</span> <span class="o">&gt;=</span> <span class="nb">len</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">sprites</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">index</span> <span class="p">=</span> <span class="mi">0</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">p</span><span class="p">.</span><span class="nx">sprite</span> <span class="p">=</span> <span class="nx">p</span><span class="p">.</span><span class="nx">sprites</span><span class="p">[</span><span class="nx">p</span><span class="p">.</span><span class="nx">index</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Animation sprites often come as a single file with frames next to each other. You can use the <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fhajimehoshi%2Febiten%2Fv2%23Image.SubImage" target="_blank"><code>SubImage</code></a> method to extract a single frame from the loaded image.</p> <p>You can also &ldquo;animate&rdquo; other parameters of an object, like the sprite&rsquo;s color or scale.</p> <h3 id="deploying-to-the-web">Deploying to the web</h3> <p>It&rsquo;s easy to deploy your game with WebAssembly and host it anywhere.</p> <p>See more details in the <a href="proxy.php?url=https%3A%2F%2Febitengine.org%2Fen%2Fdocuments%2Fwebassembly.html" target="_blank">docs</a> and on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fmeteors" target="_blank">GitHub</a>.</p> <h3 id="entity-component-system">Entity Component System</h3> <p>You probably noticed many common parts between the <code>Player</code>, <code>Meteor</code>, and <code>Bullet</code> game objects. It&rsquo;s tempting to extract the common code so it&rsquo;s easier to create new objects, and it definitely makes sense.</p> <p>One way to do it is by keeping a generic <code>GameObject</code> struct from which other game objects inherit. In Go, you could use struct embedding with similar results.</p> <p>Entity Component System (ECS) is another approach that prefers composition over inheritance. The idea is to keep common <em>components</em> like position, sprite, and collider as separate structs and then create game objects (<em>entities</em>) by combining them. All the logic is kept in <em>systems</em> that iterate over all entities and update them.</p> <p>There are many ECS libraries for Go. My pick is <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fyohamta%2Fdonburi" target="_blank">donburi</a>, and my <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fm110%2Fairplanes" target="_blank">airplanes</a> game is based on it.</p> <h3 id="debugging">Debugging</h3> <p>As with any code, you will sometimes get stuck with hard-to-debug bugs. While using prints for debugging sometimes helps, I like adding visual helpers in place, sort of &ldquo;debug mode.&rdquo; It often shows in a very explicit way where the issue is.</p> <p>You can make it appear and hide after a key so it doesn&rsquo;t interfere with regular gameplay. Here&rsquo;s an example debug mode I introduced in <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fm110%2Fairplanes" target="_blank">airplanes</a>:</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1184" height="1560" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmaking-games-in-go%2Fimages%2Fimage_14_hu5f40c3f96afdf8f4d1d14135be922fa8_378075_1184x1560_fill_q80_h2_lanczos_smart1_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fmaking-games-in-go%5C%2Fimages%5C%2Fimage_14_hu5f40c3f96afdf8f4d1d14135be922fa8_378075_1184x1560_fill_lanczos_smart1_3.png"" /> <p>Ebitengine provides some utils for this:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fhajimehoshi%2Febiten%2Fv2%2Febitenutil%23DebugPrintAt" target="_blank"><code>ebitenutil.DebugPrintAt</code></a> — to quickly draw a text.</li> <li>The <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fhajimehoshi%2Febiten%2Fv2%2Fvector" target="_blank"><code>vector</code></a> package has functions like <code>StrokeLine</code>, <code>StrokeRect</code>, and <code>StroneCircle</code>.</li> </ul> <h2 id="go-build-something">Go build something!</h2> <p>All this barely scratches the surface of Gamedev. I picked topics that should let you make games and learn the basics. Let me know if you&rsquo;d like to see more posts on some topics.</p> <p>To learn more about Ebitengine, the <a href="proxy.php?url=https%3A%2F%2Febitengine.org%2Fen%2Fexamples%2F" target="_blank">official examples</a> are a great starting point.</p> <p>My example project is on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fmeteors" target="_blank">GitHub</a>, and you can <a href="proxy.php?url=https%3A%2F%2Fthreedotslabs.github.io%2Fmeteors%2F" target="_blank">play it online</a>. Feel free to fork the repository and iterate over it!</p> <p>Here are some ideas to implement:</p> <ul> <li>Add support for controls using a mouse or gamepad.</li> <li>Make big meteors &ldquo;break&rdquo; into small ones instead of disappearing.</li> <li>Have meteors drop power-ups that the player can collect.</li> <li>Allow switching weapons.</li> <li>Add AI spaceships that follow a more complex pattern than flying towards the player.</li> <li>Add more scenes like the main menu and high scores.</li> <li>Add sound effects. (It really makes a difference!)</li> </ul> <p>If you can&rsquo;t figure out how to create some mechanic you came up with (and it will happen a lot), try to figure out each step that needs to happen. For me, sketching using pen and paper often helps.</p> <p>Now, it&rsquo;s time to go and build something. Have fun!</p>Watermill 1.3 released, an open-source event-driven Go libraryhttps://threedots.tech/post/watermill-1-3/Mon, 25 Sep 2023 00:00:00 +0200https://threedots.tech/post/watermill-1-3/<p>Hey, it&rsquo;s been a long time!</p> <p>We&rsquo;re happy to share that Watermill v1.3 is now out!</p> <h2 id="what-is-watermill">What is Watermill</h2> <p><strong><a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">Watermill</a> is an open-source library for building message-driven or event-driven applications the easy way in Go. Our definition of &ldquo;easy&rdquo; is as easy as building an HTTP server in Go.</strong> With all that, it&rsquo;s a library, not a framework. So your application is not tied to Watermill forever.</p> <p>Currently, Watermill has <strong>over 6k stars on GitHub, has over 50 contributors, and has been used by numerous projects in the last 4 years.</strong> <strong>It makes Watermill the most popular open-source event-driven library in Go.</strong></p> <p>Watermill <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2F" target="_blank">provides</a> <strong>support for 12 different Pub/Sub implementations</strong>, including Kafka, Redis Streams, NATS, Google Cloud Pub/Sub, Amazon SQS/SNS (alpha), SQL, RabbitMQ and more. It&rsquo;s also possible to provide a custom Pub/Sub implementation if needed.</p> <p>We believe that the foundation of every good open-source is excellent <a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">documentation</a> together with <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples" target="_blank">real-life examples</a> showing all features.</p> <p>Yesterday, we just wrapped up the v1.3 release. We included multiple features that make Watermill even more powerful and easy to use.</p> <h2 id="what-weve-been-up-to-over-the-last-year">What we&rsquo;ve been up to over the last year</h2> <p>In March, we kicked off the work on our second training, <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fevent-driven%2F" target="_blank">Go Event-Driven</a>. To check the interest, we organized a pre-sale with the goal of selling at least 50 copies. To our amazement, <strong>323 copies sold within a week</strong> and generated over $54k of income. Even though <strong>we didn&rsquo;t have any content ready yet.</strong></p> <p>Working on the training limited our time for Watermill, but on the other hand, it created an excellent synergy. <strong>While coming up with examples for the exercises, we also found a few improvements to Watermill.</strong></p> <p>Finally, once we finished the training, we could wrap up the work on Watermill v1.3.</p> <h2 id="celebrating-the-launch">Celebrating the Launch</h2> <p>You can watch the release party recording <a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D5LzTTyvPngQ" target="_blank">on YouTube</a>.</p> <p>Let&rsquo;s dive deeper into the changes!</p> <h2 id="whats-new">What&rsquo;s new?</h2> <p>For the complete list of changes, see the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Freleases" target="_blank">GitHub releases page</a>.</p> <p>Big thanks go to everybody who contributed to this release! 💪</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FAlexCuse" target="_blank">@AlexCuse</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Ftjnet" target="_blank">@tjnet</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fhlubek" target="_blank">@hlubek</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvladtenlive" target="_blank">@vladtenlive</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdkotik" target="_blank">@dkotik</a></li> </ul> <h2 id="new-cqrs-api">New CQRS API</h2> <p>There are three APIs for handling messages in Watermill:</p> <ul> <li>The raw <code>Subscribe()</code> calls on a <code>Subscriber</code>.</li> <li>The <code>Router</code> and message handlers.</li> <li>The <code>cqrs</code> component and event/command handlers.</li> </ul> <p>Each is a more high-level abstraction than the previous one. There&rsquo;s no best API — depending on your use case, you may want to use one or the other.</p> <p>During the last release, we mentioned we wanted to introduce a generic API to eliminate conversions from the <code>interface{}</code> (now: <code>any</code>).</p> <p>At the same time, we noticed some design issues with the CQRS component. The way to use it was via the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fa7fa497bf3a6a353b83017728719277868d770cb%2Fcomponents%2Fcqrs%2Fcqrs.go%23L107" target="_blank"><code>Facade</code></a>, which wired everything up using a <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fa7fa497bf3a6a353b83017728719277868d770cb%2Fcomponents%2Fcqrs%2Fcqrs.go%23L11" target="_blank">big config</a>. If you wanted to create a simple setup, it felt like too much effort compared to the <code>Router</code>.</p> <p>Here&rsquo;s what changed in v1.3.</p> <h4 id="the-cqrsfacade-is-now-deprecated">The <code>cqrs.Facade</code> is now deprecated</h4> <p>We won&rsquo;t remove it, so your current code will keep working as before. We recommend using the new API instead of the Facade for new use cases.</p> <p>For consuming messages, you want to use the <code>CommandProcessor</code> and <code>EventProcessor</code>:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fa7fa497bf3a6a353b83017728719277868d770cb%2Fcomponents%2Fcqrs%2Fcommand_processor.go%23L117" target="_blank"><code>cqrs.NewCommandProcessorWithConfig</code></a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fa7fa497bf3a6a353b83017728719277868d770cb%2Fcomponents%2Fcqrs%2Fevent_processor.go%23L118" target="_blank"><code>cqrs.NewEventProcessorWithConfig</code></a></li> </ul> <p>For publishing messages, you want to use the <code>CommandBus</code> and <code>EventBus</code>:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fa7fa497bf3a6a353b83017728719277868d770cb%2Fcomponents%2Fcqrs%2Fcommand_bus.go%23L77" target="_blank"><code>cqrs.NewCommandBusWithConfig</code></a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fa7fa497bf3a6a353b83017728719277868d770cb%2Fcomponents%2Fcqrs%2Fevent_bus.go%23L103" target="_blank"><code>cqrs.NewEventBusWithConfig</code></a></li> </ul> <p>The bus and processor concepts are not new. The previous API also had them, so this change is backward-compatible. Their API is now more cohesive and easier to use.</p> <h4 id="generic-handlers">Generic handlers</h4> <p>With the new API, you can use the same event or command handlers as before. There&rsquo;s also the new generic API that eliminates the type assertion and is easier to write.</p> <p>This is the original API (still supported):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">OrderPlacedHandler</span> <span class="kd">struct</span> <span class="p">{}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">OrderPlacedHandler</span><span class="p">)</span> <span class="nf">HandlerName</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="s">&#34;OrderPlacedHandler&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">OrderPlacedHandler</span><span class="p">)</span> <span class="nf">NewCommand</span><span class="p">()</span> <span class="kd">interface</span><span class="p">{}</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">OrderPlaced</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">OrderPlacedHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">e</span> <span class="kd">interface</span><span class="p">{})</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">event</span> <span class="o">:=</span> <span class="nx">e</span><span class="p">.(</span><span class="o">*</span><span class="nx">OrderPlaced</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>And this is the new generic API:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">cqrs</span><span class="p">.</span><span class="nf">NewEventHandler</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;OrderPlacedHandler&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="o">*</span><span class="nx">OrderPlaced</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// No more type assertions! </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><p>Of course, you can still use structs for dependency injection:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">OrderPlacedHandler</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">repository</span> <span class="nx">OrderRepository</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">OrderPlacedHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="o">*</span><span class="nx">OrderPlaced</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">.</span><span class="nx">repository</span><span class="p">.</span><span class="nf">Save</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">h</span> <span class="o">:=</span> <span class="nx">OrderPlacedHandler</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">repository</span><span class="p">:</span> <span class="nf">NewRepository</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">eventHandler</span> <span class="o">:=</span> <span class="nx">cqrs</span><span class="p">.</span><span class="nf">NewEventHandler</span><span class="p">(</span><span class="s">&#34;OrderPlacedHandler&#34;</span><span class="p">,</span> <span class="nx">h</span><span class="p">.</span><span class="nx">Handle</span><span class="p">)</span> </span></span></code></pre></div><h4 id="event-handler-groups">Event handler groups</h4> <p>The <code>EventProcessor</code> creates one subscriber for each event handler. This works great out of the box for the common use case of one event type per topic.</p> <p>If you keep all your events on one topic, you can use the new <code>AddHandlersGroup()</code> method to create a single subscriber for all specified handlers. This is especially important if you care about the message ordering. Otherwise, your events may be processed in parallel by different handlers.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">err</span> <span class="o">:=</span> <span class="nx">eventProcessor</span><span class="p">.</span><span class="nf">AddHandlersGroup</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;events&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserSignedUpHandler</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">OrderPlacedHandler</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">OrderCancelledHandler</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><p>In this setup, set the <code>AckOnUnknownEvent</code> config option to <code>true</code> so the processor skips events that don&rsquo;t have a handler assigned.</p> <h3 id="circuit-breaker-middleware">Circuit Breaker Middleware</h3> <p>A new <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fmiddlewares%2F%23circuit-breaker" target="_blank">CircuitBreaker</a> middleware implements the circuit breaker pattern. It uses the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsony%2Fgobreaker" target="_blank">gobreaker</a> library.</p> <p>Like the <code>Throttle</code> middleware, it can help to avoid overloading your downstream services in case of a failure.</p> <p>If processing the message fails several times, the middleware immediately returns an error without calling the handler for the following messages. After some time, it tries to rerun the handler. If it succeeds, it returns to normal. Otherwise, it keeps failing.</p> <p>The number of fails, the time it needs to fail within, and the time to wait before retrying are configurable, among other things.</p> <h3 id="request-reply-support-for-commands">Request-Reply support for commands</h3> <p>While communication over messages is asynchronous most of the time, it sometimes makes sense to receive a confirmation or result from a command. Many Pub/Subs support this use case with the request-reply pattern. We introduced a similar feature that works on top of any Pub/Sub.</p> <p>The new <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2Fcomponents%2Frequestreply" target="_blank"><code>requestreply</code> package</a> works on top of the <code>cqrs.CommandBus</code>. It allows you to send a command and wait for a reply. The <code>Reply</code> can contain an error or, optionally, a (generic) result. If you are returning just an error from the handler, the function signature is compatible with the <a href="#generic-handlers">generic handler</a> signature.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">err</span> <span class="o">:=</span> <span class="nx">commandProcessor</span><span class="p">.</span><span class="nf">AddHandlers</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">requestreply</span><span class="p">.</span><span class="nf">NewCommandHandler</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;hotel_room_booking&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">ts</span><span class="p">.</span><span class="nx">RequestReplyBackend</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="o">*</span><span class="nx">BookHotelRoom</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;some error&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span></code></pre></div><p>After running the command processor with the handler, you can send a command and wait for a reply:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">reply</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">requestreply</span><span class="p">.</span><span class="nx">SendWithReply</span><span class="p">[</span><span class="nx">requestreply</span><span class="p">.</span><span class="nx">NoResult</span><span class="p">](</span> </span></span><span class="line"><span class="cl"> <span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="nx">ts</span><span class="p">.</span><span class="nx">CommandBus</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">ts</span><span class="p">.</span><span class="nx">RequestReplyBackend</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="o">&amp;</span><span class="nx">BookHotelRoom</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;1&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">reply</span><span class="p">.</span><span class="nx">Error</span><span class="p">)</span> <span class="c1">// it&#39;s equal to `fmt.Errorf(&#34;some error&#34;)` </span></span></span></code></pre></div><p>You can also send a result from the handler:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">err</span> <span class="o">:=</span> <span class="nx">commandProcessor</span><span class="p">.</span><span class="nf">AddHandlers</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">requestreply</span><span class="p">.</span><span class="nx">NewCommandHandlerWithResult</span><span class="p">[</span><span class="nx">PayForRoom</span><span class="p">,</span> <span class="nx">PayForRoomResult</span><span class="p">](</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;pay_for_room&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">ts</span><span class="p">.</span><span class="nx">RequestReplyBackend</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="o">*</span><span class="nx">PayForRoom</span><span class="p">)</span> <span class="p">(</span><span class="nx">PayForRoomResult</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">PayForRoomResult</span><span class="p">{</span><span class="nx">PaymentReference</span><span class="p">:</span> <span class="s">&#34;1234&#34;</span><span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span></code></pre></div><p>After running the command processor with the handler, you can send a command and wait for a reply:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">reply</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">requestreply</span><span class="p">.</span><span class="nx">SendWithReply</span><span class="p">[</span><span class="nx">requestreply</span><span class="p">.</span><span class="nx">NoResult</span><span class="p">](</span> </span></span><span class="line"><span class="cl"> <span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="nx">ts</span><span class="p">.</span><span class="nx">CommandBus</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">ts</span><span class="p">.</span><span class="nx">RequestReplyBackend</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="o">&amp;</span><span class="nx">TestCommand</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;1&#34;</span><span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">reply</span><span class="p">.</span><span class="nx">Result</span><span class="p">.</span><span class="nx">PaymentReference</span><span class="p">)</span> <span class="c1">// it&#39;s equal to &#34;1234&#34; </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">reply</span><span class="p">.</span><span class="nx">Error</span><span class="p">)</span> <span class="c1">// it&#39;s nil </span></span></span></code></pre></div><p>We have also used generic types here, so your code is type-safe.</p> <p>By default, we don&rsquo;t recommend using the request-reply pattern because it introduces coupling between services. Usually, using RPC is a better choice. But if you have a good argument for using request-reply (for example, you want to ensure that the request is processed, even if there was a transient error, and you wish to return a response to the customer), we&rsquo;ve got you covered.</p> <h3 id="slog-adapter">Slog adapter</h3> <p>Watermill now supports a logger backed by the new <code>slog</code> API.</p> <h3 id="watermill-sql-v2-released">watermill-sql: v2 released</h3> <p>We discovered a low chance of losing messages in watermill-sql in some specific cases. This is due to how transactions work — if you&rsquo;re interested in the details, see <a href="proxy.php?url=https%3A%2F%2Fevent-driven.io%2Fen%2Fordering_in_postgres_outbox%2F" target="_blank">this blog post</a>.</p> <p>We had to change the default schema and the public API to fix this, so this is a major version bump. We recommend using watermill-sql v2 from now on.</p> <p><strong>Note the minimal PostgreSQL version to use it is now 13.</strong></p> <p>Migrating the existing database schema would be pretty complicated. Instead, we recommend using new tables.</p> <p>If you use watermill-sql just for the outbox pattern and there is one topic you care about, you can rename it. For example, if you&rsquo;ve used the topic <code>events-to-forward</code>, you can rename it to <code>events-to-forward-v2</code> while bumping watermill-sql to v2. This will create new tables with the new schema.</p> <p>If you use more topics, you can use the <code>GenerateMessagesTableName</code> option to customize the table name. The default prefix is <code>watermill_</code>. You can change it to something else, like <code>watermill_v2_</code>, while bumping watermill-sql to v2.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">GenerateMessagesTableName</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">topic</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;watermill_v2_%s&#34;</span><span class="p">,</span> <span class="nx">topic</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><strong>Remember to pass this option to both the schema and the offsets schema!</strong></p> <p>Usually, these tables are used just for delivering the messages and are not needed after it&rsquo;s done. But you need to ensure all messages are processed before switching to new tables. In high-traffic environments, you may need to run both versions of watermill-sql for the migration period.</p> <h3 id="amazon-sqssns--looking-for-testers">Amazon SQS/SNS — looking for testers!</h3> <p>There&rsquo;s some work going on within PRs of <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-amazonsqs" target="_blank">watermill-amazonsqs</a> (thank you, <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FMariscal6" target="_blank">@Mariscal6</a>!). If you&rsquo;re familiar with this Pub/Sub or are running a project using it, we&rsquo;d love to hear your feedback!</p> <h2 id="live-event-recording">Live Event Recording</h2> <p>You can watch the release party recording <a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D5LzTTyvPngQ" target="_blank">on YouTube</a>.</p>Watermill v1.2 releasedhttps://threedots.tech/post/watermill-1-2/Thu, 16 Feb 2023 00:00:00 +0100https://threedots.tech/post/watermill-1-2/<p>After almost three years since the last stable release, Watermill v1.2 is finally out!</p> <p>If you&rsquo;re new here, <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2F" target="_blank">Watermill</a> is our open-source library for building event-driven applications in Go, the easy way.</p> <p>A lot has happened since v1.1. We&rsquo;ve been running Watermill on production in many projects, using it in new ways and adding features to support new use cases. We also received a lot of feedback and contributions from the community.</p> <p>Watermill grew to over 5.2k stars on GitHub and 39 contributors, counting just the main repository. We&rsquo;ve added 4 new official Pub/Subs, and there are many unofficial integrations.</p> <h1 id="celebrating-the-launch">Celebrating the Launch</h1> <img title="" loading="lazy" decoding="async" class="img img-center" width="600" height="400" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-2%2Fgopher_hu7c3d9f72f2a912b992200a15a40020d9_162612_600x400_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwatermill-1-2%5C%2Fgopher_hu7c3d9f72f2a912b992200a15a40020d9_162612_600x400_resize_q80_lanczos.jpeg"" /> <div class="code-ref"> Meet the Gopher at our live event! </div> <p>To celebrate the release, we plan to host a live event on March 1st at 5pm UTC. Join us <a href="proxy.php?url=https%3A%2F%2Fyoutube.com%2Flive%2Fwjnd0Hj6CaM" target="_blank">on YouTube</a>.</p> <p>The planned agenda is as follows:</p> <ul> <li>Watermill 1.2 Showcase.</li> <li>Using Consumer Groups in Watermill - a live demo with examples.</li> <li>Announcing our <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fevent-driven%2F" target="_blank">Event-Driven Training</a>.</li> <li>Coming next to Watermill (generics!).</li> <li>A chance to win a unique crotchet Gopher.</li> </ul> <h1 id="whats-new">What&rsquo;s new?</h1> <p>You can find the full list of changes on the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Freleases%2Ftag%2Fv1.2.0" target="_blank">releases page</a>. It&rsquo;s long and includes many bug fixes and minor improvements, often from outside contributors (thank you, everyone!).</p> <p>Below are highlights of the bigger changes.</p> <h2 id="the-forwarder-component">The Forwarder Component</h2> <p>One of the best use cases for Watermill is publishing messages in transactions (AKA the outbox pattern). I wrote about it before in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwhen-sql-database-makes-great-pub-sub%2F" target="_blank">When an SQL database makes a great Pub/Sub</a> featuring a <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Ftransactional-events" target="_blank">Watermill example</a>.</p> <p>The <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fcomponents%2Fforwarder%2Fforwarder.go" target="_blank">Forwarder component</a> provides a universal mechanism for this behavior: forwarding messages from one Pub/Sub to another (e.g., from SQL to Kafka).</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-2%2Fforwarder.svg" alt="" onerror="this.onerror='null';this.src=''" /> <p>It&rsquo;s a high-level API on top of the Router, so it&rsquo;s even easier to forward messages.</p> <p>Forwarder has its own <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fforwarder%2F" target="_blank">documentation entry</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Ftransactional-events-forwarder" target="_blank">a complete example</a>. It was contributed by <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fczeslavo" target="_blank">Grzegorz Burzyński</a>.</p> <h2 id="the-fanin-component">The FanIn Component</h2> <p>We already had the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fpubsub%2Fgochannel%2Ffanout.go" target="_blank">FanOut component</a> before. It takes a single stream of messages and re-publishes them on a few topics.</p> <p><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fcomponents%2Ffanin%2Ffanin.go" target="_blank">FanIn</a> does the opposite: it takes a collection of topics and merges the messages from them into a single stream.</p> <p>FanIn has its own <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Ffanin%2F" target="_blank">documentation entry</a>. It was contributed by <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2F0michalsokolowski0" target="_blank">Michał Sokołowski</a>.</p> <p>I&rsquo;m thrilled to see it&rsquo;s possible to write such components on top of existing structures. It shows that Watermill&rsquo;s abstractions follow the UNIX philosophy of modular components that work well together.</p> <h2 id="new-supported-pubsubs">New Supported Pub/Subs</h2> <img title="" loading="lazy" decoding="async" class="img img-center" width="1000" height="500" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-2%2Fpubsubs_hu700675a3593f2196f90a24bec185a055_55613_1000x500_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwatermill-1-2%5C%2Fpubsubs_hu700675a3593f2196f90a24bec185a055_55613_1000x500_resize_q80_lanczos.jpg"" /> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-firestore" target="_blank">Firestore</a> - a NoSQL document database from Google. Contributed by <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fczeslavo" target="_blank">Grzegorz Burzyński</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fboreq" target="_blank">Filip Borkiewicz</a>.</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-bolt" target="_blank">Bolt</a> - an embedded key/value database for Go. Contributed by <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fboreq" target="_blank">Filip Borkiewicz</a>.</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-nats" target="_blank">NATS Jetstream</a> - NATS&rsquo;s built-in distributed persistence system. We released v2.0.0 in the existing NATS repository. Contributed by <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FAlexCuse" target="_blank">Alex Ullrich</a>.</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-redisstream" target="_blank">Redis Stream</a> - Redis-based Pub/Sub. Contributed by <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fminghsu0107" target="_blank">Hao-Ming Hsu</a>.</li> </ul> <h2 id="new-examples">New Examples</h2> <h3 id="server-sent-events-example">Server-Sent Events example</h3> <p>SSE is our preferred way of pushing updates from the server. It&rsquo;s simpler than WebSockets and plays nicely with Watermill&rsquo;s abstractions.</p> <p>See the example on the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Fserver-sent-events" target="_blank">Watermill repository</a>.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-2%2Fsse.gif" alt="" onerror="this.onerror='null';this.src=''" /> <h3 id="exactly-once-delivery-example">Exactly-once delivery example</h3> <p>Exactly-once delivery is not possible in most setups, but there&rsquo;s one specific scenario where you can achieve it.</p> <p>See the example on the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Fexactly-once-delivery-counter" target="_blank">Watermill repository</a>.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1282" height="547" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-2%2Fexactly-once_huc280cd4551a24b2e1500e817fcf4aa31_92286_1282x547_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwatermill-1-2%5C%2Fexactly-once_huc280cd4551a24b2e1500e817fcf4aa31_92286_1282x547_resize_q80_lanczos.jpg"" /> <h2 id="adding-handlers-while-the-router-is-running">Adding handlers while the Router is running</h2> <p>It&rsquo;s now possible to add handlers to a running Router. Previously, all handlers had to be set up before starting it.</p> <h2 id="new-ci">New CI</h2> <p>We moved the CI from CircleCI to GitHub Actions. It&rsquo;s better integrated with GitHub&rsquo;s PRs, and the configuration is simpler. All Pub/Subs now use the workflows defined in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F.github%2Fworkflows" target="_blank">main repository</a>.</p> <p>All repositories now use <a href="proxy.php?url=https%3A%2F%2Fgolangci-lint.run" target="_blank">golangci-lint</a> for static analysis.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1280" height="622" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-2%2Fci_hu98eaaa7d158f62d84eac4a5dc086c75c_45833_1280x622_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwatermill-1-2%5C%2Fci_hu98eaaa7d158f62d84eac4a5dc086c75c_45833_1280x622_resize_lanczos_3.png"" /> <h2 id="awesome-watermill">Awesome Watermill</h2> <p>We&rsquo;ve added the <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fawesome%2F" target="_blank">Awesome Watermill</a> page with a list of unofficial libraries. Please add a PR if you know a repository that should appear there!</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="369" height="369" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fwatermill-1-2%2Fwatermill_huc31a6bac377418fd1e3cba8bcadd661f_63499_369x369_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fwatermill-1-2%5C%2Fwatermill_huc31a6bac377418fd1e3cba8bcadd661f_63499_369x369_resize_lanczos_3.png"" /> <h2 id="watermill-contributors">Watermill Contributors</h2> <p>Without contributors, Watermill wouldn&rsquo;t be where it is today.</p> <p>Writing a Pub/Sub integration is an especially high-impact project. To support the authors, we now keep a dedicated private channel on our <a href="proxy.php?url=https%3A%2F%2Fdiscord.gg%2FQV6VFg4YQE" target="_blank">discord server</a> where they can reach us directly. The authors of Pub/Sub integrations keep write access to the repository after it&rsquo;s transferred to the ThreeDotsLabs organization.</p> <p>Let us know if you want to join this community and have your Pub/Sub integration officially supported.</p> <h3 id="support">Support</h3> <p>As always, if something&rsquo;s not clear, check out <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2F" target="_blank">watermill.io</a>. If you can&rsquo;t find the answer there, <a href="proxy.php?url=https%3A%2F%2Fdiscord.gg%2FQV6VFg4YQE" target="_blank">join our discord server</a> and ask your questions on the <strong>#watermill</strong> channel.</p> <p>If you&rsquo;d like to get some open-source experience and contribute to Watermill, we&rsquo;ve prepared a list of <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%3Fq%3Dis%253Aissue%26amp%3B%2343%3Bis%253Aopen%26amp%3B%2343%3Blabel%253A%2522good%26amp%3B%2343%3Bfirst%26amp%3B%2343%3Bissue%2522" target="_blank">good first issues</a>.</p>The Go libraries that never failed us: 22 libraries you need to knowhttps://threedots.tech/post/list-of-recommended-libraries/Tue, 13 Dec 2022 00:00:00 +0100https://threedots.tech/post/list-of-recommended-libraries/<p>Did you have a situation when you lost a ton of time finding a Go library for your need? In theory, you can check lists like <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Favelino%2Fawesome-go" target="_blank">Awesome Go</a> or make a choice based on GitHub stars. But Awesome Go contains over 2600 libraries, and popularity is not always the best indicator of library quality. <strong>I often thought that it would be great to have a place where I could find just the best and battle-tested libraries I could use in my project.</strong> Because we didn&rsquo;t find such a place with Miłosz, we decided to create it.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flist-of-recommended-libraries%2Flibrary-gopher.svg" alt="Frankenstein Gopher" onerror="this.onerror='null';this.src=''" /> <p>Based on our experience leading multiple Go teams and working on various projects, including complex financial, health, and security, we will recommend tools that could work well for different projects.</p> <p>In addition to providing a list of libraries, we also want to show you some non-obvious uses for those tools and libraries. However, it&rsquo;s important to note that most of these tools can be misused. We&rsquo;ve included some common anti-patterns to help you avoid making those mistakes.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>This list is intended to be opinionated. <strong>We only wanted to include libraries we used on real production systems. Thanks to that, we recommend just libraries that we are 100% sure about.</strong> Unfortunately, our day is limited to 24 hours, so checking all available libraries is impossible.</p> <p><strong>If you know of any libraries we should include on this list, please let us know in the comments!</strong> We will continue to update the list with new findings over time.</p> </p></div> </div> <details class="table-of-content "> <summary> Table of Contents </summary> <nav id="TableOfContents"> <ol> <li><a href="#http">HTTP</a> <ol> <li><a href="#routers">Routers</a> <ol> <li><a href="#white_check_mark-echo-githubhttpsgithubcomlabstackecho-docshttpsecholabstackcomguide-exampleshttpsecholabstackcomcookbook"> Echo <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Flabstack%2Fecho">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fecho.labstack.com%2Fguide%2F">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fecho.labstack.com%2Fcookbook%2F">[Examples]</a></a></li> <li><a href="#white_check_mark-chi-githubhttpsgithubcomgo-chichi-docshttpspkggodevgithubcomgo-chichi-exampleshttpsgithubcomgo-chichitreemaster_examples"> chi <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-chi%2Fchi">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fgo-chi%2Fchi">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-chi%2Fchi%2Ftree%2Fmaster%2F_examples">[Examples]</a></a></li> </ol> </li> <li><a href="#middlewares">Middlewares</a></li> <li><a href="#serving-static-content">Serving static content</a></li> <li><a href="#openapi">OpenAPI</a></li> <li><a href="#generating-go-server-and-clients">Generating Go server and clients</a> <ol> <li><a href="#white_check_mark-deepmapoapi-codegen-githubhttpsgithubcomdeepmapoapi-codegen-docshttpsgithubcomdeepmapoapi-codegenreadme-examplehttpsthreedotstechpostserverless-cloud-run-firebase-modern-go-applicationpublic-http-api"> deepmap/oapi-codegen <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdeepmap%2Foapi-codegen">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdeepmap%2Foapi-codegen%23readme">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%23public-http-api">[Example]</a></a></li> </ol> </li> <li><a href="#bonus-client-for-javascripttypescript">Bonus: Client for JavaScript/TypeScript</a> <ol> <li><a href="#white_check_mark-openapi-generator-cli-githubhttpsgithubcomopenapitoolsopenapi-generator-cli-docshttpsgithubcomopenapitoolsopenapi-generator-clireadme"> openapi-generator-cli <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FOpenAPITools%2Fopenapi-generator-cli">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FOpenAPITools%2Fopenapi-generator-cli%23readme">[Docs]</a></a></li> </ol> </li> </ol> </li> <li><a href="#alternative-types-of-communication">Alternative types of communication</a> <ol> <li><a href="#grpc">gRPC</a> <ol> <li><a href="#white_check_mark-protoc-docshttpsgrpciodocs"> protoc <a href="proxy.php?url=https%3A%2F%2Fgrpc.io%2Fdocs%2F">[Docs]</a></a></li> </ol> </li> <li><a href="#messaging">Messaging</a> <ol> <li><a href="#white_check_mark-watermill-githubhttpsgithubcomthreedotslabswatermill-docshttpswatermillio-exampleshttpsgithubcomthreedotslabswatermilltreemaster_examples"> Watermill <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2F">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples">[Examples]</a></a></li> </ol> </li> </ol> </li> <li><a href="#database">Database</a> <ol> <li><a href="#sql">SQL</a> <ol> <li><a href="#white_check_mark-sqlx-githubhttpsgithubcomjmoironsqlx-docshttpjmoirongithubiosqlx"> sqlx <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjmoiron%2Fsqlx">[GitHub]</a> <a href="proxy.php?url=http%3A%2F%2Fjmoiron.github.io%2Fsqlx%2F">[Docs]</a></a></li> <li><a href="#white_check_mark-sqlc-githubhttpsgithubcomsqlc-devsqlc-docshttpsdocssqlcdev-exampleshttpsdocssqlcdevenlatesttutorialsgetting-started-postgresqlhtml"> sqlc <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsqlc-dev%2Fsqlc">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fdocs.sqlc.dev%2F">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fdocs.sqlc.dev%2Fen%2Flatest%2Ftutorials%2Fgetting-started-postgresql.html">[Examples]</a></a></li> <li><a href="#white_check_mark-sqlboiler-githubhttpsgithubcomvolatiletechsqlboiler-docshttpsgithubcomvolatiletechsqlboilertable-of-contents-exampleshttpsgithubcomvolatiletechsqlboilerfeatures--examples"> SQLBoiler <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvolatiletech%2Fsqlboiler">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvolatiletech%2Fsqlboiler%23table-of-contents">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvolatiletech%2Fsqlboiler%23features--examples">[Examples]</a></a></li> </ol> </li> <li><a href="#migrations">Migrations</a> <ol> <li><a href="#white_check_mark-sql-migrate-githubhttpsgithubcomrubenvsql-migrate-docshttpsgithubcomrubenvsql-migratereadme"> sql-migrate <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Frubenv%2Fsql-migrate">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Frubenv%2Fsql-migrate%23readme">[Docs]</a></a></li> <li><a href="#white_check_mark-goose-githubhttpsgithubcompresslygoose-docshttpspkggodevgithubcompresslygoose"> goose <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fpressly%2Fgoose">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fpressly%2Fgoose">[Docs]</a></a></li> </ol> </li> </ol> </li> <li><a href="#observability">Observability</a> <ol> <li><a href="#logging">Logging</a> <ol> <li><a href="#white_check_mark-logrus-githubhttpsgithubcomsirupsenlogrus-docshttpspkggodevgithubcomsirupsenlogrus"> Logrus <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsirupsen%2Flogrus">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsirupsen%2Flogrus">[Docs]</a></a></li> <li><a href="#white_check_mark-zap-githubhttpsgithubcomuber-gozap-docshttppkggodevgithubcomuber-gozap"> zap <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fuber-go%2Fzap">[GitHub]</a> <a href="proxy.php?url=http%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fuber-go%2Fzap">[Docs]</a></a></li> </ol> </li> <li><a href="#metrics-and-tracing">Metrics and tracing</a> <ol> <li><a href="#white_check_mark-opencensus-go-githubhttpsgithubcomcensus-instrumentationopencensus-go-docshttpsopencensusio"> opencensus-go <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fcensus-instrumentation%2Fopencensus-go">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fopencensus.io">[Docs]</a></a></li> </ol> </li> </ol> </li> <li><a href="#configuration">Configuration</a> <ol> <li><a href="#env-variables">Env variables</a> <ol> <li><a href="#white_check_mark-caarlos0env-githubhttpsgithubcomcaarlos0env-docshttpspkggodevgithubcomcaarlos0env"> caarlos0/env <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fcaarlos0%2Fenv">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fcaarlos0%2Fenv">[Docs]</a></a></li> <li><a href="#multi-format-configuration">Multi-format configuration</a></li> <li><a href="#white_check_mark-koanf-githubhttpsgithubcomknadhkoanf-docshttpspkggodevgithubcomknadhkoanf"> koanf <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fknadh%2Fkoanf">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fknadh%2Fkoanf">[Docs]</a></a></li> </ol> </li> </ol> </li> <li><a href="#building-cli">Building CLI</a> <ol> <li><a href="#building-cli-libraries">Building CLI libraries</a> <ol> <li><a href="#white_check_mark-urfavecli-githubhttpsgithubcomurfavecli-docshttpscliurfaveorg-exampleshttpscliurfaveorgv2examplesgreet"> urfave/cli <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Furfave%2Fcli%2F">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fcli.urfave.org%2F">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fcli.urfave.org%2Fv2%2Fexamples%2Fgreet%2F">[Examples]</a></a></li> </ol> </li> </ol> </li> <li><a href="#testing">Testing</a> <ol> <li><a href="#assertions">Assertions</a> <ol> <li><a href="#white_check_mark-testify-githubhttpsgithubcomstretchrtestify-docshttpspkggodevgithubcomstretchrtestify"> testify <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fstretchr%2Ftestify">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fstretchr%2Ftestify">[Docs]</a></a></li> <li><a href="#white_check_mark-go-cmp-githubhttpsgithubcomgooglego-cmp-docshttpspkggodevgithubcomgooglego-cmp-examples-1httpsgithubcomgooglego-cmpblobmastercmpexample_testgo-examples-2httpsgithubcomgooglego-cmpblobmastercmpcmpoptsexample_testgo"> go-cmp <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgoogle%2Fgo-cmp">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fgoogle%2Fgo-cmp">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgoogle%2Fgo-cmp%2Fblob%2Fmaster%2Fcmp%2Fexample_test.go">[Examples 1]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgoogle%2Fgo-cmp%2Fblob%2Fmaster%2Fcmp%2Fcmpopts%2Fexample_test.go">[Examples 2]</a></a></li> <li><a href="#white_check_mark-gofakeit-githubhttpsgithubcombrianvoegofakeit-docshttpspkggodevgithubcombrianvoegofakeit"> gofakeit <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fbrianvoe%2Fgofakeit">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fbrianvoe%2Fgofakeit">[Docs]</a></a></li> </ol> </li> <li><a href="#mocking">Mocking</a> <ol> <li><a href="#writing-mocks-by-hand">Writing mocks by hand</a></li> </ol> </li> </ol> </li> <li><a href="#misc">Misc</a> <ol> <li><a href="#extra-types-support">Extra types support</a> <ol> <li><a href="#white_check_mark-googleuuid-githubhttpsgithubcomgoogleuuid-docshttpspkggodevgithubcomgoogleuuid"> google/uuid <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgoogle%2Fuuid">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fgoogle%2Fuuid">[Docs]</a></a></li> <li><a href="#white_check_mark-oklogulid-githubhttpsgithubcomoklogulid-docshttpspkggodevgithubcomoklogulid"> oklog/ulid <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Foklog%2Fulid">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Foklog%2Fulid">[Docs]</a></a></li> <li><a href="#white_check_mark-shopspringdecimal-githubhttpsgithubcomshopspringdecimal-docshttpspkggodevgithubcomshopspringdecimal"> shopspring/decimal <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fshopspring%2Fdecimal">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fshopspring%2Fdecimal">[Docs]</a></a></li> </ol> </li> <li><a href="#errors">Errors</a> <ol> <li><a href="#white_check_mark-hashicorpgo-multierror-githubhttpsgithubcomhashicorpgo-multierror-docsgithubcomhashicorpgo-multierror"> hashicorp/go-multierror <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fhashicorp%2Fgo-multierror">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgithub.com%2Fhashicorp%2Fgo-multierror">[Docs]</a></a></li> </ol> </li> </ol> </li> <li><a href="#useful-tools">Useful tools</a> <ol> <li><a href="#misc-1">Misc</a> <ol> <li><a href="#white_check_mark-samberlo-githubhttpsgithubcomsamberlo-docshttpspkggodevgithubcomsamberlo"> samber/lo <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsamber%2Flo">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsamber%2Flo">[Docs]</a></a></li> <li><a href="#white_check_mark-task-githubhttpsgithubcomgo-tasktask-docshttpstaskfiledev"> Task <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-task%2Ftask">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Ftaskfile.dev%2F">[Docs]</a></a></li> </ol> </li> <li><a href="#live-code-reloading">Live code reloading</a> <ol> <li><a href="#white_check_mark-reflex-githubhttpsgithubcomcesparereflex-docshttpspkggodevgithubcomcesparereflex-examplehttpsthreedotstechpostgo-docker-dev-environment-with-go-modules-and-live-code-reloading"> reflex <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fcespare%2Freflex">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fcespare%2Freflex">[Docs]</a> [<a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-docker-dev-environment-with-go-modules-and-live-code-reloading%2F">Example</a>]</a></li> </ol> </li> <li><a href="#linter">Linter</a> <ol> <li><a href="#white_check_mark-golangci-lint-githubhttpsgithubcomgolangcigolangci-lint-docshttpsgolangci-lintrun"> golangci-lint <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgolangci%2Fgolangci-lint">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fgolangci-lint.run%2F">[Docs]</a></a></li> <li><a href="#white_check_mark-go-cleanarch-githubhttpsgithubcomroblaszczakgo-cleanarch-docshttpspkggodevgithubcomroblaszczakgo-cleanarchsection-readme"> go-cleanarch <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fgo-cleanarch">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Froblaszczak%2Fgo-cleanarch%23section-readme">[Docs]</a></a></li> </ol> </li> <li><a href="#formatters">Formatters</a> <ol> <li><a href="#white_check_mark-go-fmt"> go fmt</a></li> <li><a href="#white_check_mark-goimports-docshttpspkggodevgolangorgxtoolscmdgoimports"> goimports <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgolang.org%2Fx%2Ftools%2Fcmd%2Fgoimports">[Docs]</a></a></li> <li><a href="#white_check_mark-gofumpt-githubhttpsgithubcommvdangofumpt-docshttpspkggodevmvdanccgofumptsection-readme"> gofumpt <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmvdan%2Fgofumpt">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fmvdan.cc%2Fgofumpt%23section-readme">[Docs]</a></a></li> </ol> </li> </ol> </li> <li><a href="#example-projects">Example projects</a> <ol> <li><a href="#ddd--clean-architecture">DDD &amp; Clean Architecture</a> <ol> <li><a href="#white_check_mark-wild-workouts-go-ddd-example-application-githubhttpsgithubcomthreedotslabswild-workouts-go-ddd-example"> Wild Workouts Go DDD Example application <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example">[GitHub]</a></a></li> </ol> </li> <li><a href="#general-purpose">General purpose</a> <ol> <li><a href="#white_check_mark-modern-go-application-by-márk-sági-kazár-githubhttpsgithubcomsagikazarmarkmodern-go-application"> Modern Go Application by Márk Sági-Kazár <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsagikazarmark%2Fmodern-go-application">[GitHub]</a></a></li> </ol> </li> </ol> </li> <li><a href="#summary">Summary</a></li> </ol> </nav> </details> <h2 id="http">HTTP</h2> <h3 id="routers">Routers</h3> <p>As I mentioned in my <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbest-go-framework%2F">previous article</a>, it&rsquo;s generally better to use libraries instead of frameworks for long-term projects. One of the most fundamental components of any service is an HTTP router. While it&rsquo;s technically possible to build an application without one by using the standard library&rsquo;s <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fnet%2Fhttp" target="_blank">http</a> package, its routing capabilities are limited. Using a dedicated router will make your life much easier.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Frameworks in Go</p> </div> <div class="notice-body"><p> <p>If the library you consider using impacts how you write your domain models, it&rsquo;s probably a framework, not a router.</p> <p>We recommend using lightweight routers instead. Learn more about the risks of using the framework in my <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbest-go-framework%2F">previous article</a>.</p> </p></div> </div> <p>By design, router functionality is limited to routing requests to a proper handler. All non-standard functionalities like CORS, CSRF, error handling, HTTP logging, and authorization (that frameworks usually provide) are provided by reusable middlewares. I recommend some in the <a href="#middlewares">section on middlewares</a>.</p> <p>I use one of two router libraries in most projects: Echo or chi. Both of them are great routers, with different characteristics. They work perfectly with <a href="#openapi">OpenAPI</a> code generation.</p> <h4 id="white_check_mark-echo-githubhttpsgithubcomlabstackecho-docshttpsecholabstackcomguide-exampleshttpsecholabstackcomcookbook">&#x2705; Echo <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Flabstack%2Fecho" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fecho.labstack.com%2Fguide%2F" target="_blank">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fecho.labstack.com%2Fcookbook%2F" target="_blank">[Examples]</a></h4> <p>Compared to chi, Echo does offer a custom <code>*http.Request</code> handler signature. Some people may find it a downside, but I think it helps to write less error-prone HTTP handlers.</p> <p>If you have been writing Go for a while, you probably made this mistake at least once:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">someHandler</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">foo</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusBadRequest</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="c1">// you forgot the return here, bar() will be executed </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nf">bar</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Echo makes you return an error:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">someHandler</span><span class="p">(</span><span class="nx">c</span> <span class="nx">echo</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">foo</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nf">bar</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">c</span><span class="p">.</span><span class="nf">NoContent</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusNoContent</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>The advantage of Echo is the ability to define a custom <a href="proxy.php?url=https%3A%2F%2Fecho.labstack.com%2Fguide%2Ferror-handling%2F" target="_blank">error handler</a>. It&rsquo;s not possible to do it in the same way with chi.</p> <p>For detailed usage and examples, please check Echo docs.</p> <h4 id="white_check_mark-chi-githubhttpsgithubcomgo-chichi-docshttpspkggodevgithubcomgo-chichi-exampleshttpsgithubcomgo-chichitreemaster_examples">&#x2705; chi <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-chi%2Fchi" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fgo-chi%2Fchi" target="_blank">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-chi%2Fchi%2Ftree%2Fmaster%2F_examples" target="_blank">[Examples]</a></h4> <p>Compared to Echo, chi&rsquo;s handler functions are compatible with the standard library. For some people, it may be an upside; for some, it may be a downside &ndash; you should make your own judgment.</p> <p>What chi does better than Echo is the format of <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-chi%2Fchi%2Fblob%2F0fe6bf1ba3ac601700b7993bc4c62f6c5f707932%2F_examples%2Frest%2Fmain.go%23L83" target="_blank">defining routes and grouping</a>. It gives you better control over middleware per path or sub-path.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">r</span><span class="p">.</span><span class="nf">Route</span><span class="p">(</span><span class="s">&#34;/articles&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">r</span> <span class="nx">chi</span><span class="p">.</span><span class="nx">Router</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nf">With</span><span class="p">(</span><span class="nx">paginate</span><span class="p">).</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="nx">ListArticles</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nf">Post</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="nx">CreateArticle</span><span class="p">)</span> <span class="c1">// POST /articles </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">r</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;/search&#34;</span><span class="p">,</span> <span class="nx">SearchArticles</span><span class="p">)</span> <span class="c1">// GET /articles/search </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nf">Route</span><span class="p">(</span><span class="s">&#34;/{articleID}&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">r</span> <span class="nx">chi</span><span class="p">.</span><span class="nx">Router</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nf">Use</span><span class="p">(</span><span class="nx">ArticleCtx</span><span class="p">)</span> <span class="c1">// Load the *Article on the request context </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">r</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="nx">GetArticle</span><span class="p">)</span> <span class="c1">// GET /articles/123 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">r</span><span class="p">.</span><span class="nf">Put</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="nx">UpdateArticle</span><span class="p">)</span> <span class="c1">// PUT /articles/123 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">r</span><span class="p">.</span><span class="nf">Delete</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="nx">DeleteArticle</span><span class="p">)</span> <span class="c1">// DELETE /articles/123 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// GET /articles/whats-up </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">r</span><span class="p">.</span><span class="nf">With</span><span class="p">(</span><span class="nx">ArticleCtx</span><span class="p">).</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;/{articleSlug:[a-z-]+}&#34;</span><span class="p">,</span> <span class="nx">GetArticle</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-chi%2Fchi%2Fblob%2F0fe6bf1ba3ac601700b7993bc4c62f6c5f707932%2F_examples%2Frest%2Fmain.go%23L83" target="_blank">github.com/go-chi/chi/_examples/rest/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-chi%2Fchi%2Fblob%2F0fe6bf1ba3ac601700b7993bc4c62f6c5f707932%2F_examples%2Frest%2Fmain.go%23L83" target="_blank">Full source</a> </div> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: You should not choose tools based just on benchmarks</p> </div> <div class="notice-body"><p> <p>Some developers choose libraries based on the benchmark results. It&rsquo;s a risky approach because extreme performance optimizations lead to worse API and limited functionalities set. In most cases, performance differences are negligible in real-life use cases.</p> <p>Even if, for some applications, it may make a difference, for most applications, it doesn&rsquo;t matter that much. Making just one extra database query or up-scaling a service can make a much more significant difference in performance.</p> <p>If performance is not absolutely critical for you, you should prefer other characteristics, like the ease of use and number of features.</p> </p></div> </div> <h3 id="middlewares">Middlewares</h3> <p>HTTP middlewares can give you functionalities like CORS, CSRF, error handling, HTTP logging, authorization, etc.</p> <p>Echo and chi provide their set of middlewares:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fecho.labstack.com%2Fmiddleware%2F" target="_blank">Echo middlewares</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-chi%2Fchi%2Ftree%2Fmaster%2Fmiddleware" target="_blank">chi middlewares</a></li> </ul> <p>Echo middlewares have a different interface, so they can&rsquo;t be used in chi. Generally speaking, all standard-library compatible middlewares are compatible with chi and echo.</p> <p>To use standard library-compatible middleware with echo, you need to call <code>echo.WrapMiddleware</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">main</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/go-chi/chi/v5/middleware&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/labstack/echo/v4&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// echo version </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">e</span> <span class="o">:=</span> <span class="nx">echo</span><span class="p">.</span><span class="nf">New</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// You can use a middleware from chi with echo. </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">e</span><span class="p">.</span><span class="nf">Use</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">echo</span><span class="p">.</span><span class="nf">WrapMiddleware</span><span class="p">(</span><span class="nx">middleware</span><span class="p">.</span><span class="nf">BasicAuth</span><span class="p">(</span><span class="s">&#34;realm&#34;</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;admin&#34;</span><span class="p">:</span> <span class="s">&#34;password&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">})),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">e</span><span class="p">.</span><span class="nx">Logger</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nf">Start</span><span class="p">(</span><span class="s">&#34;:8080&#34;</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>If none of them provides the middleware you are looking for, you can check <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Favelino%2Fawesome-go%23middlewares" target="_blank">the Awesome Go list</a>. All of them will be compatible with chi, Echo, and servers built just with the standard library. You can also write your own middleware. Check example middlewares for inspiration!</p> <h3 id="serving-static-content">Serving static content</h3> <p>You don&rsquo;t need any library to serve static content in Go. Since Go 1.16, you can easily <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fembed" target="_blank">embed static files into your Go binary</a>.</p> <p>Here&rsquo;s how to do it for <code>Echo</code> and <code>chi</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">main</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;embed&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;net/http&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/go-chi/chi&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/go-chi/chi/v5&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/labstack/echo/v4&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// your static files should be in the static/ directory, for example static/index.html, static/main.js etc. </span></span></span><span class="line"><span class="cl"><span class="c1">// </span></span></span><span class="line"><span class="cl"><span class="c1">//go:embed static </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">staticFs</span> <span class="nx">embed</span><span class="p">.</span><span class="nx">FS</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// chi version </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span> <span class="o">:=</span> <span class="nx">chi</span><span class="p">.</span><span class="nf">NewRouter</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="s">&#34;/static/*&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nf">StripPrefix</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nf">FileServer</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nf">FS</span><span class="p">(</span><span class="nx">staticFs</span><span class="p">))))</span> </span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nf">ListenAndServe</span><span class="p">(</span><span class="s">&#34;:8080&#34;</span><span class="p">,</span> <span class="nx">r</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// echo version </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">e</span> <span class="o">:=</span> <span class="nx">echo</span><span class="p">.</span><span class="nf">New</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nx">e</span><span class="p">.</span><span class="nf">GET</span><span class="p">(</span><span class="s">&#34;/static/*&#34;</span><span class="p">,</span> <span class="nx">echo</span><span class="p">.</span><span class="nf">WrapHandler</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nf">StripPrefix</span><span class="p">(</span><span class="s">&#34;/&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nf">FileServer</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nf">FS</span><span class="p">(</span><span class="nx">staticFs</span><span class="p">)))))</span> </span></span><span class="line"><span class="cl"> <span class="nx">e</span><span class="p">.</span><span class="nx">Logger</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nf">Start</span><span class="p">(</span><span class="s">&#34;:8080&#34;</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>After running the server, assets will be available under <code>http://localhost:8080/static/index.html</code>, <code>http://localhost:8080/static/main.js</code> etc.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Do not use no-name libraries for trivial functionalities</p> </div> <div class="notice-body"><p> <p>Do you remember the <code>leftpad</code> JavaScript library? It was 11 lines of code adding padding on the left side of a string.</p> <p>Some day, the author decided to remove that library. It wouldn&rsquo;t be a big problem if it wasn&rsquo;t a dependency of thousands of projects, including Node and Babel.</p> <p>Serving static content from your web server is one of such trivial functionalities.</p> </p></div> </div> <div class="text-center"> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F%3Futm_source%3Dblog-content"> <img src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fsidebar%2Fcourse.svg" loading="lazy" decoding="async" alt="Go In One Evening" class=" img" width="500" height="500" /> </a> <span class="h5 inline-block"> Are you experienced engineer who wants to learn Go basics?<br> You don't become an engineer by watching videos. <br> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F%3Futm_source%3Dblog-content"> Learn Go hands-on by building real projects. </a> </span> </div> <h3 id="openapi">OpenAPI</h3> <p>Nobody likes to maintain API contracts manually. It&rsquo;s annoying and counterproductive to keep multiple boring JSON&rsquo;s up-to-date. OpenAPI solves this problem with a JavaScript HTTP client and Go HTTP server generated from the provided specification.</p> <p>This is how an <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Ftree%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Fapi%2Fopenapi" target="_blank">example specification</a> looks like. If you didn&rsquo;t work with OpenAPI before, you can read more details in my <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%23openapi-swagger-client">previous article</a>. Here, I focus on tools that we recommend for OpenAPI spec-generated code.</p> <h3 id="generating-go-server-and-clients">Generating Go server and clients</h3> <p>We do not recommend using the official OpenAPI generator for the Go code. We recommend the <code>oapi-codegen</code> tool instead because of the higher quality of the generated code. It also has more functionalities.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Don't try to generate OpenAPI spec from Go code</p> </div> <div class="notice-body"><p> <p>There are tools that can generate an OpenAPI spec from Go code. We don&rsquo;t recommend using them.</p> <p>The entire OpenAPI specification is very rich, and it will be hard to generate everything from Go code. It&rsquo;s likely that you will need to add something to the OpenAPI spec at some point, and it may be impossible to do it from the Go code.</p> <p>It&rsquo;s much easier to generate it the other way around: Go code from OpenAPI spec.</p> </p></div> </div> <h4 id="white_check_mark-deepmapoapi-codegen-githubhttpsgithubcomdeepmapoapi-codegen-docshttpsgithubcomdeepmapoapi-codegenreadme-examplehttpsthreedotstechpostserverless-cloud-run-firebase-modern-go-applicationpublic-http-api">&#x2705; deepmap/oapi-codegen <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdeepmap%2Foapi-codegen" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdeepmap%2Foapi-codegen%23readme" target="_blank">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%23public-http-api" target="_blank">[Example]</a></h4> <p><code>oapi-codegen</code> is a great tool that doesn&rsquo;t just generate models but also the entire <a href="#routers">router</a> definition, header&rsquo;s validation, and proper parameters parsing. It works with <a href="#chi">chi</a> and <a href="#echo">Echo</a>.</p> <p>To generate a server, run the following:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">oapi-codegen -generate types -o <span class="s2">&#34;&lt;OUTPUT DIR&gt;/openapi_types.gen.go&#34;</span> -package <span class="s2">&#34;&lt;GO PACKAGE&gt;&#34;</span> <span class="s2">&#34;api/openapi/service.yml&#34;</span> </span></span><span class="line"><span class="cl">oapi-codegen -generate &lt;TYPE&gt; -o <span class="s2">&#34;&lt;OUTPUT DIR&gt;/openapi_api.gen.go&#34;</span> -package <span class="s2">&#34;&lt;GO PACKAGE&gt;&#34;</span> <span class="s2">&#34;api/openapi/service.yml&#34;</span> </span></span></code></pre></div><p>Where <code>&lt;TYPE&gt;</code> for <code>chi</code> should be <code>chi-server</code>, and for <code>Echo</code> just <code>server</code>.</p> <p>To generate clients:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">oapi-codegen -generate types -o <span class="s2">&#34;&lt;OUTPUT DIR&gt;/</span><span class="nv">$service</span><span class="s2">/openapi_types.gen.go&#34;</span> -package <span class="s2">&#34;&lt;GO PACKAGE&gt;&#34;</span> <span class="s2">&#34;api/openapi/service.yml&#34;</span> </span></span><span class="line"><span class="cl">oapi-codegen -generate client -o <span class="s2">&#34;&lt;OUTPUT DIR&gt;/</span><span class="nv">$service</span><span class="s2">/openapi_client_gen.go&#34;</span> -package <span class="s2">&#34;&lt;GO PACKAGE&gt;&#34;</span> <span class="s2">&#34;api/openapi/service.yml&#34;</span> </span></span></code></pre></div><p>Don&rsquo;t forget to change <code>&lt;GO PACKAGE&gt;</code> to the desired Go package name and <code>&lt;OUTPUT DIR&gt;</code> to the desired output dir. &#x1f609;</p> <p>Your job on the server side is just to implement the <code>ServerInterface</code> interface, like:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// ServerInterface represents all server handlers. </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">ServerInterface</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// (GET /trainer/calendar) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">GetTrainerAvailableHours</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">,</span> <span class="nx">params</span> <span class="nx">GetTrainerAvailableHoursParams</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// (PUT /trainer/calendar/make-hour-available) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">MakeHourAvailable</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// (PUT /trainer/calendar/make-hour-unavailable) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">MakeHourUnavailable</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fb519c611e9d1248a149c89db9bcf879fd78b1e35%2Finternal%2Ftrainer%2Fports%2Fopenapi_api.gen.go" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/ports/openapi_api.gen.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fb519c611e9d1248a149c89db9bcf879fd78b1e35%2Finternal%2Ftrainer%2Fports%2Fopenapi_api.gen.go" target="_blank">Full source</a> </div> <p>You can see it in action in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">Wild Workouts project</a>.</p> <h3 id="bonus-client-for-javascripttypescript">Bonus: Client for JavaScript/TypeScript</h3> <p>Even if it&rsquo;s the list of <strong>recommended Go libraries</strong>, you may need to generate code for the browser.</p> <h4 id="white_check_mark-openapi-generator-cli-githubhttpsgithubcomopenapitoolsopenapi-generator-cli-docshttpsgithubcomopenapitoolsopenapi-generator-clireadme">&#x2705; openapi-generator-cli <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FOpenAPITools%2Fopenapi-generator-cli" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FOpenAPITools%2Fopenapi-generator-cli%23readme" target="_blank">[Docs]</a></h4> <p>In that case, we also recommend a non-official library instead of the official one.</p> <p>In contrast to <code>oapi-codegen</code>, <code>openapi-generator-cli</code> is a Java tool. To avoid any JVM-related issues, we recommend generating clients using Docker:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm --env <span class="s2">&#34;JAVA_OPTS=-Dlog.level=error&#34;</span> -v <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PWD</span><span class="si">}</span><span class="s2">:/local&#34;</span> <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> <span class="s2">&#34;openapitools/openapi-generator-cli:v6.2.1&#34;</span> generate <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> -i <span class="s2">&#34;/local/api/openapi/service.yml&#34;</span> <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> -g javascript <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> -o <span class="s2">&#34;/local/web/src/clients/service&#34;</span> </span></span></code></pre></div><p>It assumes that the spec is available locally in <code>api/openapi/service.yml</code>.</p> <p>You can use <code>openapi-generator-cli</code> for TypeScript and other languages as well.</p> <h2 id="alternative-types-of-communication">Alternative types of communication</h2> <h3 id="grpc">gRPC</h3> <p>gRPC is a technology that can help you with building robust, internal communication between your services (but not only!).</p> <p>I already described in detail why it&rsquo;s <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frobust-grpc-google-cloud-run%2F">worth using gRPC for internal communication</a> and how to do it.</p> <p>I&rsquo;ll not repeat it here and will focus on the tooling you need.</p> <p>With gRPC, you have little choice for generating server and client: you should use official tooling. The good news is that you don&rsquo;t need anything more because it does its job!</p> <h4 id="white_check_mark-protoc-docshttpsgrpciodocs">&#x2705; protoc <a href="proxy.php?url=https%3A%2F%2Fgrpc.io%2Fdocs%2F" target="_blank">[Docs]</a></h4> <p>To generate Go code from <code>.proto</code> files, you need to install <a href="proxy.php?url=https%3A%2F%2Fgrpc.io%2Fdocs%2Fprotoc-installation%2F" target="_blank">protoc</a> and <a href="proxy.php?url=https%3A%2F%2Fgrpc.io%2Fdocs%2Fquickstart%2Fgo%2F" target="_blank">protoc Go Plugin</a>.</p> <p>A list of supported types can be found in <a href="proxy.php?url=https%3A%2F%2Fdevelopers.google.com%2Fprotocol-buffers%2Fdocs%2Freference%2Fproto3-spec%23fields" target="_blank">Protocol Buffers Version 3 Language Specification</a>. More complex built-in types like Timestamp can be found in <a href="proxy.php?url=https%3A%2F%2Fdevelopers.google.com%2Fprotocol-buffers%2Fdocs%2Freference%2Fgoogle.protobuf" target="_blank">Well-Known Types list</a>.</p> <h3 id="messaging">Messaging</h3> <h4 id="white_check_mark-watermill-githubhttpsgithubcomthreedotslabswatermill-docshttpswatermillio-exampleshttpsgithubcomthreedotslabswatermilltreemaster_examples">&#x2705; Watermill <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2F" target="_blank">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples" target="_blank">[Examples]</a></h4> <p>About four years ago, when working on one of our projects, we found that there is no library that can simplify building message-driven or event-driven applications easily. To make our lives easier, we decided to write a library that will allow us to write event-driven code as easily as writing HTTP services. This is how Watermill was born.</p> <p>Today, Watermill is one of the most popular Go libraries with almost 5k GitHub stars, +35 contributors, and 10 officially supported Pub/Subs.</p> <p>Usually, message-broker libraries are very low-level. With Watermill, publishing messages may be as simple as:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">publisher</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="s">&#34;example.topic&#34;</span><span class="p">,</span> <span class="nx">msg</span><span class="p">)</span> </span></span></code></pre></div><p>And subscribing like:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">messages</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">subscriber</span><span class="p">.</span><span class="nf">Subscribe</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="s">&#34;example.topic&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">msg</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">messages</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;received message: %s, payload: %s\n&#34;</span><span class="p">,</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">UUID</span><span class="p">,</span> <span class="nb">string</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">Payload</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="nx">msg</span><span class="p">.</span><span class="nf">Ack</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Compared to using just the message broker&rsquo;s library, Watermill provides support for some higher level functionalities like <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fmiddlewares%2F" target="_blank">middlewares</a>, <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fcqrs%2F" target="_blank">CQRS support</a>, or <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fforwarder%2F" target="_blank">event-forwarder</a> component (that can be used to stream your messages from an SQL database to the message broker).</p> <p>Today, Watermill officially supports <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Fkafka%2F" target="_blank">Kafka</a>, <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Fgooglecloud%2F" target="_blank">GCP Pub/Sub</a>, <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Fnats%2F" target="_blank">NATS</a>, <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Famqp%2F" target="_blank">RabbitMQ</a> message brokers (Pub/Subs). It can also listen to and emit events as <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Fhttp%2F" target="_blank">HTTP hooks</a>, from databases like <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Fsql%2F" target="_blank">MySQL/Postgres</a>, <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Fbolt%2F" target="_blank">BoltDB</a> or <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Ffirestore%2F" target="_blank">Firestore</a>. It can also work with in-memory <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Fgochannel%2F" target="_blank">Go-channel based Pub/Sub</a>.</p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/list-of-recommended-libraries/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="libraries;web-applications;go;golang"> <input type="hidden" name="fields[blog_series]" value=""> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h2 id="database">Database</h2> <h3 id="sql">SQL</h3> <p>There is no golden hammer solution for interacting with SQL databases. The reason is simple: it depends greatly on what kind of data you store.</p> <p>In some projects, data models are relatively simple. In some, they are very complex. Because of that, I have two libraries to recommend. You should choose one of them based on the requirements of your project.</p> <p>For projects with straightforward data models, you should check <code>sqlx</code>. For a bit more complex, you should look at <code>SQLBoiler</code>.</p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Using ORM</p> </div> <div class="notice-body"><p> <p>I hear more and more that using ORM is not a good idea. I understand a reason for such thinking: many people are hurt by the improper use of ORMs.</p> <p>It&rsquo;s like with a knife: I have an extremally sharp Japanese knife without which I can&rsquo;t imagine cooking. On another side, I need to be very careful with using it. But that fact doesn&rsquo;t make this knife a bad tool! If you are using it properly, it&rsquo;s making your life much easier. It&rsquo;s the same situation with ORMs. Writing queries by hand may be time-consuming and error-prone when your models are complex. ORMs were invented to solve that problem.</p> <p>If you have a bad experience using ORMs, you should check <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthings-to-know-about-dry%2F">Things to know about DRY</a> article. The tactics presented in that article will help you to avoid all common problems with ORMs.</p> </p></div> </div> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Avoid weakly typed ORMs</p> </div> <div class="notice-body"><p> Most ORMs depend heavily on reflection and <code>interface{}</code>/<code>any</code>. The type system is one of the biggest strengths of Go. It helps you build applications efficiently. Resigning from strict typing makes your application more error-prone. </p></div> </div> <h4 id="white_check_mark-sqlx-githubhttpsgithubcomjmoironsqlx-docshttpjmoirongithubiosqlx">&#x2705; sqlx <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjmoiron%2Fsqlx" target="_blank">[GitHub]</a> <a href="proxy.php?url=http%3A%2F%2Fjmoiron.github.io%2Fsqlx%2F" target="_blank">[Docs]</a></h4> <p>The standard library&rsquo;s <code>database/sql</code> package is rather a low-level one. <code>sqlx</code> provides a more convenient and powerful API to work with databases. It includes helper functions for common tasks like inserting and querying data and support for more advanced features like prepared statements and transactions. <code>sqlx</code> also has more advanced support for data unmarshaling (for example to structs, lists of structs, json data, etc.). As a nice bonus, <code>sqlx</code>&rsquo;s interface is compatible with interfaces from <code>database/sql</code>.</p> <p>But even if <code>sqlx</code> is a great library, it works well for relatively simple database models. At some level of complexity, you should consider migration to an ORM.</p> <h4 id="white_check_mark-sqlc-githubhttpsgithubcomsqlc-devsqlc-docshttpsdocssqlcdev-exampleshttpsdocssqlcdevenlatesttutorialsgetting-started-postgresqlhtml">&#x2705; sqlc <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsqlc-dev%2Fsqlc" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fdocs.sqlc.dev%2F" target="_blank">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fdocs.sqlc.dev%2Fen%2Flatest%2Ftutorials%2Fgetting-started-postgresql.html" target="_blank">[Examples]</a></h4> <p><strong>sqlc is now our default choice for new projects.</strong> Like SQLBoiler, it generates type-safe Go code, but from your SQL queries rather than just the schema. You write standard SQL, and sqlc generates fully type-safe Go code for your queries.</p> <p>The key advantages of sqlc:</p> <ul> <li><strong>Query-first approach</strong>: You write SQL queries directly, and sqlc generates Go functions for them. No new query DSL to learn.</li> <li><strong>Compile-time safety</strong>: Catches SQL errors before runtime, including column mismatches and type errors.</li> <li><strong>Excellent PostgreSQL support</strong>: First-class support for PostgreSQL features like CTEs, window functions, and arrays.</li> <li><strong>Active development</strong>: The project receives regular updates and has a growing community.</li> </ul> <p>Here&rsquo;s an example workflow.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- queries/users.sql </span></span></span><span class="line"><span class="cl"><span class="c1">-- name: GetUser :one </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">email</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">$</span><span class="mi">1</span><span class="p">;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- name: ListUsers :many </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">email</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">users</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">name</span><span class="p">;</span><span class="w"> </span></span></span></code></pre></div><p>sqlc generates type-safe Go functions you can call directly:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">queries</span><span class="p">.</span><span class="nf">GetUser</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">userID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">users</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">queries</span><span class="p">.</span><span class="nf">ListUsers</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> </span></span></code></pre></div><p>sqlc supports PostgreSQL, MySQL, and SQLite.</p> <h4 id="white_check_mark-sqlboiler-githubhttpsgithubcomvolatiletechsqlboiler-docshttpsgithubcomvolatiletechsqlboilertable-of-contents-exampleshttpsgithubcomvolatiletechsqlboilerfeatures--examples">&#x2705; SQLBoiler <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvolatiletech%2Fsqlboiler" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvolatiletech%2Fsqlboiler%23table-of-contents" target="_blank">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvolatiletech%2Fsqlboiler%23features--examples" target="_blank">[Examples]</a></h4> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <strong>Update (2026):</strong> SQLBoiler is now in maintenance mode. It still receives updates and works well in existing projects, but we recommend sqlc for new projects. </p></div> </div> <p>SQLBoiler is an ORM that generates Go models from your database schema. At first, how you define SQLBoiler models may surprise you. Most ORMs generate the database schema out of your Go models. SQLBoiler does the opposite: it generates Go models from your database schema.</p> <p>This approach has multiple advantages. One of the most important features is stricter typing than other libraries. Thanks to that, many checks are done during compilation. You don&rsquo;t need to depend on a ton of reflection and magic struct tags. In most cases, as long as the code compiles, it will work correctly.</p> <p>Generating code from the database schema helps with migration from an existing database because you don&rsquo;t need to re-write DB models: SQLBoiler generates them for you. So if you start with <code>sqlx</code> and move to SQLBoiler later, the migration should be pretty easy.</p> <p>SQLBoiler supports PostgreSQL, MySQL, MSSQLServer 2012+, SQLite3, and CockroachDB.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Using database models in the API responses</p> </div> <div class="notice-body"><p> <p>As long as you&rsquo;re not writing a stupid simple CRUD application (and the chances are you&rsquo;re not), you should not couple your database models with the API responses.</p> <p>At some point, requirements will force you to return data in a format different from the format you have in the database. Instead of trying to follow DRY at all costs, it&rsquo;s time to split your models.</p> <p>You can read more on this in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthings-to-know-about-dry%2F" target="_blank">&ldquo;Business Applications in Go: Things to know about DRY&rdquo; article</a> and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcommon-anti-patterns-in-go-web-applications%2F" target="_blank">&ldquo;Common Anti-Patterns in Go Web Applications&rdquo;</a>.</p> </p></div> </div> <h3 id="migrations">Migrations</h3> <p>SQLBoiler and <code>sqlx</code> don&rsquo;t provide out-of-the-box support for migrations. It&rsquo;s okay because you are not forced to use any particular solution.</p> <p>In my recent projects, I used both <code>sql-migrate</code> and <code>goose</code>, and I was happy about them.</p> <h4 id="white_check_mark-sql-migrate-githubhttpsgithubcomrubenvsql-migrate-docshttpsgithubcomrubenvsql-migratereadme">&#x2705; sql-migrate <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Frubenv%2Fsql-migrate" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Frubenv%2Fsql-migrate%23readme" target="_blank">[Docs]</a></h4> <h4 id="white_check_mark-goose-githubhttpsgithubcompresslygoose-docshttpspkggodevgithubcompresslygoose">&#x2705; goose <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fpressly%2Fgoose" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fpressly%2Fgoose" target="_blank">[Docs]</a></h4> <p>We like <code>sql-migrate</code> and <code>goose</code> because of their simplicity and flexibility. <code>sql-migrate</code> and <code>goose</code> can be executed as CLI tools and as part of your service.</p> <p>I like to embed it into the binary of the service. Thanks to that, the migration is executed when the service starts, and it keeps the setup simple. It&rsquo;s also much less complex to run. For example, <code>sql-migrate</code> with <code>go:embed</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// migrations/run.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kn">package</span> <span class="nx">migrations</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;database/sql&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;embed&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">migrate</span> <span class="s">&#34;github.com/rubenv/sql-migrate&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">//go:embed * </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">migrationsFiles</span> <span class="nx">embed</span><span class="p">.</span><span class="nx">FS</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">Run</span><span class="p">(</span><span class="nx">postgresConn</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">sql</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="s">&#34;postgres&#34;</span><span class="p">,</span> <span class="nx">postgresConn</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">migrations</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">migrate</span><span class="p">.</span><span class="nx">EmbedFileSystemMigrationSource</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">FileSystem</span><span class="p">:</span> <span class="nx">migrationsFiles</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Root</span><span class="p">:</span> <span class="s">&#34;.&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">migrate</span><span class="p">.</span><span class="nf">Exec</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="s">&#34;postgres&#34;</span><span class="p">,</span> <span class="nx">migrations</span><span class="p">,</span> <span class="nx">migrate</span><span class="p">.</span><span class="nx">Up</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Put your migrations in <code>migrations/</code>, for example: <code>migrations/1_init.sql</code>, <code>migrations/2_alter_some_table.sql</code>, etc. Then run <code>Run</code> in your <code>main</code>.</p> <h2 id="observability">Observability</h2> <h3 id="logging">Logging</h3> <p>The standard library&rsquo;s logger doesn&rsquo;t provide essential features like log levels and output formatting.</p> <p>For logging, we can recommend two libraries: <code>Logrus</code> and <code>zap</code>. In contrast to <code>zap</code>, <code>Logrus</code> provides a bit nicer user API, but <code>zap</code> is faster.</p> <p>You can check detailed benchmarks in <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fuber-go%2Fzap%23performance" target="_blank">zap&rsquo;s readme</a>.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: You should not choose tools based just on benchmarks</p> </div> <div class="notice-body"><p> <p>Some developers tend to choose libraries based on the benchmark results. It&rsquo;s a risky approach because extreme performance optimizations lead to worse API and limited functionalities set. In most cases, performance differences are negligible in real-life use cases.</p> <p>Even if, for some applications, it may make a difference, for most applications, it doesn&rsquo;t matter that much. Making just one extra database query or up-scaling a service can make a much more significant difference in performance.</p> <p>If performance is not absolutely critical for you, you should prefer other characteristics, like the ease of use and number of features.</p> </p></div> </div> <h4 id="white_check_mark-logrus-githubhttpsgithubcomsirupsenlogrus-docshttpspkggodevgithubcomsirupsenlogrus">&#x2705; Logrus <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsirupsen%2Flogrus" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsirupsen%2Flogrus" target="_blank">[Docs]</a></h4> <h4 id="white_check_mark-zap-githubhttpsgithubcomuber-gozap-docshttppkggodevgithubcomuber-gozap">&#x2705; zap <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fuber-go%2Fzap" target="_blank">[GitHub]</a> <a href="proxy.php?url=http%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fuber-go%2Fzap" target="_blank">[Docs]</a></h4> <h3 id="metrics-and-tracing">Metrics and tracing</h3> <h4 id="white_check_mark-opencensus-go-githubhttpsgithubcomcensus-instrumentationopencensus-go-docshttpsopencensusio">&#x2705; opencensus-go <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fcensus-instrumentation%2Fopencensus-go" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fopencensus.io" target="_blank">[Docs]</a></h4> <p>OpenCensus Go is a library that helps you add metrics and tracing to your endpoints or database queries. The integration uses middleware/decorator patterns, and it doesn&rsquo;t require a lot of custom code. It supports <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgo.opencensus.io%2Fplugin%2Fochttp" target="_blank">HTTP endpoints</a>, <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgo.opencensus.io%2Fplugin%2Focgrpc" target="_blank">gRPC endpoints</a>, <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fopencensus-integrations%2Focsql" target="_blank">SQL databases</a>, <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Forijtech%2Fmongo-go-driver" target="_blank">MongoDB</a>, etc.</p> <p>You can export traces and metrics to Prometheus, OpenZipkin, GCP Stackdriver Monitoring, Jaeger, AWS X-Ray, Datadog, Graphite, Honeycomb, or New Relic.</p> <h2 id="configuration">Configuration</h2> <p>Go&rsquo;s standard library doesn&rsquo;t support much more configuration options than the <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fflag" target="_blank">flag package</a>. Even if it&rsquo;s enough for simple CLI tools, you may need a bit more for building services.</p> <h3 id="env-variables">Env variables</h3> <h4 id="white_check_mark-caarlos0env-githubhttpsgithubcomcaarlos0env-docshttpspkggodevgithubcomcaarlos0env">&#x2705; caarlos0/env <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fcaarlos0%2Fenv" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fcaarlos0%2Fenv" target="_blank">[Docs]</a></h4> <p>This library should provide everything you need for configuration for most applications. Compared to the standard library, it does support loading envs to structs and setting env defaults. It helps to save a lot of boilerplate for bigger configurations. It also supports embedded structs, so you can compose bigger a configuration from independent components.</p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Use env variables for your services configuration</p> </div> <div class="notice-body"><p> <p>For most applications, environment variables should be good enough as configuration.</p> <p>Configuration is where you should keep secrets and things that differ between environments. If your configuration is massive and does not change often, it may be worth hardcoding it instead. It&rsquo;s much more pragmatic than having tens of never-changing configuration options.</p> </p></div> </div> <h4 id="multi-format-configuration">Multi-format configuration</h4> <h4 id="white_check_mark-koanf-githubhttpsgithubcomknadhkoanf-docshttpspkggodevgithubcomknadhkoanf">&#x2705; koanf <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fknadh%2Fkoanf" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fknadh%2Fkoanf" target="_blank">[Docs]</a></h4> <p>Koanf is an excellent tool if your project requires multiple configuration formats. It&rsquo;s often the case when you write tools that are used externally (for example, CLI tools).</p> <p>This is my most recent finding. Compared to other <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fknadh%2Fkoanf%23alternative-to-viper" target="_blank">more popular libraries</a>, <code>koanf</code> just does loading multi-format configuration right. Bonus points for a nice abstraction that allows extending parsing and loading.</p> <p>Koanf does support the most important configuration formats, like <code>json</code>, <code>yaml</code>, <code>dotenv</code>, env vars, or <code>hcl</code>. They can be loaded from the filesystem, flags, and multiple external sources like <code>s3</code>, <code>vault</code>, <code>etcd</code>, or <code>consul</code>.</p> <h2 id="building-cli">Building CLI</h2> <h3 id="building-cli-libraries">Building CLI libraries</h3> <h4 id="white_check_mark-urfavecli-githubhttpsgithubcomurfavecli-docshttpscliurfaveorg-exampleshttpscliurfaveorgv2examplesgreet">&#x2705; urfave/cli <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Furfave%2Fcli%2F" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fcli.urfave.org%2F" target="_blank">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fcli.urfave.org%2Fv2%2Fexamples%2Fgreet%2F" target="_blank">[Examples]</a></h4> <p>We like <code>urfave/cli</code> because of its simple interface and extensibility. We used it in multiple projects without any issues.</p> <p>Compared to other alternatives, it offers a big-enough feature set while keeping the library lightweight.</p> <h2 id="testing">Testing</h2> <h3 id="assertions">Assertions</h3> <h4 id="white_check_mark-testify-githubhttpsgithubcomstretchrtestify-docshttpspkggodevgithubcomstretchrtestify">&#x2705; testify <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fstretchr%2Ftestify" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fstretchr%2Ftestify" target="_blank">[Docs]</a></h4> <p><code>testify</code> became the standard assertion library, and I&rsquo;ve seen it in every project I worked on. It provides asserts for the most common cases and also some more complex. One of <code>testify</code>&rsquo;s key features are friendly messages for all failed asserts. It makes writing and debugging tests much faster.</p> <p>The library provides two ways of asserting:</p> <ul> <li><code>assert</code> from <code>github.com/stretchr/testify/assert</code> - the test continues after failure. You should use it when you want to see multiple errors (not just the first one). Works when called in a goroutine.</li> <li><code>require</code> from <code>github.com/stretchr/testify/require</code> - the test is interrupted after failure. You should use it when some critical condition was not met and continuing doesn&rsquo;t make any sense (for example: storing to database failed). Doesn&rsquo;t work when called in a goroutine.</li> </ul> <p>Some example asserts:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fstretchr%2Ftestify%2Fassert%23Equal" target="_blank">Equal</a> - good enough in most cases</li> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fstretchr%2Ftestify%2Fassert%23Eventually" target="_blank">Eventually</a> - useful for asserting asynchronous conditions</li> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fstretchr%2Ftestify%2Fassert%23ElementsMatch" target="_blank">ElementsMatch</a> - useful for unsorted slices</li> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fstretchr%2Ftestify%2Fassert%23WithinDuration" target="_blank">WithinDuration</a> - useful when comparing time that is not exactly equal</li> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fstretchr%2Ftestify%2Fassert%23ErrorIs" target="_blank">ErrorIs</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fstretchr%2Ftestify%2Fassert%23JSONEq" target="_blank">JSONEq</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fstretchr%2Ftestify%2Fassert%23Panics" target="_blank">Panics</a></li> </ul> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Use assert messages just if it is really needed</p> </div> <div class="notice-body"><p> <p>I&rsquo;ve seen people who obsessively write messages for all failed assert.</p> <p>For example:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">assert</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="mi">123</span><span class="p">,</span> <span class="mi">321</span><span class="p">,</span> <span class="s">&#34;123 is not equal to 321&#34;</span><span class="p">)</span> </span></span></code></pre></div><p>will give output:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error: Not equal: </span></span><span class="line"><span class="cl"> expected: 123 </span></span><span class="line"><span class="cl"> actual : 321 </span></span><span class="line"><span class="cl">Test: TestEqual </span></span><span class="line"><span class="cl">Messages: 123 is not equal to 321 </span></span></code></pre></div><p>As you can see, the message doesn&rsquo;t add anything more than testify would figure out. It can even be harmful because with time, you will need to spend a lot of time to keep the message up to date.</p> <p>In most cases, the message provided by testify will be good enough. If the test fails, the person who sees the failure will navigate to this test and will understand the reason from the surrounding code.</p> </p></div> </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Do not write basic asserts by hand</p> </div> <div class="notice-body"><p> <p>Many people advocate for writing all asserts by hand. It won&rsquo;t give you much advantage in the end.</p> <p><code>testify</code> is also very smart in showing the difference between the expected and actual value.</p> <p>For example:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">assert</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;foo bar baz&#34;</span><span class="p">),</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="s">&#34;foo bar 42&#34;</span><span class="p">))</span> </span></span></code></pre></div><p>prints:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"> Error: Not equal: </span></span><span class="line"><span class="cl"> expected: []byte{0x66, 0x6f, 0x6f, 0x20, 0x62, 0x61, 0x72, 0x20, 0x62, 0x61, 0x7a} </span></span><span class="line"><span class="cl"> actual : []byte{0x66, 0x6f, 0x6f, 0x20, 0x62, 0x61, 0x72, 0x20, 0x34, 0x32} </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> Diff: </span></span><span class="line"><span class="cl"> --- Expected </span></span><span class="line"><span class="cl"> +++ Actual </span></span><span class="line"><span class="cl"> @@ -1,3 +1,3 @@ </span></span><span class="line"><span class="cl"> -([]uint8) (len=11) { </span></span><span class="line"><span class="cl"> - 00000000 66 6f 6f 20 62 61 72 20 62 61 7a |foo bar baz| </span></span><span class="line"><span class="cl"> +([]uint8) (len=10) { </span></span><span class="line"><span class="cl"> + 00000000 66 6f 6f 20 62 61 72 20 34 32 |foo bar 42| </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> Test: TestEqual </span></span><span class="line"><span class="cl">--- FAIL: TestEqual (0.00s) </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">Expected :[]byte{0x66, 0x6f, 0x6f, 0x20, 0x62, 0x61, 0x72, 0x20, 0x62, 0x61, 0x7a} </span></span><span class="line"><span class="cl">Actual :[]byte{0x66, 0x6f, 0x6f, 0x20, 0x62, 0x61, 0x72, 0x20, 0x34, 0x32} </span></span></code></pre></div><p>It makes no sense to reinvent the wheel and write it from scratch.</p> </p></div> </div> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Do not use test suites from testify</p> </div> <div class="notice-body"><p> <p>Testify is an excellent library for assertions, but we don&rsquo;t recommend its test suites. They don&rsquo;t support parallel sub-tests. They may be fine for unit tests, but for integration/API/E2E tests <strong>it&rsquo;s a deal-breaker</strong>.</p> <p>The standard library can achieve most of the functionalities provided by testify&rsquo;s test suites. You can see specific examples in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-test-architecture%2F%23keeping-integration-tests-stable-and-fast" target="_blank">this article on testing microservices</a>.</p> </p></div> </div> <h4 id="white_check_mark-go-cmp-githubhttpsgithubcomgooglego-cmp-docshttpspkggodevgithubcomgooglego-cmp-examples-1httpsgithubcomgooglego-cmpblobmastercmpexample_testgo-examples-2httpsgithubcomgooglego-cmpblobmastercmpcmpoptsexample_testgo">&#x2705; go-cmp <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgoogle%2Fgo-cmp" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fgoogle%2Fgo-cmp" target="_blank">[Docs]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgoogle%2Fgo-cmp%2Fblob%2Fmaster%2Fcmp%2Fexample_test.go" target="_blank">[Examples 1]</a> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgoogle%2Fgo-cmp%2Fblob%2Fmaster%2Fcmp%2Fcmpopts%2Fexample_test.go" target="_blank">[Examples 2]</a></h4> <p>Sometimes, you must assert a complex struct in your tests skipping some fields. Or the struct contains fields that should be compared in a specific way. Or you need to ignore the slice order or time delta. It&rsquo;s where <code>go-cmp</code> can help you!</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/google/go-cmp/cmp&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/google/go-cmp/cmp/cmpopts&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">diff</span> <span class="o">:=</span> <span class="nx">cmp</span><span class="p">.</span><span class="nf">Diff</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">want</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">got</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="c1">// FieldToIgnore and AnotherFieldToIgnore will be ignored in SomeStruct </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">cmpopts</span><span class="p">.</span><span class="nf">IgnoreFields</span><span class="p">(</span><span class="nx">SomeStruct</span><span class="p">{},</span> <span class="s">&#34;FieldToIgnore&#34;</span><span class="p">,</span> <span class="s">&#34;AnotherFieldToIgnore&#34;</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// when comparing time, truncate it to one second </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// can be written for any type </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">opt</span> <span class="o">:=</span> <span class="nx">cmp</span><span class="p">.</span><span class="nf">Comparer</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">x</span><span class="p">,</span> <span class="nx">y</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">x</span><span class="p">.</span><span class="nf">Truncate</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">).</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">y</span><span class="p">.</span><span class="nf">Truncate</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// sort all []int </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">cmpopts</span><span class="p">.</span><span class="nf">SortSlices</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">x</span><span class="p">,</span> <span class="nx">y</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">x</span> <span class="p">&lt;</span> <span class="nx">y</span> </span></span><span class="line"><span class="cl"> <span class="p">}))</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// cmp returns diff if two objects are different </span></span></span><span class="line"><span class="cl"><span class="c1">// to check if objects are equal, you can assert if the diff is empty </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">assert</span><span class="p">.</span><span class="nf">Empty</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">diff</span><span class="p">)</span> </span></span></code></pre></div><p>To see the list of all available options, I recommend checking the godoc of <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fgoogle%2Fgo-cmp%2Fcmp" target="_blank"><code>cmp</code></a> and <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fgoogle%2Fgo-cmp%2Fcmp%2Fcmpopts" target="_blank"><code>cmpopts</code></a> package.</p> <p>go-cmp can also be used outside of tests, but be careful &ndash; it&rsquo;s another tool that, used irresponsibly, may hurt your project.</p> <h4 id="white_check_mark-gofakeit-githubhttpsgithubcombrianvoegofakeit-docshttpspkggodevgithubcombrianvoegofakeit">&#x2705; gofakeit <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fbrianvoe%2Fgofakeit" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fbrianvoe%2Fgofakeit" target="_blank">[Docs]</a></h4> <p>If you need more realistic data for your tests, <code>gofakeit</code> helps.</p> <h3 id="mocking">Mocking</h3> <h4 id="writing-mocks-by-hand">Writing mocks by hand</h4> <p><em>Initially, I recommended one popular mocking tool here. But after some thinking, we decided that the tool is not good enough to recommend. Instead, consider an alternative mocking strategy &#x1f447;</em></p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Consider writing mocks by hand</p> </div> <div class="notice-body"><p> <p>Even if it sounds like a waste of time, writing mocks yourself may be good enough. Objectively speaking, writing them by hand doesn&rsquo;t require much more code and time. As a bonus, it gives you much more flexibility.</p> <p>This is how an example mock can look like:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">BalanceUpdate</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserID</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">AmountChange</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">UserServiceMock</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">BalanceUpdates</span> <span class="p">[]</span><span class="nx">BalanceUpdate</span> </span></span><span class="line"><span class="cl"> <span class="nx">balanceUpdatesLock</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">u</span> <span class="o">*</span><span class="nx">UserServiceMock</span><span class="p">)</span> <span class="nf">UpdateTrainingBalance</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">amountChange</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">u</span><span class="p">.</span><span class="nx">balanceUpdatesLock</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="nx">u</span><span class="p">.</span><span class="nx">balanceUpdatesLock</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">u</span><span class="p">.</span><span class="nx">BalanceUpdates</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">u</span><span class="p">.</span><span class="nx">BalanceUpdates</span><span class="p">,</span> <span class="nx">BalanceUpdate</span><span class="p">{</span><span class="nx">userID</span><span class="p">,</span> <span class="nx">amountChange</span><span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>It took me literally 1 minute to write it.</p> </p></div> </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Keep your interfaces small, so it's easier to mock them</p> </div> <div class="notice-body"><p> <p>It&rsquo;s hard to mock complex types by hand. But if your interface is so complex you can&rsquo;t write a mock for it, you should reconsider if it needs to be that big. Using mocking libraries obfuscates the real problem.</p> <p>Try to simplify the type that you are mocking. Maybe <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FInterface_segregation_principle" target="_blank">the interface segregation principle</a> will help? It could be possible to split this type into multiple smaller types.</p> <p>It will not only simplify your mocks but will improve your codebase.</p> </p></div> </div> <h2 id="misc">Misc</h2> <h3 id="extra-types-support">Extra types support</h3> <h4 id="white_check_mark-googleuuid-githubhttpsgithubcomgoogleuuid-docshttpspkggodevgithubcomgoogleuuid">&#x2705; google/uuid <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgoogle%2Fuuid" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fgoogle%2Fuuid" target="_blank">[Docs]</a></h4> <p>This library generates UUIDs.</p> <h4 id="white_check_mark-oklogulid-githubhttpsgithubcomoklogulid-docshttpspkggodevgithubcomoklogulid">&#x2705; oklog/ulid <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Foklog%2Fulid" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Foklog%2Fulid" target="_blank">[Docs]</a></h4> <p>UUIDs <a href="proxy.php?url=https%3A%2F%2Fwww.percona.com%2Fblog%2F2014%2F12%2F19%2Fstore-uuid-optimized-way%2F" target="_blank">may be slow to store</a> at a larger scale in relational databases. A solution may be using Universally Unique Lexicographically Sortable Identifier: ULIDs. ULIDs are compatible with UUIDs, are unique enough for large scale, and have shorter string representation (Crockford&rsquo;s base32). ULIDs are lexicographically sortable, thanks to what building indexes should be much faster.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> It&rsquo;s worth mentioning that UUID v6, v7, and v8 will also be lexicographically sortable. But its spec is still draft when during the release of the article. If you want to try UUID v6 or v7, you can check <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgofrs%2Fuuid%2Fblob%2Fe1079f31cfcadf78856b9866d15574dd6546e29b%2Fuuid.go%23L66" target="_blank">github.com/gofrs/uuid</a> which does already implement them. </p></div> </div> <h4 id="white_check_mark-shopspringdecimal-githubhttpsgithubcomshopspringdecimal-docshttpspkggodevgithubcomshopspringdecimal">&#x2705; shopspring/decimal <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fshopspring%2Fdecimal" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fshopspring%2Fdecimal" target="_blank">[Docs]</a></h4> <p>Go doesn&rsquo;t have built-in support for decimals. <code>shopspring/decimal</code> does the job. We have used this library for a couple of years to build a large financial system.</p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Use decimals for monetary values</p> </div> <div class="notice-body"><p> <p>Floats are not designed to accurately store decimal numbers.</p> <p>For example:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">fmt</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;%.16f&#34;</span><span class="p">,</span> <span class="mf">12.1</span><span class="o">+</span><span class="mf">0.03</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="p">&gt;</span> <span class="nx">Output</span><span class="p">:</span> <span class="mf">12.1300000000000008</span> </span></span></code></pre></div><p>To make sure your money calculations are correct (and you are not losing or getting extra cents in calculations), we recommend using a decimal type.</p> <p>It&rsquo;s also a good idea to use the string representation of decimals instead of floats in transport (in events, API requests and responses, etc.).</p> </p></div> </div> <h3 id="errors">Errors</h3> <h4 id="white_check_mark-hashicorpgo-multierror-githubhttpsgithubcomhashicorpgo-multierror-docsgithubcomhashicorpgo-multierror">&#x2705; hashicorp/go-multierror <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fhashicorp%2Fgo-multierror" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgithub.com%2Fhashicorp%2Fgo-multierror">[Docs]</a></h4> <p>Did you ever need to handle an error while you were handling another error? <code>hashicorp/go-multierror</code> is here to help you!</p> <p>It&rsquo;s also helpful if an operation can return multiple errors, and you don&rsquo;t want to return just the first one (for example, validation).</p> <p>Example use cases:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">validate</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">resultErr</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">validateFoo</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">resultErr</span> <span class="p">=</span> <span class="nx">multierror</span><span class="p">.</span><span class="nf">Append</span><span class="p">(</span><span class="nx">resultErr</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">validateBar</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">resultErr</span> <span class="p">=</span> <span class="nx">multierror</span><span class="p">.</span><span class="nf">Append</span><span class="p">(</span><span class="nx">resultErr</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">resultErr</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>or</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">ExecuteStuff</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">makeStuff</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">cleanupErr</span> <span class="o">:=</span> <span class="nf">cleanup</span><span class="p">();</span> <span class="nx">cleanupErr</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">multierror</span><span class="p">.</span><span class="nf">Append</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">cleanupErr</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><em>Note: Go 1.20 <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgolang%2Fgo%2Fissues%2F53435" target="_blank">will introduce</a> <code>errors.Join</code> function. After release of Go 1.20 you should consider using it instead.</em></p> <h2 id="useful-tools">Useful tools</h2> <h3 id="misc-1">Misc</h3> <h4 id="white_check_mark-samberlo-githubhttpsgithubcomsamberlo-docshttpspkggodevgithubcomsamberlo">&#x2705; samber/lo <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsamber%2Flo" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsamber%2Flo" target="_blank">[Docs]</a></h4> <p>Lodash-style Go library based on Go 1.18+ Generics. It may be especially useful for you if you are coming to Go from Python and missing some basic slice/map functions.</p> <p>Some functions that I&rsquo;m using the most:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsamber%2Flo%23Filter" target="_blank">Filter</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsamber%2Flo%23Map" target="_blank">Map</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsamber%2Flo%23Keys" target="_blank">Keys</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsamber%2Flo%23Values" target="_blank">Values</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsamber%2Flo%23Find" target="_blank">Find</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsamber%2Flo%23Max" target="_blank">Max</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fsamber%2Flo%23Must" target="_blank">Must</a> &#x1f608; please use it just for tests or if you really have a good reason</li> </ul> <p>Even if some may find it &ldquo;non-idiomatic&rdquo;, I find it useful in some cases. It&rsquo;s similar to using an <a href="#sql">ORM</a> &ndash; as long as such libraries are used responsibly and don&rsquo;t obfuscate code, they are useful.</p> <p>So if you find yourself writing code like:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">lo</span><span class="p">.</span><span class="nf">Map</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">lo</span><span class="p">.</span><span class="nf">Filter</span><span class="p">(</span><span class="nx">someSlice</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">v</span> <span class="nx">SomeType</span><span class="p">,</span> <span class="nx">_</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">bool</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">v</span><span class="p">.</span><span class="nx">IsSpecial</span> </span></span><span class="line"><span class="cl"> <span class="p">}),</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="nx">SomeType</span><span class="p">,</span> <span class="nx">_</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">t</span><span class="p">.</span><span class="nf">SpecialName</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><p>&hellip;it&rsquo;s just better to convert it to a simple, more readable loop. &#x1f609;</p> <h4 id="white_check_mark-task-githubhttpsgithubcomgo-tasktask-docshttpstaskfiledev">&#x2705; Task <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-task%2Ftask" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Ftaskfile.dev%2F" target="_blank">[Docs]</a></h4> <p>Task is not really a Go library, but it&rsquo;s a tool written in Go that may be useful for your projects.</p> <p>It&rsquo;s an excellent alternative to Makefile. The most important features that it offers are:</p> <ul> <li>Parallel tasks execution (supported by <a href="proxy.php?url=https%3A%2F%2Ftaskfile.dev%2Fusage%2F%23task-dependencies" target="_blank">task dependencies</a>)</li> <li>Preventing <a href="proxy.php?url=https%3A%2F%2Ftaskfile.dev%2Fusage%2F%23prevent-unnecessary-work" target="_blank">unnecessary work</a></li> <li><a href="proxy.php?url=https%3A%2F%2Ftaskfile.dev%2Fusage%2F%23env-files" target="_blank">Loading .env</a></li> <li><a href="proxy.php?url=https%3A%2F%2Ftaskfile.dev%2Fusage%2F%23dynamic-variables" target="_blank">Dynamic variables</a></li> <li><a href="proxy.php?url=https%3A%2F%2Ftaskfile.dev%2Fusage%2F%23forwarding-cli-arguments-to-commands" target="_blank">Forwarding CLI arguments</a></li> <li><a href="proxy.php?url=https%3A%2F%2Ftaskfile.dev%2Fusage%2F%23gos-template-engine" target="_blank">Templating</a></li> </ul> <p>It&rsquo;s a must-have for each of my new projects.</p> <h3 id="live-code-reloading">Live code reloading</h3> <h4 id="white_check_mark-reflex-githubhttpsgithubcomcesparereflex-docshttpspkggodevgithubcomcesparereflex-examplehttpsthreedotstechpostgo-docker-dev-environment-with-go-modules-and-live-code-reloading">&#x2705; reflex <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fcespare%2Freflex" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Fcespare%2Freflex" target="_blank">[Docs]</a> [<a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-docker-dev-environment-with-go-modules-and-live-code-reloading%2F" target="_blank">Example</a>]</h4> <p>Go doesn&rsquo;t provide code live-reloading out of the box. But you can achieve it quickly with the <code>reflex</code> library.</p> <p>Some time ago, Miłosz wrote an article that shows how to create a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-docker-dev-environment-with-go-modules-and-live-code-reloading%2F">local environment with Docker and reflex</a>.</p> <h3 id="linter">Linter</h3> <h4 id="white_check_mark-golangci-lint-githubhttpsgithubcomgolangcigolangci-lint-docshttpsgolangci-lintrun">&#x2705; golangci-lint <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgolangci%2Fgolangci-lint" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fgolangci-lint.run%2F" target="_blank">[Docs]</a></h4> <p>golangci-lint is a linter that aggregates multiple linters and runs them in parallel and does it very fast.</p> <p>Here&rsquo;s <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fb519c611e9d1248a149c89db9bcf879fd78b1e35%2Finternal%2Ftrainer%2F.golangci.yml" target="_blank">an example configuration</a> that we use in our projects.</p> <h4 id="white_check_mark-go-cleanarch-githubhttpsgithubcomroblaszczakgo-cleanarch-docshttpspkggodevgithubcomroblaszczakgo-cleanarchsection-readme">&#x2705; go-cleanarch <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fgo-cleanarch" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgithub.com%2Froblaszczak%2Fgo-cleanarch%23section-readme" target="_blank">[Docs]</a></h4> <p>If you use <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F" target="_blank">Clean/Hexagonal Architecture</a> in your project, you can use this linter to ensure that The Dependency Inversion Rule and interaction between packages are kept.</p> <h3 id="formatters">Formatters</h3> <h4 id="white_check_mark-go-fmt">&#x2705; go fmt</h4> <p>The standard formatter provided by Go toolchain.</p> <h4 id="white_check_mark-goimports-docshttpspkggodevgolangorgxtoolscmdgoimports">&#x2705; goimports <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fgolang.org%2Fx%2Ftools%2Fcmd%2Fgoimports" target="_blank">[Docs]</a></h4> <p>Goimports does all that <code>go fmt</code> does, but it also sorts imports of your Go files. It&rsquo;s one of the tools that you will see widely adopted in most Go projects.</p> <p>Not everybody knows, but you can also separately group your local imports with the <code>-local</code> flag.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">goimports -local <span class="s2">&#34;github.com/ThreeDotsLabs/some-repository&#34;</span> -l -w . </span></span></code></pre></div><h4 id="white_check_mark-gofumpt-githubhttpsgithubcommvdangofumpt-docshttpspkggodevmvdanccgofumptsection-readme">&#x2705; gofumpt <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmvdan%2Fgofumpt" target="_blank">[GitHub]</a> <a href="proxy.php?url=https%3A%2F%2Fpkg.go.dev%2Fmvdan.cc%2Fgofumpt%23section-readme" target="_blank">[Docs]</a></h4> <p>Just for the biggest formatting freaks! Does all that <code>go fmt</code> and <code>goimports</code> do and more!</p> <p>Personally, I like gofumpt&rsquo;s formatting decisions.</p> <h2 id="example-projects">Example projects</h2> <h3 id="ddd--clean-architecture">DDD &amp; Clean Architecture</h3> <h4 id="white_check_mark-wild-workouts-go-ddd-example-application-githubhttpsgithubcomthreedotslabswild-workouts-go-ddd-example">&#x2705; Wild Workouts Go DDD Example application <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">[GitHub]</a></h4> <p><strong>Wild Workouts is an example Go DDD project that we created to show how to build Go applications that are easy to develop, maintain, and fun to work with, It shows a project developed over time and with complex problems to solve.</strong> In contrast to other example projects, it was not blindly copied from other languages.</p> <p>This is the way how we build our services daily. Highly recommended if you are looking for patterns that will allow you to build more complex projects!</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Low-quality example repositories</p> </div> <div class="notice-body"><p> <p>Avoid projects that look like over-engineered copies from other programming languages.</p> <p>People who write such &ldquo;DDD&rdquo; projects often just read a couple of articles about it without understanding it correctly and without using it in real-life projects. If you see DDD/Clean Architecture examples without encapsulated domain models (with public fields) and <code>json</code> tags: run! It&rsquo;s definitely not DDD nor Clean Architecture.</p> </p></div> </div> <h3 id="general-purpose">General purpose</h3> <h4 id="white_check_mark-modern-go-application-by-márk-sági-kazár-githubhttpsgithubcomsagikazarmarkmodern-go-application">&#x2705; Modern Go Application by Márk Sági-Kazár <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fsagikazarmark%2Fmodern-go-application" target="_blank">[GitHub]</a></h4> <p>Another example repository that we can recommend. It doesn&rsquo;t cover patterns like DDD or Clean Architecture but emphasizes infrastructure beats like observability.</p> <h2 id="summary">Summary</h2> <p>Should we check some library that is not listed here? Please let us know in the <a href="#disqus_thread">comments</a>!</p>The Best Go framework: no framework?https://threedots.tech/post/best-go-framework/Tue, 29 Nov 2022 00:00:00 +0100https://threedots.tech/post/best-go-framework/<p>While writing this blog and leading Go teams for a couple of years, the most common question I heard from beginners was <strong>&ldquo;What framework should I use?&rdquo;.</strong> One of the worst things you can do in Go is follow an approach from other programming languages.</p> <p>Other languages have established, &ldquo;default&rdquo; frameworks. Java has Spring, Python has Django and Flask, Ruby has Rails, C# has ASP.NET, Node has Express, and PHP has Symfony and Laravel. <strong>Go is different: there is no default framework</strong>.</p> <p>What&rsquo;s even more interesting, many suggest you shouldn&rsquo;t use a framework at all. Are they insane?</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbest-go-framework%2Ffrankenstein-gopher.svg" alt="Frankenstein Gopher" onerror="this.onerror='null';this.src=''" /> <div class="code-ref"> Building Go service from libraries may give a feeling of building Frankenstein’s Monster </div></p> <h3 id="the-philosophy-of-go">The Philosophy of Go</h3> <p>Go frameworks exist, but none provides a feature set like frameworks from other languages. <strong>This won&rsquo;t change soon.</strong></p> <p>You may think that it&rsquo;s because the Go ecosystem is younger. But there is a more important factor. Go is built around the Unix Philosophy that says:</p> <blockquote> <ul> <li>Write programs that do one thing and do it well.</li> <li>Write programs to work together.</li> <li>Write programs to handle text streams because that is a universal interface.</li> </ul> </blockquote> <p>This philosophy originated from Ken Thompson, the designer of the B programming language (a precursor of C) and also&hellip; Go!</p> <p>In practice, <strong>the Unix philosophy favors building small independent pieces of software that do one thing well rather than big chunks that do everything.</strong> You may have seen it in your terminal. For example: <code>cat example.txt | sort | uniq</code>. <code>cat</code> reads the text from the file, <code>sort</code> sorts the lines, and <code>uniq</code> removes the duplicates. All commands are independent and do one thing. It comes directly from the Unix philosophy. Thanks to such design, you can independently develop much smaller, autonomous commands.</p> <p>In Go, the Unix philosophy is visible in the standard library. The best examples are the most widespread interfaces: the <code>io.Reader</code> and the <code>io.Writer</code>. The best libraries follow this philosophy as well.</p> <p>Frameworks are designed against this philosophy. Often, they try to cover all possible use cases within one framework. They are not designed to work with other tools and often can&rsquo;t be reused. It means that it&rsquo;s not possible to transfer development efforts to other non-compatible frameworks. If the framework adoption is low (or it just dies), all the effort is lost.</p> <div class="text-center"> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F%3Futm_source%3Dblog-content"> <img src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fsidebar%2Fcourse.svg" loading="lazy" decoding="async" alt="Go In One Evening" class=" img" width="500" height="500" /> </a> <span class="h5 inline-block"> Are you experienced engineer who wants to learn Go basics?<br> You don't become an engineer by watching videos. <br> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-in-one-evening%2F%3Futm_source%3Dblog-content"> Learn Go hands-on by building real projects. </a> </span> </div> <h3 id="what-is-important-for-you">What is important for you</h3> <p>Every technical decision has tradeoffs. You need to choose tradeoffs that make more sense for you and your project.</p> <p><strong>Some approaches make sense when you work on a one-weekend proof of concept and throw it away (you will, right?). In that case, the most critical factor is how quick you can finish it. But if you work on a project that will last a long time and you build it with multiple people, the impact of that decision is tremendous.</strong></p> <p>For most projects, the most important parameters are:</p> <ul> <li><strong>how fast can you start the project,</strong></li> <li><strong>how fast will you be able to develop this project in the long term,</strong></li> <li><strong>how flexible for future changes the project will be (it&rsquo;s strongly connected with the previous point).</strong></li> </ul> <p>Let&rsquo;s start assessing our decisions.</p> <h3 id="time-saving">Time saving</h3> <p>One of the biggest promises given by frameworks is time saving. You run one command, and you get a fully functional project. Frameworks usually provide an opinionated structure of the project, and it helps if you don&rsquo;t know how to do it. But as with most other technical decisions, it&rsquo;s not free.</p> <p>With time, when the project grows, you&rsquo;ll quickly hit the framework&rsquo;s wall of conventions and limitations. The framework&rsquo;s author requirements were likely different from yours. The decision taken by the framework creator may work nicely for simple CRUD applications but may not handle more complex scenarios. <strong>It&rsquo;s easy to quickly lose all your time saved on the project bootstrap just to fight with one framework limitation.</strong> Over time, it can lead to a lot of frustration in your team.</p> <p>A couple of years ago, I worked in a company that initially started with one Go framework (I&rsquo;ll skip the name of the framework). The company was growing and creating new services. With time, we started to feel more pain when we wanted to support more complex use cases. It was also a source of severe bugs. Unfortunately, getting rid of the framework wasn&rsquo;t easy.</p> <p>At one point, some framework components became unmaintained and incompatible with the rest of the ecosystem. We were forced to get rid of it. The framework has already become very tightly coupled to the entire system. Removing it from tens of services was a non-trivial task. It required a cross-team initiative that took multiple person-months and a few incidents to get rid of. Even if the project was successful in the end, I didn&rsquo;t see it in such a way. <strong>All the time spent could be used in a much better way if someone took a different decision earlier. The entire project would not be needed. It&rsquo;s not surprising that many companies suffer from the lack of trust in the development team.</strong></p> <p>It&rsquo;s a great example how such a small decision can become a costly rescue project after a couple of years.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1234" height="954" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbest-go-framework%2Fchart_hu3307ff9a967d2af14049e194f5be1131_111235_1234x954_resize_q80_h2_lanczos.webp" alt="Chart" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fbest-go-framework%5C%2Fchart_hu3307ff9a967d2af14049e194f5be1131_111235_1234x954_resize_q80_lanczos.jpg"" /> <h3 id="maintainability-of-the-project">Maintainability of the project</h3> <p>Measuring the maintainability of projects is a controversial topic — it&rsquo;s hard to compare two projects. Some people say that frameworks are great and they don&rsquo;t feel the pain of using them. For others, frameworks can be the biggest nightmare in the long term. Some projects are much more challenging than others. Many people think that fighting with the framework is just a part of the job. That&rsquo;s why it&rsquo;s hard to objectively measure the impact of frameworks on the project&rsquo;s maintainability.</p> <p>Fortunately, we can help ourselves understand it with a bit of science. More precisely, with the excellent <em>Accelerate: The Science of Lean Software and DevOps</em> book based on scientific research. The book is focused on finding the characteristics of the best and worst-performing teams. What&rsquo;s important for us, <strong>one of the most significant factors for good performance is loosely-coupled architecture.</strong></p> <p>The teams I led often asked me how to know if our architecture is loosely coupled. One of the simplest ways is ensuring that it&rsquo;s easy to replace or delete parts of your application. <strong>If it&rsquo;s hard to remove parts of your application, your application is tightly coupled. Touching one thing stars a domino effect of changes in different places.</strong></p> <p>Why is the <em>loosely coupled architecture</em> that important? Let&rsquo;s admit it. We are humans, and even after the best research, we make mistakes. When you choose the wrong framework or library, it should be easy to replace it without rewriting the entire project. If we want to save time, we should think what helps in the long term, not only at the beginning of the project.</p> <p>Consider a scenario when you want to remove the framework entirely. Will it require a lot of code rewrites? Can it be done on multiple services independently? If not, you could put some effort to separate the framework from your core logic. But it requires sacrificing the &ldquo;time-saving&rdquo; it gives in the first place.</p> <h3 id="the-alternative-building-services-without-a-framework">The alternative? Building services without a framework</h3> <p>You may feel that building your services without a framework will take ages. Especially if you are coming from other programming languages. I understand that. I had the same feeling a couple of years ago when I started writing in Go. It was an unjustified fear. Not using a framework doesn&rsquo;t mean that you will need to build everything yourself. There are many proven libraries that provide the functionality you need.</p> <p>You need to put a bit more effort into research. You&rsquo;re reading this, so you&rsquo;re already doing that! Even a couple hours of research are nothing in the entire project&rsquo;s lifetime. You&rsquo;ll also get that time back very soon thanks to the flexibility it gives you.</p> <p>What should you do if you decide not to use a framework? The biggest blocker at the beginning may be how you build a service. <strong>The easiest way is to start by putting everything into one file. You can start simple, defer some decisions, and evolve your project with time.</strong></p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/best-go-framework/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="frameworks;basics;web-applications;anti-patterns;go;golang"> <input type="hidden" name="fields[blog_series]" value=""> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <p>It&rsquo;s helpful to have example projects you can use as a reference. You can take a look at the project I used for my <em>Let&rsquo;s build event-driven application in 15 minutes with Watermill</em> presentation at <a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D6Zgi5nUPf70" target="_blank">GoRemoteFest</a> &ndash; <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fgoremotefest-livecoding" target="_blank">github.com/roblaszczak/goremotefest-livecoding</a>. This example needed just two external libraries to make it functional.</p> <p>Feel free to clone this repository and adopt to your needs. I&rsquo;m sure this example doesn&rsquo;t have all libraries required by your project. <strong>To help you with more specific use-cases, we wrote a next article with a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flist-of-recommended-libraries%2F">list of Go libraries you can use to build your Go services</a>. We have been using them for a couple of years. We are explaining why we use those libraries and how to know if a similar library is good or bad.</strong></p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="2500" height="2500" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimages%2Fcovers%2Fbest-go-framework_hu573c689b8d4828c7a2a2c14ae7715a36_176706_2500x2500_resize_q80_h2_lanczos_3.webp" alt="Frankenstein Gopher" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fimages%5C%2Fcovers%5C%2Fbest-go-framework_hu573c689b8d4828c7a2a2c14ae7715a36_176706_2500x2500_resize_lanczos_3.png"" /> <div class="code-ref"> It's practical to change parts of your project without killing it. </div></p> <p>When your project becomes more complex, and you already know how your libraries work together, you can start to refactor it. In the end, you may not need most of the framework features that seemed critical. Thanks to that, you can end up with a simpler project and less research.</p> <p>If you are looking for a reference on how more advanced projects can look like, you should check <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">Wild Workouts</a> &ndash; our fully functional example Go project. We released a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F">+200 pages free e-book</a> describing how we built that application.</p> <h3 id="summary">Summary</h3> <p>Deciding how you build services is not where you should take shortcuts. Making a wrong decision may have a very negative impact on your time in the long term. It negatively affects your team&rsquo;s velocity and, more importantly, morale.</p> <p>After making a wrong decision, you can quickly fall into the sunk cost fallacy trap. Instead of becoming heroes who solve problems they created, we should avoid creating them.</p> <p>After that article&rsquo;s lecture, you should know each way&rsquo;s tradeoffs and implications. You can now make a responsible decision. I hope this article will help avoid at least one company a couple-man-months painful refactoring project.</p> <p>Do you have any framework horror stories (even from a different programming language)? Let us know in the comments!</p>Increasing Cohesion in Go with Generic Decoratorshttps://threedots.tech/post/increasing-cohesion-in-go-with-generic-decorators/Wed, 25 May 2022 00:00:00 +0200https://threedots.tech/post/increasing-cohesion-in-go-with-generic-decorators/<p>Cohesion is part of the <em>low coupling, high cohesion</em> principle that&rsquo;s supposed to keep your code maintainable. While <em>low coupling</em> means <em>few dependencies</em>, <em>high cohesion</em> roughly translates to <em>single responsibility</em>. Highly cohesive code (a module or a function) is focused on a single purpose. Low cohesion means it does many unrelated things.</p> <p>I&rsquo;ve written about coupling in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcommon-anti-patterns-in-go-web-applications%2F" target="_blank">the previous article on anti-patterns</a>. Here are some tips on increasing cohesion in Go applications using the recently released generics.</p> <h2 id="the-application-logic">The Application Logic</h2> <p>There’s a place in your project where your application logic lives. We call it the <em>application layer</em> and split it into commands and queries, CQRS style. Perhaps you call it handlers, services, controllers, or use cases. Hopefully, it’s not mixed with implementation details, like HTTP handlers.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If you don&rsquo;t keep your logic separate yet, check our articles on <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F" target="_blank">Clean Architecture</a> and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbasic-cqrs-in-go%2F" target="_blank">CQRS</a>. </p></div> </div> <p>Whatever you call this code, it&rsquo;s probably not only logic. Some common operations need to happen for each request your application handles. They&rsquo;re often related to observability, like logging, metrics, and tracing. Other examples are retries, timeouts, or an authorization mechanism.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="563" height="506" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fincreasing-cohesion-in-go-with-generic-decorators%2Fhandler_hu1cd31c5f19d6a8f1bcc1a6f605658987_117711_563x506_resize_q80_h2_lanczos_3.webp" alt="Handler" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fincreasing-cohesion-in-go-with-generic-decorators%5C%2Fhandler_hu1cd31c5f19d6a8f1bcc1a6f605658987_117711_563x506_resize_lanczos_3.png"" /> <div class="code-ref"> Code sections in a typical handler. </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">SubscribeHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">Subscribe</span><span class="p">)</span> <span class="p">(</span><span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">start</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nx">h</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Subscribing to newsletter&#34;</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">end</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Since</span><span class="p">(</span><span class="nx">start</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">h</span><span class="p">.</span><span class="nx">metricsClient</span><span class="p">.</span><span class="nf">Inc</span><span class="p">(</span><span class="s">&#34;commands.subscribe.duration&#34;</span><span class="p">,</span> <span class="nb">int</span><span class="p">(</span><span class="nx">end</span><span class="p">.</span><span class="nf">Seconds</span><span class="p">()))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">h</span><span class="p">.</span><span class="nx">metricsClient</span><span class="p">.</span><span class="nf">Inc</span><span class="p">(</span><span class="s">&#34;commands.subscribe.success&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">h</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Subscribed to newsletter&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">h</span><span class="p">.</span><span class="nx">metricsClient</span><span class="p">.</span><span class="nf">Inc</span><span class="p">(</span><span class="s">&#34;commands.subscribe.failure&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">h</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Failed subscribing to newsletter:&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F01-low-cohesion%2Fsubscribe.go%23L34" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/03-cohesion/01-low-cohesion/subscribe.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F01-low-cohesion%2Fsubscribe.go%23L34" target="_blank">Full source</a> </div> <p><strong>Keeping such operations close to the logic decreases the cohesion. It mixes different concepts.</strong></p> <p>Eventually, it gets hard to grasp the logic itself. Even if you move some code to helper functions, you&rsquo;ll end up passing many arguments and handling errors. You risk that someone copy-pastes the entire <em>header</em> of the function and forgets to change one of the fields. Updating the supporting code requires changing the logic parts.</p> <h2 id="the-user-context">The User Context</h2> <p>An interesting scenario is when you need to handle requests differently depending on the caller.</p> <p>A typical example is checking the user&rsquo;s permissions or attributes. You need to pass some kind of user context to the handler — a token, a user ID, or a complete <code>User</code> object. The logic can&rsquo;t proceed without it.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="2649" height="612" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fincreasing-cohesion-in-go-with-generic-decorators%2Fauth-middleware_huba1a447e8dc7ecf96ea735760e679748_331020_2649x612_resize_q80_h2_lanczos_3.webp" alt="Authorization middleware" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fincreasing-cohesion-in-go-with-generic-decorators%5C%2Fauth-middleware_huba1a447e8dc7ecf96ea735760e679748_331020_2649x612_resize_lanczos_3.png"" /> <div class="code-ref"> A middleware transforming an authorization token into a user context. </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">UserFromContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">!</span><span class="nx">user</span><span class="p">.</span><span class="nx">Active</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;the user&#39;s account is not active&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F01-low-cohesion%2Fsubscribe.go%23L50" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/03-cohesion/01-low-cohesion/subscribe.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F01-low-cohesion%2Fsubscribe.go%23L50" target="_blank">Full source</a> </div> <p>But often, there are a few entry points to do the same action. For example, in a system subscribing users to a newsletter, we could have:</p> <ul> <li>A public HTTP API that uses the user context coming from a session token.</li> <li>An internal gRPC API that other services call. An admin CLI tool also uses it.</li> <li>A message handler that reacts to an event that already happened (a user signed up who agreed to receive a newsletter).</li> <li>A migration that runs on the service&rsquo;s startup. It backfills subscriptions that weren&rsquo;t there because of a bug.</li> </ul> <p>Except for the first entry point, all others miss a proper user context the logic could use</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="872" height="456" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fincreasing-cohesion-in-go-with-generic-decorators%2Fports_hu4c4a5c8ea644955d0c64ab8b3398e423_76157_872x456_resize_q80_h2_lanczos_3.webp" alt="Ports" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fincreasing-cohesion-in-go-with-generic-decorators%5C%2Fports_hu4c4a5c8ea644955d0c64ab8b3398e423_76157_872x456_resize_lanczos_3.png"" /> <div class="code-ref"> Multiple entry points to the same command handler. </div> <p>You must manually craft and pass a <em>fake</em> user context for entry points other than the HTTP API.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">fakeUser</span> <span class="o">:=</span> <span class="nx">User</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;1&#34;</span><span class="p">,</span> <span class="c1">// Missing ID in the context, let&#39;s assume it&#39;s the root user making changes </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">Active</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">ctx</span> <span class="p">=</span> <span class="nf">ContextWithUser</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">fakeUser</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">err</span> <span class="o">:=</span> <span class="nx">handler</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F01-low-cohesion%2Fmain.go%23L64" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/03-cohesion/01-low-cohesion/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F01-low-cohesion%2Fmain.go%23L64" target="_blank">Full source</a> </div> <p>Even if your application exposes only a public HTTP API, testing this function isn&rsquo;t a good experience. You need to prepare a fake user every time you call the handler or mock it somewhere in the dependencies.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestSubscribe</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span> <span class="o">:=</span> <span class="nx">log</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">os</span><span class="p">.</span><span class="nx">Stdout</span><span class="p">,</span> <span class="s">&#34;&#34;</span><span class="p">,</span> <span class="nx">log</span><span class="p">.</span><span class="nx">LstdFlags</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">metricsClient</span> <span class="o">:=</span> <span class="nx">nopMetricsClient</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">handler</span> <span class="o">:=</span> <span class="nf">NewSubscribeHandler</span><span class="p">(</span><span class="nx">logger</span><span class="p">,</span> <span class="nx">metricsClient</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">user</span> <span class="o">:=</span> <span class="nx">User</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span><span class="p">:</span> <span class="s">&#34;1000&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Active</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="o">:=</span> <span class="nf">ContextWithUser</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="nx">user</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span> <span class="o">:=</span> <span class="nx">Subscribe</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Email</span><span class="p">:</span> <span class="s">&#34;[email protected]&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">NewsletterID</span><span class="p">:</span> <span class="s">&#34;product-news&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">handler</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F01-low-cohesion%2Fsubscribe_test.go%23L10" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/03-cohesion/01-low-cohesion/subscribe_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F01-low-cohesion%2Fsubscribe_test.go%23L10" target="_blank">Full source</a> </div> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Low cohesion</p> </div> <div class="notice-body"><p> Don&rsquo;t mix your application&rsquo;s logic with other code directly in a handler. </p></div> </div> <h2 id="the-decorator-pattern">The Decorator Pattern</h2> <p>Decorators are one of the least controversial design patterns. After all, HTTP middlewares are widely used, and it&rsquo;s the same idea. You can think of decorators as <em>application-level middlewares</em>.</p> <p>Decorators let you separate things that don&rsquo;t belong together, but you can still explicitly use them where needed.</p> <p>You <em>wrap</em> the logic with other operations. The logic handler doesn&rsquo;t know what decorates it.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="723" height="617" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fincreasing-cohesion-in-go-with-generic-decorators%2Fdecorators_hu7f6324ee5ebb890ca9faf56ec1ef1877_153276_723x617_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fincreasing-cohesion-in-go-with-generic-decorators%5C%2Fdecorators_hu7f6324ee5ebb890ca9faf56ec1ef1877_153276_723x617_resize_lanczos_3.png"" /> <div class="code-ref"> Decorators wrapping the command handler. </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">subscribeAuthorizationDecorator</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">base</span> <span class="nx">SubscribeHandler</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">d</span> <span class="nx">subscribeAuthorizationDecorator</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">Subscribe</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">UserFromContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">user</span><span class="p">.</span><span class="nx">Active</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;the user&#39;s account is not active&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">d</span><span class="p">.</span><span class="nx">base</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F02-decorators%2Fdecorators.go%23L48" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/03-cohesion/02-decorators/decorators.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F02-decorators%2Fdecorators.go%23L48" target="_blank">Full source</a> </div> <p>The logic part stays super short now.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">subscribeHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">Subscribe</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Subscribe the user to the newsletter </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>You can pick which decorators to use depending on the use case. You can set them up in constructors.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1553" height="571" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fincreasing-cohesion-in-go-with-generic-decorators%2Ftests-and-production_hu8d14d4418adc167f2528d040e8e9b75c_99809_1553x571_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fincreasing-cohesion-in-go-with-generic-decorators%5C%2Ftests-and-production_hu8d14d4418adc167f2528d040e8e9b75c_99809_1553x571_resize_lanczos_3.png"" /> <div class="code-ref"> Different decorator sets for different purposes. </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewAuthorizedSubscribeHandler</span><span class="p">(</span><span class="nx">logger</span> <span class="nx">Logger</span><span class="p">,</span> <span class="nx">metricsClient</span> <span class="nx">MetricsClient</span><span class="p">)</span> <span class="nx">SubscribeHandler</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">subscribeLoggingDecorator</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">base</span><span class="p">:</span> <span class="nx">subscribeMetricsDecorator</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">base</span><span class="p">:</span> <span class="nx">subscribeAuthorizationDecorator</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">base</span><span class="p">:</span> <span class="nx">subscribeHandler</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">client</span><span class="p">:</span> <span class="nx">metricsClient</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">:</span> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewUnauthorizedSubscribeHandler</span><span class="p">(</span><span class="nx">logger</span> <span class="nx">Logger</span><span class="p">,</span> <span class="nx">metricsClient</span> <span class="nx">MetricsClient</span><span class="p">)</span> <span class="nx">SubscribeHandler</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">subscribeLoggingDecorator</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">base</span><span class="p">:</span> <span class="nx">subscribeMetricsDecorator</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">base</span><span class="p">:</span> <span class="nx">subscribeHandler</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">client</span><span class="p">:</span> <span class="nx">metricsClient</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">:</span> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewSubscribeHandler</span><span class="p">()</span> <span class="nx">SubscribeHandler</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">subscribeHandler</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F02-decorators%2Fsubscribe.go%23L26" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/03-cohesion/02-decorators/subscribe.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F02-decorators%2Fsubscribe.go%23L26" target="_blank">Full source</a> </div> <p>You can write a unit test for the exact same code but without things not related directly to the logic. You don&rsquo;t need to mock the logger or the metrics client. Tests become shorter and easier to follow.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestSubscribe</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">handler</span> <span class="o">:=</span> <span class="nf">NewSubscribeHandler</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span> <span class="o">:=</span> <span class="nx">Subscribe</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Email</span><span class="p">:</span> <span class="s">&#34;[email protected]&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">NewsletterID</span><span class="p">:</span> <span class="s">&#34;product-news&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">handler</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="nx">cmd</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Fatal</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F02-decorators%2Fsubscribe_test.go%23L8" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/03-cohesion/02-decorators/subscribe_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F02-decorators%2Fsubscribe_test.go%23L8" target="_blank">Full source</a> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>You still need to test the authorization mechanism. I suggest doing this on the component tests level.</p> <p>You can learn more about different test types in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-test-architecture%2F" target="_blank">Microservices test architecture</a>.</p> </p></div> </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Decorators</p> </div> <div class="notice-body"><p> Wrap your logic with decorators. Choose which to use depending on the use case. </p></div> </div> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/increasing-cohesion-in-go-with-generic-decorators/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="web-applications;anti-patterns;go;golang;design-patterns;generics"> <input type="hidden" name="fields[blog_series]" value=""> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h2 id="generic-decorators">Generic decorators</h2> <p>Splitting code into decorators comes with some boilerplate. Usually, we like to deal with boilerplate using code generation. For example, you can easily <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcommon-anti-patterns-in-go-web-applications%2F" target="_blank">generate API or storage models</a>.</p> <p>We used to generate our decorators as well. But with Go 1.18 out there, we can now replace them with generic decorators.</p> <p>Because all our command handlers follow the same convention, the command is the only generic type we need. (A command is a <code>struct</code> containing all inputs for the command handler.)</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">CommandHandler</span><span class="p">[</span><span class="nx">C</span> <span class="nx">any</span><span class="p">]</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">C</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F03-generics%2Fsubscribe.go%23L7" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/03-cohesion/03-generics/subscribe.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F03-generics%2Fsubscribe.go%23L7" target="_blank">Full source</a> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">authorizationDecorator</span><span class="p">[</span><span class="nx">C</span> <span class="nx">any</span><span class="p">]</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">base</span> <span class="nx">CommandHandler</span><span class="p">[</span><span class="nx">C</span><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">d</span> <span class="nx">authorizationDecorator</span><span class="p">[</span><span class="nx">C</span><span class="p">])</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">C</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">UserFromContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">user</span><span class="p">.</span><span class="nx">Active</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;the user&#39;s account is not active&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">d</span><span class="p">.</span><span class="nx">base</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F03-generics%2Fdecorators.go%23L50" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/03-cohesion/03-generics/decorators.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F03-generics%2Fdecorators.go%23L50" target="_blank">Full source</a> </div> <p>You can use the generic decorators with any other command handler. You could even move them to a separate library.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewUnauthorizedSubscribeHandler</span><span class="p">(</span><span class="nx">logger</span> <span class="nx">Logger</span><span class="p">,</span> <span class="nx">metricsClient</span> <span class="nx">MetricsClient</span><span class="p">)</span> <span class="nx">CommandHandler</span><span class="p">[</span><span class="nx">Subscribe</span><span class="p">]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">loggingDecorator</span><span class="p">[</span><span class="nx">Subscribe</span><span class="p">]{</span> </span></span><span class="line"><span class="cl"> <span class="nx">base</span><span class="p">:</span> <span class="nx">metricsDecorator</span><span class="p">[</span><span class="nx">Subscribe</span><span class="p">]{</span> </span></span><span class="line"><span class="cl"> <span class="nx">base</span><span class="p">:</span> <span class="nx">SubscribeHandler</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">client</span><span class="p">:</span> <span class="nx">metricsClient</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">logger</span><span class="p">:</span> <span class="nx">logger</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F03-generics%2Fsubscribe.go%23L38" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/03-cohesion/03-generics/subscribe.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F03-generics%2Fsubscribe.go%23L38" target="_blank">Full source</a> </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Generic decorators</p> </div> <div class="notice-body"><p> Use generics to reduce boilerplate. </p></div> </div> <h2 id="one-caveat">One Caveat</h2> <p>In most cases in Go, it makes sense to <strong>accept interfaces and return structs</strong>. But to make wrapping possible, you need to return an interface from constructors.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">SubscribeHandler</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">Subscribe</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">subscribeHandler</span> <span class="kd">struct</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewSubscribeHandler</span><span class="p">()</span> <span class="nx">SubscribeHandler</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">subscribeHandler</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F02-decorators%2Fsubscribe.go%23L20" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/03-cohesion/02-decorators/subscribe.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F769cdacf9cd2d6407af8014bfea3b2f4351c89f1%2F03-cohesion%2F02-decorators%2Fsubscribe.go%23L20" target="_blank">Full source</a> </div> <p>The downside is that it&rsquo;s more challenging to navigate the project. You can&rsquo;t just click the <code>Handle</code> method to land in the definition. You should be fine if you use a solid IDE that shows what implements an interface.</p> <p>Also, be aware that it can surprise someone trying to follow the general rule. It&rsquo;s OK to break the rules but understand (and be ready to explain) <em>why</em> you make an exception.</p> <p>Another thing to watch out for is how you wrap your decorators. <em>Outer</em> decorators get executed first, and sometimes the order makes a difference. For example, you want to keep logs and metrics even if the authorization fails.</p> <h2 id="the-complete-example">The Complete Example</h2> <p>Check the complete source code on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Ftree%2Fmaster%2F03-cohesion" target="_blank">the anti-patterns repository</a>.</p> <p>It features logging and metrics decorators ready to use with minimal changes.</p> <h2 id="wild-workouts">Wild Workouts</h2> <p>In parallel, Robert introduced decorators to <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">Wild Workouts</a>, the example project you might know from our previous posts. Check the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F4cc4ad70c2051ff31c7a20e8d5abe26b5dacd3fb" target="_blank">commit</a> to see what this looks like in a real application.</p> <p>There are two helper functions not covered in this post: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F4cc4ad70c2051ff31c7a20e8d5abe26b5dacd3fb%2Finternal%2Fcommon%2Fdecorator%2Fcommand.go%23L11" target="_blank"><code>ApplyCommandDecorators</code></a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F4cc4ad70c2051ff31c7a20e8d5abe26b5dacd3fb%2Finternal%2Fcommon%2Fdecorator%2Fquery.go%23L9" target="_blank"><code>ApplyQueryDecorators</code></a>.</p> <p>Robert also added support for <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%23running-locally" target="_blank">running Wild Workouts locally</a> on M1 CPUs. If you didn&rsquo;t have a chance to do it yet, it might be a good moment to play with the project.</p>Auto-generated C4 Architecture Diagrams in Gohttps://threedots.tech/post/auto-generated-c4-architecture-diagrams-in-go/Thu, 02 Dec 2021 00:00:00 +0100https://threedots.tech/post/auto-generated-c4-architecture-diagrams-in-go/<div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>Hello! Please give Krzysztof a warm welcome in the first guest post on our blog. 🎉 We&rsquo;ve been working with Krzysztof for the past two years, and we&rsquo;re excited to share his work here.</p> <p><em>Miłosz &amp; Robert</em></p> </p></div> </div> <p>We all struggle with software architecture diagrams, don&rsquo;t we? Have you ever wondered why? If you ask yourself that question, why maintenance of up-to-date and detailed software architecture diagrams is so painful, you will come up with a long list of valid answers.</p> <p>Our software changes all the time. We update it daily, starting with a simple change of names, and ending with nuke-refactoring, reshaping the entire application. Each of those changes requires careful alignment of the software architecture diagrams. Most likely, you have also thought that drawing diagrams with even the most convenient tools is time-consuming. It requires a lot of effort. Those diagrams notoriously get out of control, storing outdated naming and already-dropped modules while missing their initial readable and well-organised structure. All of that makes this particularly frustrating.</p> <h2 id="if-an-ideal-world-ever-existed">If an ideal world ever existed&hellip;</h2> <p>Everyone appreciates a well-structured diagram when joining a new, unexplored project. Even better if it is an easy-to-navigate, map-like C4 model diagram.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="4096" height="967" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fauto-generated-c4-architecture-diagrams-in-go%2Fexample_hu215c0224212e0335768e552d11218246_201201_4096x967_resize_q80_h2_lanczos_3.webp" alt="Auto-generated C4 diagram example" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fauto-generated-c4-architecture-diagrams-in-go%5C%2Fexample_hu215c0224212e0335768e552d11218246_201201_4096x967_resize_lanczos_3.png"" /> <div class="code-ref"> Auto-generated C4 diagram example </div> <p>If this concept doesn&rsquo;t sound familiar to you, there is a simple idea behind it. C4 model introduces four layers of software architecture visualisation: Context, Containers, Components, and Code. Depending on the information you are looking for, you can drill down to the specific part of the implementation, the same way you look into geographical maps. When searching for a particular address, you usually use a city or even a district map, not a map of the whole country. Consequently, when you check how single service modules talk to each other, you will find it in the diagram of that service <code>Components</code> layer. When you look for services and runtimes interactions, you will go to the <code>Containers</code> one. Do you get the idea? For more information, go to <a href="proxy.php?url=https%3A%2F%2Fc4model.com%2F" target="_blank">C4 model site</a>. In my opinion, this is the best approach proposed so far.</p> <p>Is maintaining detailed and up-to-date diagrams of your applications&rsquo; architecture worth all that effort? And does it have to be done manually? What if we could…</p> <h2 id="automate-it">Automate it!</h2> <p>If you work within a Go ecosystem, you are used to code generation. What if we also could generate software diagrams from the code? Moreover, what if we could do this automatically, within continuous integration pipelines?</p> <p>Driven by this idea, I wrote a library that generates diagrams out of code with just a single configuration file. Let me show you how to use it.</p> <h2 id="lets-start-simple">Let&rsquo;s start simple</h2> <p>To demonstrate the feature, I selected a repository that most readers are familiar with – <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">wild-workouts-go-ddd-example application</a>. There are a couple of services that we could try to generate diagrams for. Let&rsquo;s start with the <code>trainer</code> one.</p> <p>At the moment, the library provides a set of components to code your own diagram auto-generation command.</p> <p>I did that in the <code>wild-workouts-go-ddd-example</code> repository. Within a separate <code>tools/c4</code> directory I created a simple command file.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">s</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">scraper</span><span class="p">.</span><span class="nf">NewScraperFromConfigFile</span><span class="p">(</span><span class="s">&#34;scraper.yml&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">app</span> <span class="o">:=</span> <span class="nx">trainerService</span><span class="p">.</span><span class="nf">NewApplication</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">structure</span> <span class="o">:=</span> <span class="nx">s</span><span class="p">.</span><span class="nf">Scrape</span><span class="p">(</span><span class="nx">app</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">v</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">view</span><span class="p">.</span><span class="nf">NewViewFromConfigFile</span><span class="p">(</span><span class="s">&#34;scraper.yml&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">outFile</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="s">&#34;out/view.plantuml&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="nx">outFile</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">v</span><span class="p">.</span><span class="nf">RenderStructureTo</span><span class="p">(</span><span class="nx">structure</span><span class="p">,</span> <span class="nx">outFile</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="o">...</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fmaster%2Ftools%2Fc4%2Fmain.go%23L21" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/tools/c4/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fmaster%2Ftools%2Fc4%2Fmain.go%23L21" target="_blank">Full source</a> </div> <p>In this command, I use a couple of components provided by the library.</p> <p>The first one, the <code>scraper</code>, crawls down any Go structure and collects visited components following provided configuration and scraping rules. I started with a minimal configuration file:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">configuration</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pkgs</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;github.com&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">rules</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">name_regexp</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;.*&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pkg_regexps</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;.*&#34;</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fmaster%2Ftools%2Fc4%2Fscraper.yml" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/tools/c4/scraper.yml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fmaster%2Ftools%2Fc4%2Fscraper.yml" target="_blank">Full source</a> </div> <p>In the configuration, I instruct the scraper to crawl all components from any package starting with a <code>github.com</code> prefix and then to collect the ones that match the rule with the provided name and package regular expressions. The expression <code>.*</code> would match any string, so basically, I want to capture everything.</p> <p>For those of you who are YAML haters, the very same configuration may be done programmatically.</p> <p>Moving next, I need to instantiate the service application context as the &ldquo;content&rdquo; to scrape. Robert and Miłosz had implemented a convenient constructor, which I invoke here. Then I can pass it to the scraper and get the returned <code>Structure</code> model containing all the collected data.</p> <p>I instantiate another library component to render the acquired structure into the output file – the <code>view</code>. It consumes the same configuration file as the scraper, enhanced with just a single line of the view definition.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">view</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">Trainer service components</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fmaster%2Ftools%2Fc4%2Fview.yml" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/tools/c4/view.yml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fmaster%2Ftools%2Fc4%2Fview.yml" target="_blank">Full source</a> </div> <p>&hellip;and I render it.</p> <p>The output file is of type *.plantuml. I need to render it into a *.png image file with the <code>plantuml</code> CLI tool.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"> plantuml out/view.plantuml </span></span></code></pre></div><p>Here is what I got: <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fauto-generated-c4-architecture-diagrams-in-go%2Ftdl-go_structurizr_1_1.png" style="display: block;" class="glightbox"> <img title="" loading="lazy" decoding="async" class="img img-center" width="4096" height="2053" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fauto-generated-c4-architecture-diagrams-in-go%2Ftdl-go_structurizr_1_1_hu24fde6ec90ee00a43b32933419d60fd0_208533_4096x2053_resize_q80_h2_lanczos_3.webp" alt="Raw C4 diagram" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fauto-generated-c4-architecture-diagrams-in-go%5C%2Ftdl-go_structurizr_1_1_hu24fde6ec90ee00a43b32933419d60fd0_208533_4096x2053_resize_lanczos_3.png"" /> </a> </p> <div class="code-ref"> Raw C4 diagram </div> <h3 id="getting-fancier">Getting fancier</h3> <p>So far, so good. But there is at least a couple of issues here. It works, but it is all flat! All I can see in the diagram is a map of components without any details, description, or color-coding. Which of the components belong to the application or domain layer? Also, we do not know much about the infrastructure around. Is there any database being used? If yes, which of the components are using it? Let&rsquo;s iterate.</p> <p>I need to extend the scraping range by adding the package with the firestore client. The application uses the firestore database, so I included its client in the diagram.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">configuration</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pkgs</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;github.com&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;cloud.google.com/go/firestore&#34;</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fmaster%2Ftools%2Fc4%2Fscraper.yml" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/tools/c4/scraper.yml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fmaster%2Ftools%2Fc4%2Fscraper.yml" target="_blank">Full source</a> </div> <p>Then, I create several rules that would instruct the scraper on interpreting the components from specified packages. With rules, I could tag components, add descriptions, define name functions, and much more.</p> <p>Here are a few examples of rules where I define application, domain, and database components.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">rules</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">name_regexp</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;.*&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pkg_regexps</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;.*/app/command.*&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">component</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;application command&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">tags</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">APP</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">name_regexp</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;.*&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pkg_regexps</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;.*/domain/.*&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">component</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;domain component&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">tags</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">DOMAIN</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">name_regexp</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;.*Client$&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">pkg_regexps</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;cloud.google.com/go/firestore$&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">component</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Firestore&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;firestore client&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">tags</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">DB</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fmaster%2Ftools%2Fc4%2Fscraper.yml" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/tools/c4/scraper.yml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fmaster%2Ftools%2Fc4%2Fscraper.yml" target="_blank">Full source</a> </div> <p>Finally, I can add some view styles and assign those to the tags defined above.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">view</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">Trainer service components</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">line_color</span><span class="p">:</span><span class="w"> </span><span class="l">000000ff</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">styles</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">APP</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">background_color</span><span class="p">:</span><span class="w"> </span><span class="l">1a4577ff</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">font_color</span><span class="p">:</span><span class="w"> </span><span class="l">ffffffff</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">border_color</span><span class="p">:</span><span class="w"> </span><span class="l">000000ff</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">DOMAIN</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">background_color</span><span class="p">:</span><span class="w"> </span><span class="l">ffffffff</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">font_color</span><span class="p">:</span><span class="w"> </span><span class="l">000000ff</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">border_color</span><span class="p">:</span><span class="w"> </span><span class="l">000000ff</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">DB</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">background_color</span><span class="p">:</span><span class="w"> </span><span class="l">c8c8c8ff</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">font_color</span><span class="p">:</span><span class="w"> </span><span class="l">000000ff</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">border_color</span><span class="p">:</span><span class="w"> </span><span class="l">000000ff</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">shape</span><span class="p">:</span><span class="w"> </span><span class="l">database</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fmaster%2Ftools%2Fc4%2Fview.yml" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/tools/c4/view.yml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fmaster%2Ftools%2Fc4%2Fview.yml" target="_blank">Full source</a> </div> <p>When I reran the scraper, I got the following result. Doesn&rsquo;t it look much better now?</p> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fauto-generated-c4-architecture-diagrams-in-go%2Ftdl-go_structurizr_1_2.png" style="display: block;" class="glightbox"> <img title="" loading="lazy" decoding="async" class="img img-center" width="4096" height="2071" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fauto-generated-c4-architecture-diagrams-in-go%2Ftdl-go_structurizr_1_2_hu77f064fe76edbd6ec9b648a163a3b315_284325_4096x2071_resize_q80_h2_lanczos_3.webp" alt="Styled C4 diagram" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fauto-generated-c4-architecture-diagrams-in-go%5C%2Ftdl-go_structurizr_1_2_hu77f064fe76edbd6ec9b648a163a3b315_284325_4096x2071_resize_lanczos_3.png"" /> </a> <div class="code-ref"> Styled C4 diagram </div> <h3 id="want-more">Want more?</h3> <p>If you want to have a deeper look into this example, go to <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">wild-workouts-go-ddd-example application repo</a> and check the implementation where I generate complete diagrams for all of the microservices.</p> <p>You can also learn more about the library itself. Visit <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fkrzysztofreczek%2Fgo-structurizr" target="_blank">go-structurizr repo</a> and dive deep into the documentation and provided examples.</p> <p>Having a pretty powerful way to auto-generate diagrams out of the golang code, every time you change your code, you can just regenerate diagrams reusing the very same configuration! Finally, it may sound tempting to fully automate it as part of our regular CI pipelines. If you want to know how I do that, let me know in the comments.</p>Safer Enums in Gohttps://threedots.tech/post/safer-enums-in-go/Tue, 12 Oct 2021 00:00:00 +0200https://threedots.tech/post/safer-enums-in-go/<p>Enums are a crucial part of web applications. Go doesn&rsquo;t support them out of the box, but there are ways to emulate them.</p> <p>Many obvious solutions are far from ideal. Here are some ideas we use that make enums safer by design.</p> <h2 id="iota">iota</h2> <p>Go lets you enumerate things with <code>iota</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">const</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">Guest</span> <span class="p">=</span> <span class="kc">iota</span> </span></span><span class="line"><span class="cl"> <span class="nx">Member</span> </span></span><span class="line"><span class="cl"> <span class="nx">Moderator</span> </span></span><span class="line"><span class="cl"> <span class="nx">Admin</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F01-iota%2Frole%2Frole.go%23L3" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/02-enums/01-iota/role/role.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F01-iota%2Frole%2Frole.go%23L3" target="_blank">Full source</a> </div> <p>While Go is explicit, <code>iota</code> seems relatively obscure. If you sort the <code>const</code> group any other way, you introduce side effects. In the example above, you could accidentally create moderators instead of members. You can explicitly assign a number to each value to avoid this issue, but it makes <code>iota</code> obsolete.</p> <p><code>iota</code> works well for flags represented by powers of two.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">const</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">Guest</span> <span class="p">=</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="kc">iota</span> <span class="c1">// 1 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">Member</span> <span class="c1">// 2 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">Moderator</span> <span class="c1">// 4 </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">Admin</span> <span class="c1">// 8 </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="nx">user</span><span class="p">.</span><span class="nx">Roles</span> <span class="p">=</span> <span class="nx">Member</span> <span class="p">|</span> <span class="nx">Moderator</span> <span class="c1">// 6 </span></span></span></code></pre></div><p>Bitmasks are efficient and sometimes helpful. However, it&rsquo;s a different use case than enums in most web applications. Often, you&rsquo;ll be fine storing all roles in a list. It&rsquo;ll also be more readable.</p> <p>The main issue with <code>iota</code> is it works on integers that don&rsquo;t guard against passing invalid values.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">CreateUser</span><span class="p">(</span><span class="nx">role</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Creating user with role&#34;</span><span class="p">,</span> <span class="nx">role</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">CreateUser</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nf">CreateUser</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F01-iota%2Fmain.go" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/02-enums/01-iota/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F01-iota%2Fmain.go" target="_blank">Full source</a> </div> <p><code>CreateUser</code> will happily accept <code>-1</code> or <code>42</code> even though there are no corresponding roles.</p> <p>Of course, we could validate this within the function. But we use a language with strong types, so let&rsquo;s take advantage of it. In our application&rsquo;s context, a user role is much more than a vague number.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Integer enums</p> </div> <div class="notice-body"><p> Don&rsquo;t use <code>iota</code>-based integers to represent enums that are not sequential numbers or flags. </p></div> </div> <p>We could introduce a type to improve the solution.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Role</span> <span class="kt">uint</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">const</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">Guest</span> <span class="nx">Role</span> <span class="p">=</span> <span class="kc">iota</span> </span></span><span class="line"><span class="cl"> <span class="nx">Member</span> </span></span><span class="line"><span class="cl"> <span class="nx">Moderator</span> </span></span><span class="line"><span class="cl"> <span class="nx">Admin</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F02-typed-iota%2Frole%2Frole.go%23L5" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/02-enums/02-typed-iota/role/role.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F02-typed-iota%2Frole%2Frole.go%23L5" target="_blank">Full source</a> </div> <p>It looks better, but it&rsquo;s still possible to pass any arbitrary integer in place of <code>Role</code>. The Go compiler doesn&rsquo;t help us here.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">CreateUser</span><span class="p">(</span><span class="nx">role</span> <span class="nx">Role</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Creating user with role&#34;</span><span class="p">,</span> <span class="nx">role</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">main</span> <span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">CreateUser</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nf">CreateUser</span><span class="p">(</span><span class="nx">role</span><span class="p">.</span><span class="nf">Role</span><span class="p">(</span><span class="mi">42</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F02-typed-iota%2Fmain.go" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/02-enums/02-typed-iota/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F02-typed-iota%2Fmain.go" target="_blank">Full source</a> </div> <p>The type is an improvement over a bare integer, but it&rsquo;s still an illusion. It doesn&rsquo;t give us any guarantee that the role is valid.</p> <h2 id="sentinel-values">Sentinel values</h2> <p>Because <code>iota</code> starts from 0, the <code>Guest</code> is also the <code>Role</code>&rsquo;s zero-value. It makes it hard to detect if the role is empty or someone passed a <code>Guest</code> value.</p> <p>You can avoid this by counting from 1. Even better, keep an explicit sentinel value you can compare and can&rsquo;t mistake for an actual role.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">const</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">Unknown</span> <span class="nx">Role</span> <span class="p">=</span> <span class="kc">iota</span> </span></span><span class="line"><span class="cl"> <span class="nx">Guest</span> </span></span><span class="line"><span class="cl"> <span class="nx">Member</span> </span></span><span class="line"><span class="cl"> <span class="nx">Moderator</span> </span></span><span class="line"><span class="cl"> <span class="nx">Admin</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F02-typed-iota%2Frole%2Frole.go%23L6" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/02-enums/02-typed-iota/role/role.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F02-typed-iota%2Frole%2Frole.go%23L6" target="_blank">Full source</a> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">CreateUser</span><span class="p">(</span><span class="nx">r</span> <span class="nx">role</span><span class="p">.</span><span class="nx">Role</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">r</span> <span class="o">==</span> <span class="nx">role</span><span class="p">.</span><span class="nx">Unknown</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;no role provided&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Creating user with role&#34;</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F02-typed-iota%2Fmain.go%23L11" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/02-enums/02-typed-iota/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F02-typed-iota%2Fmain.go%23L11" target="_blank">Full source</a> </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Explicit sentinels</p> </div> <div class="notice-body"><p> Keep an explicit variable for the enum&rsquo;s zero-value. </p></div> </div> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/safer-enums-in-go/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="web-applications;anti-patterns;go;golang;enums"> <input type="hidden" name="fields[blog_series]" value=""> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h2 id="slugs">Slugs</h2> <p>Enums seem to be about sequential integers, but it&rsquo;s rarely a valid representation. In web applications, we use enums to group possible variants of some type. They don&rsquo;t map well to numbers.</p> <p>It&rsquo;s hard to understand the context when you see a <code>3</code> in the API response, a database table, or logs. You have to check the source or the outdated documentation to know what it&rsquo;s about.</p> <p>String slugs are more meaningful than integers in most scenarios. Wherever you see it, a <code>moderator</code> is obvious. Since <code>iota</code> doesn&rsquo;t help us anyway, we can as well use human-readable strings.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Role</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">const</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">Unknown</span> <span class="nx">Role</span> <span class="p">=</span> <span class="s">&#34;&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">Guest</span> <span class="nx">Role</span> <span class="p">=</span> <span class="s">&#34;guest&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">Member</span> <span class="nx">Role</span> <span class="p">=</span> <span class="s">&#34;member&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">Moderator</span> <span class="nx">Role</span> <span class="p">=</span> <span class="s">&#34;moderator&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">Admin</span> <span class="nx">Role</span> <span class="p">=</span> <span class="s">&#34;admin&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F03-slugs%2Frole%2Frole.go%23L5" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/02-enums/03-slugs/role/role.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F03-slugs%2Frole%2Frole.go%23L5" target="_blank">Full source</a> </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Slugs</p> </div> <div class="notice-body"><p> <p>Use string values instead of integers.</p> <p>Avoid whitespace for easier parsing and logging. Use <code>camelCase</code>, <code>snake_case</code>, or <code>kebab-case</code>.</p> </p></div> </div> <p>Slug are especially useful for error codes. An error response like <code>{&quot;error&quot;: &quot;user-not-found&quot;}</code> is obvious in contrast to <code>{&quot;error&quot;: 4102}</code>.</p> <p>However, the type can still hold any arbitrary string.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">err</span> <span class="p">=</span> <span class="nf">CreateUser</span><span class="p">(</span><span class="s">&#34;super-admin&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F03-slugs%2Fmain.go%23L36" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/02-enums/03-slugs/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F03-slugs%2Fmain.go%23L36" target="_blank">Full source</a> </div> <h2 id="struct-based-enums">Struct-based Enums</h2> <p>The final iteration uses structs. It lets us work with code secure by design. We don&rsquo;t need to check if the passed value is correct.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Role</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">slug</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="nx">Role</span><span class="p">)</span> <span class="nf">String</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">r</span><span class="p">.</span><span class="nx">slug</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">Unknown</span> <span class="p">=</span> <span class="nx">Role</span><span class="p">{</span><span class="s">&#34;&#34;</span><span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">Guest</span> <span class="p">=</span> <span class="nx">Role</span><span class="p">{</span><span class="s">&#34;guest&#34;</span><span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">Member</span> <span class="p">=</span> <span class="nx">Role</span><span class="p">{</span><span class="s">&#34;member&#34;</span><span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">Moderator</span> <span class="p">=</span> <span class="nx">Role</span><span class="p">{</span><span class="s">&#34;moderator&#34;</span><span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">Admin</span> <span class="p">=</span> <span class="nx">Role</span><span class="p">{</span><span class="s">&#34;admin&#34;</span><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F04-structs%2Frole%2Frole.go%23L14" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/02-enums/04-structs/role/role.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F04-structs%2Frole%2Frole.go%23L14" target="_blank">Full source</a> </div> <p>Because the <code>slug</code> field is unexported, it&rsquo;s not possible to fill it from outside the package. The only invalid role you can construct is the empty one: <code>Role{}</code>.</p> <p>We can add a constructor to create a valid role based on a slug:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">FromString</span><span class="p">(</span><span class="nx">s</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">Role</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">switch</span> <span class="nx">s</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">Guest</span><span class="p">.</span><span class="nx">slug</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Guest</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">Member</span><span class="p">.</span><span class="nx">slug</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Member</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">Moderator</span><span class="p">.</span><span class="nx">slug</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Moderator</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">Admin</span><span class="p">.</span><span class="nx">slug</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Admin</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Unknown</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;unknown role: &#34;</span> <span class="o">+</span> <span class="nx">s</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F04-structs%2Frole%2Frole.go%23L21" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/02-enums/04-structs/role/role.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fmaster%2F02-enums%2F04-structs%2Frole%2Frole.go%23L21" target="_blank">Full source</a> </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Struct-based enums</p> </div> <div class="notice-body"><p> Encapsulate enums in structs for extra compile-time safety. </p></div> </div> <p>This approach is perfect when you work with business logic. Keeping structures always in a valid state in memory makes your code easier to work with and understand. It&rsquo;s enough to check if the enum type isn&rsquo;t empty, and you&rsquo;re sure it&rsquo;s a correct value.</p> <p>There&rsquo;s one potential issue with this approach. Structs cannot be constants in Go, so it&rsquo;s possible to overwrite the global variables like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">roles</span><span class="p">.</span><span class="nx">Guest</span> <span class="p">=</span> <span class="nx">role</span><span class="p">.</span><span class="nx">Admin</span> </span></span></code></pre></div><p>There is no sane reason to do this, though. You&rsquo;re much more likely to pass an invalid integer by accident.</p> <p>Another disadvantage is you have to make updates in two places: the enums list and the constructor. However, it&rsquo;s easy to spot, even if you miss it at first.</p> <h2 id="how-do-you-do-it">How do <strong>you</strong> do it?</h2> <p>When there&rsquo;s a missing Go feature, we tend to develop our ways of doing things. What I described is just one possible approach. Do you use another pattern to store your enums? Please share it in the comments. 🙂</p> <p>For complete source code, see <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns" target="_blank">the anti-patterns repository</a>.</p>Common Anti-Patterns in Go Web Applicationshttps://threedots.tech/post/common-anti-patterns-in-go-web-applications/Thu, 12 Aug 2021 00:00:00 +0200https://threedots.tech/post/common-anti-patterns-in-go-web-applications/<p>At one point in my career, I was no longer excited about the software I was building.</p> <p>My favorite part of the job were low-level details and complex algorithms. After switching to user-facing applications, they were mostly gone. It seemed programming was about moving data from one place to another using existing libraries and tools. What I&rsquo;ve learned so far about software wasn&rsquo;t that useful anymore.</p> <p>Let&rsquo;s face it: most web applications don&rsquo;t solve tough technical challenges. They should correctly model the product and allow improving it faster than the competition.</p> <p>It seems boring at first, but then you realize supporting this goal is harder than it sounds. There&rsquo;s an entirely different set of challenges. Even if they&rsquo;re not as complex in the technical sense, solving them has a massive impact on the product and is deeply satisfying.</p> <p><strong>The biggest challenge web apps face is not becoming an unmaintainable Big Ball of Mud. It slows you down and can put you out of business.</strong></p> <p>Here&rsquo;s why it happens in Go and how I&rsquo;ve learned to avoid it.</p> <h2 id="loose-coupling-is-the-key">Loose Coupling Is the Key</h2> <p>A big reason applications get hard to maintain is strong coupling.</p> <p><strong>In tightly coupled applications, anything you touch has unexpected side effects.</strong> Each refactoring attempt uncovers new issues. Eventually, you decide it&rsquo;s best to rewrite the project from scratch. In a fast-growing product, you can&rsquo;t afford to freeze all development to re-do what you&rsquo;ve built. You have no guarantee you&rsquo;ll get everything right this time.</p> <p>In contrast, <strong>loosely coupled applications keep clear boundaries.</strong> They allow replacing a broken part without effect on the rest of the project. They&rsquo;re easier to build and maintain. So why are they so rare?</p> <p>Microservices promised us loose coupling, but we&rsquo;re after their hype era, and unmaintainable applications still exist. Sometimes it&rsquo;s even worse, and we fall into the trap of <em><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-or-monolith-its-detail%2F">distributed monoliths</a></em>, dealing with the same issues as before with added network overhead.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1637" height="483" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcommon-anti-patterns-in-go-web-applications%2Fmicroservices_hu9bba1f555db4edc69cca9493a004fc3f_173483_1637x483_resize_q80_h2_lanczos_3.webp" alt="Microservices" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fcommon-anti-patterns-in-go-web-applications%5C%2Fmicroservices_hu9bba1f555db4edc69cca9493a004fc3f_173483_1637x483_resize_lanczos_3.png"" /> <div class="code-ref"> From a tightly coupled monolith to a distributed monolith. </div> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: The Distributed Monolith</p> </div> <div class="notice-body"><p> Don&rsquo;t split your application into microservices before you know the boundaries. </p></div> </div> <p>Microservices don&rsquo;t lower coupling because <strong>it&rsquo;s not important how many times you split the application. What matters is how you connect the pieces.</strong></p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1681" height="531" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcommon-anti-patterns-in-go-web-applications%2Fmonolith_hud8b365994a10d58119a66941b1c02ea5_113704_1681x531_resize_q80_h2_lanczos_3.webp" alt="Modular Monolith" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fcommon-anti-patterns-in-go-web-applications%5C%2Fmonolith_hud8b365994a10d58119a66941b1c02ea5_113704_1681x531_resize_lanczos_3.png"" /> <div class="code-ref"> From a modular monolith to loosely-coupled microservices. </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Loose Coupling</p> </div> <div class="notice-body"><p> Aim for loosely coupled modules. How you deploy them (as a modular monolith or microservices) is an implementation detail. </p></div> </div> <h2 id="dry-introduces-coupling">DRY introduces coupling</h2> <p>Strong coupling is common because we&rsquo;re taught early about the &ldquo;Don&rsquo;t Repeat Yourself&rdquo; (DRY) rule.</p> <p>Short rules are easy to remember, but it&rsquo;s hard to capture all the details in three words. <em>The Pragmatic Programmer</em> offers a longer version:</p> <blockquote> <p>Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.</p> </blockquote> <p>&ldquo;Every piece of knowledge&rdquo; is quite extreme. The answer to most programming dilemmas is &ldquo;it depends&rdquo;, and DRY is no different.</p> <p><strong>When you make two things use a common abstraction, you introduce coupling. If you follow DRY too strictly, you add abstractions before they&rsquo;re needed.</strong></p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1800" height="957" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcommon-anti-patterns-in-go-web-applications%2Fcoupling_hu8dcae9cb34a2a3ab43728bd8577ed7f8_160498_1800x957_resize_q80_h2_lanczos_3.webp" alt="Coupling" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fcommon-anti-patterns-in-go-web-applications%5C%2Fcoupling_hu8dcae9cb34a2a3ab43728bd8577ed7f8_160498_1800x957_resize_lanczos_3.png"" /> <h2 id="being-dry-in-go">Being DRY in Go</h2> <p>Compared to other modern languages, Go is explicit and stripped from features. There&rsquo;s not much syntactic sugar to hide complexity.</p> <p>We&rsquo;re used to shortcuts, so at first, it&rsquo;s hard to accept Go&rsquo;s verbosity. It&rsquo;s like we&rsquo;ve developed an instinct to find &ldquo;smarter&rdquo; ways of writing code.</p> <p>The best example is error handling. If you have any experience writing Go, this snippet feels natural:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>For newcomers, repeating these three lines over and over seems like breaking the DRY rule. They often look for a way to avoid this boilerplate, but it never ends well.</p> <p>Eventually, everyone accepts that&rsquo;s how Go works. <strong>It makes you repeat yourself, but it&rsquo;s not the duplication DRY tells you to avoid.</strong></p> <h2 id="a-single-model-couples-your-application">A Single Model Couples Your Application</h2> <p><strong>There&rsquo;s one feature in Go that introduces strong coupling and makes you think you&rsquo;re following DRY. It&rsquo;s using multiple tags in a single struct.</strong> It seems like a good idea because we often use <em>similar</em> models for different things.</p> <p>Here&rsquo;s a popular way of keeping a single <code>User</code> model.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">int</span> <span class="s">`json:&#34;id&#34; gorm:&#34;autoIncrement primaryKey&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">FirstName</span> <span class="kt">string</span> <span class="s">`json:&#34;first_name&#34; validate:&#34;required_without=LastName&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastName</span> <span class="kt">string</span> <span class="s">`json:&#34;last_name&#34; validate:&#34;required_without=FirstName&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">DisplayName</span> <span class="kt">string</span> <span class="s">`json:&#34;display_name&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Email</span> <span class="kt">string</span> <span class="s">`json:&#34;email,omitempty&#34; gorm:&#34;-&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Emails</span> <span class="p">[]</span><span class="nx">Email</span> <span class="s">`json:&#34;emails&#34; validate:&#34;required,dive&#34; gorm:&#34;constraint:OnDelete:CASCADE&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">PasswordHash</span> <span class="kt">string</span> <span class="s">`json:&#34;-&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastIP</span> <span class="kt">string</span> <span class="s">`json:&#34;-&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">CreatedAt</span> <span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> <span class="s">`json:&#34;-&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">UpdatedAt</span> <span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> <span class="s">`json:&#34;-&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Email</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">int</span> <span class="s">`json:&#34;-&#34; gorm:&#34;primaryKey&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Address</span> <span class="kt">string</span> <span class="s">`json:&#34;address&#34; validate:&#34;required,email&#34; gorm:&#34;size:256;uniqueIndex&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Primary</span> <span class="kt">bool</span> <span class="s">`json:&#34;primary&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserID</span> <span class="kt">int</span> <span class="s">`json:&#34;-&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fab94d47350756716a7fed42b7ae546c51c11406a%2F01-coupling%2F01-tightly-coupled%2Finternal%2Fuser.go%23L9" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/01-tightly-coupled/internal/user.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fab94d47350756716a7fed42b7ae546c51c11406a%2F01-coupling%2F01-tightly-coupled%2Finternal%2Fuser.go%23L9" target="_blank">Full source</a> </div> <p>This approach takes a few lines of code and makes you maintain just a single structure.</p> <p>However, fitting everything in a single model requires many tricks. The API shouldn&rsquo;t expose some fields, so they&rsquo;re hidden using <code>json:&quot;-&quot;</code>. Only one API endpoint uses the <code>Email</code> field, so the ORM skips it, and the <code>omitempty</code> tag hides it from regular JSON responses.</p> <p><strong>Most importantly, this solution gives you one of the worst issues: strong coupling between the API, storage, and logic.</strong></p> <p>When you want to update anything in the struct, you have no idea what else can change. You can break the API contract by changing the database schema or corrupt the stored data when updating validation rules.</p> <p>The more complex the model, the more issues you face.</p> <p>For example, the <code>json</code> tag means JSON, not HTTP. What happens when you introduce events that also marshal to JSON but with a bit different format than the API response? You will keep adding hacks to make it work.</p> <p>Eventually, your team avoids any changes to the structure because they don&rsquo;t know what can break after you touch it.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1226" height="510" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcommon-anti-patterns-in-go-web-applications%2Fsingle-model_hu61d56791a1dda7991606e12ed392204a_67743_1226x510_resize_q80_h2_lanczos_3.webp" alt="Single Model" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fcommon-anti-patterns-in-go-web-applications%5C%2Fsingle-model_hu61d56791a1dda7991606e12ed392204a_67743_1226x510_resize_lanczos_3.png"" /> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: The Single Model</p> </div> <div class="notice-body"><p> <p>Don&rsquo;t give a single model more than one responsibility.</p> <p>Don&rsquo;t use more than one tag per structure field.</p> </p></div> </div> <h2 id="duplication-removes-coupling">Duplication removes coupling</h2> <p>The easiest way to reduce coupling is using separate models.</p> <p>We extract the API-specific part as HTTP models:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">CreateUserRequest</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">FirstName</span> <span class="kt">string</span> <span class="s">`json:&#34;first_name&#34; validate:&#34;required_without=LastName&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastName</span> <span class="kt">string</span> <span class="s">`json:&#34;last_name&#34; validate:&#34;required_without=FirstName&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Email</span> <span class="kt">string</span> <span class="s">`json:&#34;email&#34; validate:&#34;required,email&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">UpdateUserRequest</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">FirstName</span> <span class="o">*</span><span class="kt">string</span> <span class="s">`json:&#34;first_name&#34; validate:&#34;required_without=LastName&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastName</span> <span class="o">*</span><span class="kt">string</span> <span class="s">`json:&#34;last_name&#34; validate:&#34;required_without=FirstName&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">UserResponse</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">int</span> <span class="s">`json:&#34;id&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">FirstName</span> <span class="kt">string</span> <span class="s">`json:&#34;first_name&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastName</span> <span class="kt">string</span> <span class="s">`json:&#34;last_name&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">DisplayName</span> <span class="kt">string</span> <span class="s">`json:&#34;display_name&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Emails</span> <span class="p">[]</span><span class="nx">EmailResponse</span> <span class="s">`json:&#34;emails&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">EmailResponse</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Address</span> <span class="kt">string</span> <span class="s">`json:&#34;address&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Primary</span> <span class="kt">bool</span> <span class="s">`json:&#34;primary&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fab94d47350756716a7fed42b7ae546c51c11406a%2F01-coupling%2F02-loosely-coupled%2Finternal%2Fhttp.go%23L15" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/02-loosely-coupled/internal/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fab94d47350756716a7fed42b7ae546c51c11406a%2F01-coupling%2F02-loosely-coupled%2Finternal%2Fhttp.go%23L15" target="_blank">Full source</a> </div> <p>And the database-related part as storage models:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">UserDBModel</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">int</span> <span class="s">`gorm:&#34;column:id;primaryKey&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">FirstName</span> <span class="kt">string</span> <span class="s">`gorm:&#34;column:first_name&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastName</span> <span class="kt">string</span> <span class="s">`gorm:&#34;column:last_name&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Emails</span> <span class="p">[]</span><span class="nx">EmailDBModel</span> <span class="s">`gorm:&#34;foreignKey:UserID;constraint:OnDelete:CASCADE&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">PasswordHash</span> <span class="kt">string</span> <span class="s">`gorm:&#34;column:password_hash&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastIP</span> <span class="kt">string</span> <span class="s">`gorm:&#34;column:last_ip&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">CreatedAt</span> <span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> <span class="s">`gorm:&#34;column:created_at&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">UpdatedAt</span> <span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> <span class="s">`gorm:&#34;column:updated_at&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">EmailDBModel</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">int</span> <span class="s">`gorm:&#34;column:id;primaryKey&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Address</span> <span class="kt">string</span> <span class="s">`gorm:&#34;column:address;size:256;uniqueIndex&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Primary</span> <span class="kt">bool</span> <span class="s">`gorm:&#34;column:primary&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserID</span> <span class="kt">int</span> <span class="s">`gorm:&#34;column:user_id&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fab94d47350756716a7fed42b7ae546c51c11406a%2F01-coupling%2F02-loosely-coupled%2Finternal%2Fdb.go%23L16" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/02-loosely-coupled/internal/db.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fab94d47350756716a7fed42b7ae546c51c11406a%2F01-coupling%2F02-loosely-coupled%2Finternal%2Fdb.go%23L16" target="_blank">Full source</a> </div> <p>At first, it seemed we would use the same &ldquo;user&rdquo; model everywhere. Now, it&rsquo;s clear we&rsquo;ve avoided duplication too early. The API and storage structures are similar but different enough to use separate models.</p> <p>In web applications, <strong>the views your API returns (read models) are not the same thing you store in the database (write models).</strong></p> <p>The storage code should know nothing about the HTTP models, so we need to convert the structs.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1226" height="510" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcommon-anti-patterns-in-go-web-applications%2Fmapping-models_hu1b30ef4c243ca77dce56ca57e95afdbe_113690_1226x510_resize_q80_h2_lanczos_3.webp" alt="Mapping Models" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fcommon-anti-patterns-in-go-web-applications%5C%2Fmapping-models_hu1b30ef4c243ca77dce56ca57e95afdbe_113690_1226x510_resize_lanczos_3.png"" /> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">userResponseFromDBModel</span><span class="p">(</span><span class="nx">u</span> <span class="nx">UserDBModel</span><span class="p">)</span> <span class="nx">UserResponse</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">emails</span> <span class="p">[]</span><span class="nx">EmailResponse</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">e</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">u</span><span class="p">.</span><span class="nx">Emails</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">emails</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">emails</span><span class="p">,</span> <span class="nf">emailResponseFromDBModel</span><span class="p">(</span><span class="nx">e</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">UserResponse</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span><span class="p">:</span> <span class="nx">u</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">FirstName</span><span class="p">:</span> <span class="nx">u</span><span class="p">.</span><span class="nx">FirstName</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastName</span><span class="p">:</span> <span class="nx">u</span><span class="p">.</span><span class="nx">LastName</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">DisplayName</span><span class="p">:</span> <span class="nf">displayName</span><span class="p">(</span><span class="nx">u</span><span class="p">.</span><span class="nx">FirstName</span><span class="p">,</span> <span class="nx">u</span><span class="p">.</span><span class="nx">LastName</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">Emails</span><span class="p">:</span> <span class="nx">emails</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">emailResponseFromDBModel</span><span class="p">(</span><span class="nx">e</span> <span class="nx">EmailDBModel</span><span class="p">)</span> <span class="nx">EmailResponse</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">EmailResponse</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Address</span><span class="p">:</span> <span class="nx">e</span><span class="p">.</span><span class="nx">Address</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Primary</span><span class="p">:</span> <span class="nx">e</span><span class="p">.</span><span class="nx">Primary</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">userDBModelFromCreateRequest</span><span class="p">(</span><span class="nx">r</span> <span class="nx">CreateUserRequest</span><span class="p">)</span> <span class="nx">UserDBModel</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">UserDBModel</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">FirstName</span><span class="p">:</span> <span class="nx">r</span><span class="p">.</span><span class="nx">FirstName</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastName</span><span class="p">:</span> <span class="nx">r</span><span class="p">.</span><span class="nx">LastName</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Emails</span><span class="p">:</span> <span class="p">[]</span><span class="nx">EmailDBModel</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Address</span><span class="p">:</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Email</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fab94d47350756716a7fed42b7ae546c51c11406a%2F01-coupling%2F02-loosely-coupled%2Finternal%2Fhttp.go%23L210" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/02-loosely-coupled/internal/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fab94d47350756716a7fed42b7ae546c51c11406a%2F01-coupling%2F02-loosely-coupled%2Finternal%2Fhttp.go%23L210" target="_blank">Full source</a> </div> <p>That&rsquo;s all the code you need: functions mapping one type to another. <strong>Writing such banal code may seem boring, but it&rsquo;s essential for decoupling.</strong></p> <p>It&rsquo;s tempting to create a generic solution to map the structs, like marshaling or using <code>reflect</code>. <strong>Resist it.</strong> <strong>Writing the boilerplate takes less time and effort than debugging mapping edge cases.</strong> Plain functions are easy to understand for everyone in your team. Arcane converters will be hard to grasp, even for you, after some time.</p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: One Model, One Responsibility.</p> </div> <div class="notice-body"><p> Aim for loose coupling by using separate models. Write plain and obvious functions to convert between them. </p></div> </div> <p>If you&rsquo;re afraid of too much duplication, consider the worst-case scenario. If you end up with a few more structures that stay the same as the application grows, you can merge them back into one. <strong>In contrast to tightly-coupled code, fixing duplicated code is trivial.</strong></p> <h3 id="generate-the-boilerplate">Generate the Boilerplate</h3> <p>If you worry about writing all this code by hand, there&rsquo;s an idiomatic way to avoid it. <strong>Use libraries that generate the boilerplate for you.</strong></p> <p>You can generate things like:</p> <ul> <li>HTTP models and routes out of the OpenAPI definition (<a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdeepmap%2Foapi-codegen" target="_blank">oapi-codegen</a> and other libraries).</li> <li>Database models and related code out of the SQL schema (<a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvolatiletech%2Fsqlboiler" target="_blank">sqlboiler</a> and other ORMs).</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frobust-grpc-google-cloud-run">gRPC</a> models out of ProtoBuf files.</li> </ul> <p>Generated code gives you strong types, so you no longer pass <code>interface{}</code> to generic functions. You keep compile-time checks and don&rsquo;t need to write the code manually.</p> <p>Here&rsquo;s how the generated models look.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// PostUserRequest defines model for PostUserRequest. </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">PostUserRequest</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// E-mail </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">Email</span> <span class="kt">string</span> <span class="s">`json:&#34;email&#34;`</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// First name </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">FirstName</span> <span class="kt">string</span> <span class="s">`json:&#34;first_name&#34;`</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Last name </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">LastName</span> <span class="kt">string</span> <span class="s">`json:&#34;last_name&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// UserResponse defines model for UserResponse. </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">UserResponse</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">DisplayName</span> <span class="kt">string</span> <span class="s">`json:&#34;display_name&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Emails</span> <span class="p">[]</span><span class="nx">EmailResponse</span> <span class="s">`json:&#34;emails&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">FirstName</span> <span class="kt">string</span> <span class="s">`json:&#34;first_name&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Id</span> <span class="kt">int</span> <span class="s">`json:&#34;id&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastName</span> <span class="kt">string</span> <span class="s">`json:&#34;last_name&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fab94d47350756716a7fed42b7ae546c51c11406a%2F01-coupling%2F03-loosely-coupled-generated%2Finternal%2Fhttp_types.go%23L22" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/03-loosely-coupled-generated/internal/http_types.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fab94d47350756716a7fed42b7ae546c51c11406a%2F01-coupling%2F03-loosely-coupled-generated%2Finternal%2Fhttp_types.go%23L22" target="_blank">Full source</a> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">int64</span> <span class="s">`boil:&#34;id&#34; json:&#34;id&#34; toml:&#34;id&#34; yaml:&#34;id&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">FirstName</span> <span class="kt">string</span> <span class="s">`boil:&#34;first_name&#34; json:&#34;first_name&#34; toml:&#34;first_name&#34; yaml:&#34;first_name&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastName</span> <span class="kt">string</span> <span class="s">`boil:&#34;last_name&#34; json:&#34;last_name&#34; toml:&#34;last_name&#34; yaml:&#34;last_name&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">PasswordHash</span> <span class="nx">null</span><span class="p">.</span><span class="nx">String</span> <span class="s">`boil:&#34;password_hash&#34; json:&#34;password_hash,omitempty&#34; toml:&#34;password_hash&#34; yaml:&#34;password_hash,omitempty&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastIP</span> <span class="nx">null</span><span class="p">.</span><span class="nx">String</span> <span class="s">`boil:&#34;last_ip&#34; json:&#34;last_ip,omitempty&#34; toml:&#34;last_ip&#34; yaml:&#34;last_ip,omitempty&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">CreatedAt</span> <span class="nx">null</span><span class="p">.</span><span class="nx">Time</span> <span class="s">`boil:&#34;created_at&#34; json:&#34;created_at,omitempty&#34; toml:&#34;created_at&#34; yaml:&#34;created_at,omitempty&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">UpdatedAt</span> <span class="nx">null</span><span class="p">.</span><span class="nx">Time</span> <span class="s">`boil:&#34;updated_at&#34; json:&#34;updated_at,omitempty&#34; toml:&#34;updated_at&#34; yaml:&#34;updated_at,omitempty&#34;`</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">R</span> <span class="o">*</span><span class="nx">userR</span> <span class="s">`boil:&#34;-&#34; json:&#34;-&#34; toml:&#34;-&#34; yaml:&#34;-&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">L</span> <span class="nx">userL</span> <span class="s">`boil:&#34;-&#34; json:&#34;-&#34; toml:&#34;-&#34; yaml:&#34;-&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fab94d47350756716a7fed42b7ae546c51c11406a%2F01-coupling%2F03-loosely-coupled-generated%2Fmodels%2Fusers.go%23L26" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/03-loosely-coupled-generated/models/users.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2Fab94d47350756716a7fed42b7ae546c51c11406a%2F01-coupling%2F03-loosely-coupled-generated%2Fmodels%2Fusers.go%23L26" target="_blank">Full source</a> </div> <p>Sometimes, you may even want to write a code-generating tool. It&rsquo;s not that hard, and the result is regular Go code that everyone can read and understand. The common alternative is <code>reflect</code>, which is terrible to grasp and debug. Of course, first, consider if the effort is worth it. In most cases, writing the code by hand will be quick enough.</p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Generate the Repetitive Parts</p> </div> <div class="notice-body"><p> Generated code gives you strong types and compile-time safety. Choose it over <code>reflect</code>. </p></div> </div> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/common-anti-patterns-in-go-web-applications/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="web-applications;anti-patterns;go;golang"> <input type="hidden" name="fields[blog_series]" value=""> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h3 id="dont-overuse-libraries">Don&rsquo;t overuse libraries</h3> <p>Use the generated code only for what it&rsquo;s supposed to do. You want to avoid writing the boilerplate by hand, but you should still keep a few dedicated models. <strong>Don&rsquo;t end up with the single model anti-pattern.</strong></p> <p>It&rsquo;s easy to fall into this trap when you want to follow DRY.</p> <p>For example, both <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fkyleconroy%2Fsqlc" target="_blank">sqlc</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvolatiletech%2Fsqlboiler" target="_blank">sqlboiler</a> projects generate code out of SQL queries. sqlc allows adding JSON tags to the generated models and even lets you choose between <code>camelCase</code> and <code>snake_case</code>. sqlboiler adds <code>json</code>, <code>toml</code>, and <code>yaml</code> tags to all models by default. It&rsquo;s clear people use these models not only for storage.</p> <p>Looking through sqlc&rsquo;s issues, I&rsquo;ve found developers asking for even more flexibility, like <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fkyleconroy%2Fsqlc%2Fpull%2F756%2Ffiles" target="_blank">renaming the generated fields</a> or <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fkyleconroy%2Fsqlc%2Fissues%2F298" target="_blank">skipping some JSON fields entirely</a>. Someone even mentions they <em>Need some way to hide sensitive fields in REST API</em>.</p> <p>All this encourages keeping a single model for many responsibilities. It lets you write less code, but always consider if the coupling is worth it.</p> <p>Similarly, watch out for <em>magic</em> hidden in struct tags. For example, consider the permissions model that gorm supports:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> <span class="s">`gorm:&#34;&lt;-:create&#34;`</span> <span class="c1">// allow read and create </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">Name</span> <span class="kt">string</span> <span class="s">`gorm:&#34;&lt;-:update&#34;`</span> <span class="c1">// allow read and update </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">Name</span> <span class="kt">string</span> <span class="s">`gorm:&#34;&lt;-&#34;`</span> <span class="c1">// allow read and write (create and update) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">Name</span> <span class="kt">string</span> <span class="s">`gorm:&#34;&lt;-:false&#34;`</span> <span class="c1">// allow read, disable write permission </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">Name</span> <span class="kt">string</span> <span class="s">`gorm:&#34;-&gt;&#34;`</span> <span class="c1">// readonly (disable write permission unless it configured ) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">Name</span> <span class="kt">string</span> <span class="s">`gorm:&#34;-&gt;;&lt;-:create&#34;`</span> <span class="c1">// allow read and create </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">Name</span> <span class="kt">string</span> <span class="s">`gorm:&#34;-&gt;:false;&lt;-:create&#34;`</span> <span class="c1">// createonly (disabled read from db) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">Name</span> <span class="kt">string</span> <span class="s">`gorm:&#34;-&#34;`</span> <span class="c1">// ignore this field when write and read with struct </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgorm.io%2Fdocs%2Fmodels.html%23Field-Level-Permission" target="_blank">gorm.io/docs/models.html#Field-Level-Permission</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgorm.io%2Fdocs%2Fmodels.html%23Field-Level-Permission" target="_blank">Full source</a> </div> <p>You can also use quite complex comparisons using the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-playground%2Fvalidator" target="_blank">validator</a> library, like referencing other fields:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">FirstName</span> <span class="kt">string</span> <span class="s">`validate:&#34;required_without=LastName&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastName</span> <span class="kt">string</span> <span class="s">`validate:&#34;required_without=FirstName&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>It saves you a bit of time writing the code, but you give up the compile-time checks. It&rsquo;s easy to make a typo in a struct tag, and it&rsquo;s a risk using it for sensitive areas like validation and permissions. It&rsquo;s also confusing to anyone not familiar with the library&rsquo;s arcane syntax.</p> <p>I don&rsquo;t mean to pick on the mentioned libraries. They all have their uses, but these examples show how <strong>we tend to take DRY to the extremes, so we don&rsquo;t have to write more code.</strong></p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Choosing magic to save time writing code</p> </div> <div class="notice-body"><p> Don&rsquo;t overuse the libraries to avoid verbosity. </p></div> </div> <h2 id="avoid-implicit-tag-names">Avoid implicit tag names</h2> <p>Most libraries don&rsquo;t require the tags to be present and use field names by default.</p> <p>While refactoring the project, someone can rename a field, unaware they edit an API response or a database model. If there are no tags, this can break your API contract or even corrupt your storage.</p> <p>Always fill all the tags. Even if you have to type the same name twice, it&rsquo;s not against DRY.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Omitting Structure Tags</p> </div> <div class="notice-body"><p> <p>Don&rsquo;t skip struct tags if a library uses them.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Email</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">int</span> <span class="s">`gorm:&#34;primaryKey&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Address</span> <span class="kt">string</span> <span class="s">`gorm:&#34;size:256;uniqueIndex&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Primary</span> <span class="kt">bool</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserID</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div> </p></div> </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Explicit Structure Tags</p> </div> <div class="notice-body"><p> <p>Always fill the struct tags, even if the field names are the same.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Email</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">int</span> <span class="s">`gorm:&#34;column:id;primaryKey&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Address</span> <span class="kt">string</span> <span class="s">`gorm:&#34;column:address;size:256;uniqueIndex&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Primary</span> <span class="kt">bool</span> <span class="s">`gorm:&#34;column:primary&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserID</span> <span class="kt">int</span> <span class="s">`gorm:&#34;column:user_id&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div> </p></div> </div> <h2 id="separate-logic-from-implementation-details">Separate Logic from Implementation Details</h2> <p>Decoupling the API from the storage and using generated models is a good start. But we still keep validation in the HTTP handlers.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">createRequest</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Email</span> <span class="kt">string</span> <span class="s">`validate:&#34;required,email&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">FirstName</span> <span class="kt">string</span> <span class="s">`validate:&#34;required_without=LastName&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastName</span> <span class="kt">string</span> <span class="s">`validate:&#34;required_without=FirstName&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">validate</span> <span class="o">:=</span> <span class="nx">validator</span><span class="p">.</span><span class="nf">New</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="nx">err</span> <span class="p">=</span> <span class="nx">validate</span><span class="p">.</span><span class="nf">Struct</span><span class="p">(</span><span class="nf">createRequest</span><span class="p">(</span><span class="nx">postUserRequest</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusBadRequest</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F21d60c78dbd2c81800899dd9fd7169325fa6199a%2F01-coupling%2F03-loosely-coupled-generated%2Finternal%2Fhttp.go%23L83" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/03-loosely-coupled-generated/internal/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F21d60c78dbd2c81800899dd9fd7169325fa6199a%2F01-coupling%2F03-loosely-coupled-generated%2Finternal%2Fhttp.go%23L83" target="_blank">Full source</a> </div> <p><strong>Validation is just one part of the business logic you find in most web applications.</strong> Often, there will be more of it, like:</p> <ul> <li>showing fields only in particular cases,</li> <li>checking permissions,</li> <li>hiding fields depending on the role,</li> <li>calculating the price,</li> <li>taking action depending on a few factors.</li> </ul> <p>Mixing logic with implementation details (like keeping it in the HTTP handlers) is a quick way to deliver an MVP. But it also introduces the worst kind of technical debt. It&rsquo;s why you get vendor lock-in and why you keep adding hacks to support new features.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Mixing logic and details</p> </div> <div class="notice-body"><p> Don&rsquo;t mix your application logic with implementation details. </p></div> </div> <p>Business logic deserves its own layer. Changing the implementation (database engine, HTTP library, infrastructure, Pub/Sub, etc.) should be possible without any changes to the logic parts.</p> <p>You&rsquo;re not doing this separation because you expect to change the database. It rarely happens. <strong>But the separation of concerns makes your code easy to understand and modify. You know what you&rsquo;re changing, and there are no side effects.</strong> It&rsquo;s harder to introduce bugs in the most crucial parts.</p> <p>To separate the application layer, we need to add additional models and mappings.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1226" height="510" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcommon-anti-patterns-in-go-web-applications%2Fapplication-layer_hu61d56791a1dda7991606e12ed392204a_125693_1226x510_resize_q80_h2_lanczos_3.webp" alt="Application Layer" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fcommon-anti-patterns-in-go-web-applications%5C%2Fapplication-layer_hu61d56791a1dda7991606e12ed392204a_125693_1226x510_resize_lanczos_3.png"" /> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">id</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="nx">firstName</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">lastName</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">emails</span> <span class="p">[]</span><span class="nx">Email</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewUser</span><span class="p">(</span><span class="nx">firstName</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">lastName</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">emailAddress</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">firstName</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="o">&amp;&amp;</span> <span class="nx">lastName</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">User</span><span class="p">{},</span> <span class="nx">ErrNameRequired</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">email</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">NewEmail</span><span class="p">(</span><span class="nx">emailAddress</span><span class="p">,</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">User</span><span class="p">{},</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">User</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">firstName</span><span class="p">:</span> <span class="nx">firstName</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">lastName</span><span class="p">:</span> <span class="nx">lastName</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">emails</span><span class="p">:</span> <span class="p">[]</span><span class="nx">Email</span><span class="p">{</span><span class="nx">email</span><span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Email</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">address</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">primary</span> <span class="kt">bool</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewEmail</span><span class="p">(</span><span class="nx">address</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">primary</span> <span class="kt">bool</span><span class="p">)</span> <span class="p">(</span><span class="nx">Email</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">address</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Email</span><span class="p">{},</span> <span class="nx">ErrEmailRequired</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// A naive validation to make the example short, but you get the idea </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">if</span> <span class="p">!</span><span class="nx">strings</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="nx">address</span><span class="p">,</span> <span class="s">&#34;@&#34;</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Email</span><span class="p">{},</span> <span class="nx">ErrInvalidEmail</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Email</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">address</span><span class="p">:</span> <span class="nx">address</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">primary</span><span class="p">:</span> <span class="nx">primary</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F21d60c78dbd2c81800899dd9fd7169325fa6199a%2F01-coupling%2F04-loosely-coupled-app-layer%2Finternal%2Fuser.go%23L14" target="_blank">github.com/ThreeDotsLabs/go-web-app-antipatterns/01-coupling/04-loosely-coupled-app-layer/internal/user.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns%2Fblob%2F21d60c78dbd2c81800899dd9fd7169325fa6199a%2F01-coupling%2F04-loosely-coupled-app-layer%2Finternal%2Fuser.go%23L14" target="_blank">Full source</a> </div> <p>It&rsquo;s the code I&rsquo;d like to work with when I need to update the business logic. It&rsquo;s boring, obvious, and <strong>I know exactly what&rsquo;s going to change.</strong></p> <p>We do the same thing when adding another API, like gRPC, or an external system, like a Pub/Sub. Each part uses separate models, and we use those from the application layer to map them.</p> <p>Because the application models hold all validations and other business rules, it makes no difference whether we use them from an HTTP or gRPC API. <strong>The API is just an entry point to the application.</strong></p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1226" height="510" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcommon-anti-patterns-in-go-web-applications%2Fmore-models_hud7ed202435dd882ed8cbaa80aa5edb1c_125527_1226x510_resize_q80_h2_lanczos_3.webp" alt="More Models" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fcommon-anti-patterns-in-go-web-applications%5C%2Fmore-models_hud7ed202435dd882ed8cbaa80aa5edb1c_125527_1226x510_resize_lanczos_3.png"" /> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Application Layer</p> </div> <div class="notice-body"><p> Dedicate a separate layer to your product&rsquo;s most important code. </p></div> </div> <p>The code snippets above come from the same <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns" target="_blank">repository</a> and implement a classic &ldquo;users&rdquo; domain. All examples expose the same API and pass the same test suite.</p> <p>Here&rsquo;s how they compare:</p> <table> <thead> <tr> <th></th> <th>Tightly Coupled</th> <th>Loosely Coupled</th> <th>Loosely Coupled Generated</th> <th>Loosely Coupled App Layer</th> </tr> </thead> <tbody> <tr> <td>Coupling</td> <td>Strong</td> <td>Medium</td> <td>Medium</td> <td>Weak</td> </tr> <tr> <td>Boilerplate</td> <td>Manual</td> <td>Manual</td> <td>Generated</td> <td>Generated</td> </tr> <tr> <td>Lines of code</td> <td>292</td> <td>345</td> <td>298</td> <td>408</td> </tr> <tr> <td>Generated code</td> <td>0</td> <td>0</td> <td>2154</td> <td>2154</td> </tr> </tbody> </table> <h2 id="the-standard-go-project-structure">The Standard Go Project Structure</h2> <p>If you&rsquo;ve seen the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns" target="_blank">repository</a>, you may be surprised there&rsquo;s just a single package in each example.</p> <p>There&rsquo;s no official directory structure in Go. You can find many &ldquo;example microservice&rdquo; or &ldquo;REST boilerplate&rdquo; repositories that propose how to split the packages. They usually have a well-designed directory structure. Some even mention they follow &ldquo;Clean Architecture&rdquo; or &ldquo;Hexagonal Architecture&rdquo;.</p> <p>The first thing I check is how the example keeps models. Most often, it uses structures with JSON and database tags combined.</p> <p>It&rsquo;s an illusion: <strong>the packages look nicely separated on the outside, but in reality, the single model couples them.</strong> It&rsquo;s common even for popular examples that newcomers use to learn.</p> <p>Ironically, the &ldquo;Standard Go Project Structure&rdquo; discussion continues in the community, while the single model anti-pattern is widespread. <strong>If types couple your application, no directory structure will change it.</strong></p> <p>While you look at the example structures, keep in mind they could be designed for a different kind of application. No approach works equally well for an open-source infrastructure tool, a web application&rsquo;s backend, and the standard library.</p> <p>The problem with package hierarchy is similar to splitting microservices. The important part is not how you split them but how they&rsquo;re connected.</p> <p><strong>When you focus on loose coupling, the structure becomes obvious. You separate the implementation details from the business logic. You group things that refer to each other and keep separate things that don&rsquo;t.</strong></p> <p>In the examples I&rsquo;ve prepared, I could easily move HTTP-related and database-related code to separate packages. It would make the namespace less polluted. There&rsquo;s already no coupling between the models, so it&rsquo;s just a detail.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Overthinking the directory structure</p> </div> <div class="notice-body"><p> <p>Don&rsquo;t start the project by separating directories. However you do it, it&rsquo;s a convention.</p> <p>You&rsquo;re unlikely to do it right before you&rsquo;ve written any code.</p> </p></div> </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Loosely Coupled Code</p> </div> <div class="notice-body"><p> The important part isn&rsquo;t the directory structure but how packages and structures reference each other. </p></div> </div> <h2 id="keeping-it-simple">Keeping it simple</h2> <p>Let&rsquo;s say you want to create a user with an ID field. The simplest approach can look like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">string</span> <span class="s">`validate:&#34;required,len=32&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">u</span> <span class="nx">User</span><span class="p">)</span> <span class="nf">Validate</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">validate</span><span class="p">.</span><span class="nf">Struct</span><span class="p">(</span><span class="nx">u</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This code works. However, you can&rsquo;t tell if the struct is correct at any point. You rely on something to call the validation and handle the error correctly.</p> <p>Another approach follows the good old encapsulation.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">id</span> <span class="nx">UserID</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">UserID</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">id</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewUserID</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">UserID</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">id</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">UserID</span><span class="p">{},</span> <span class="nx">ErrEmptyID</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">id</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">32</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">UserID</span><span class="p">{},</span> <span class="nx">ErrInvalidLength</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">UserID</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">id</span><span class="p">:</span> <span class="nx">id</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">u</span> <span class="nx">UserID</span><span class="p">)</span> <span class="nf">String</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">u</span><span class="p">.</span><span class="nx">id</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This snippet is more explicit and verbose. If you create a new <code>UserID</code> and receive no error, you&rsquo;re sure it&rsquo;s valid. Otherwise, you can easily map the error to a proper response specific to your API.</p> <p>Whatever approach you choose, you need to model the essential complexity of the user&rsquo;s ID. From the pure implementation point of view, keeping the ID in a string is the simplest solution.</p> <p>Go is supposed to be simple, but it doesn&rsquo;t mean you should use only primitive types. For complex behaviors, use code that reflects how the product works. Otherwise, you&rsquo;ll end up with a simplified model of it.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Over-simplification</p> </div> <div class="notice-body"><p> Don&rsquo;t model complex behavior with trivial code. </p></div> </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Write obvious code</p> </div> <div class="notice-body"><p> <p>Be explicit, even if it&rsquo;s verbose.</p> <p>Use encapsulation to ensure your structs are always in a valid state.</p> </p></div> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>It&rsquo;s possible to create an empty structure outside the package, even if all fields are unexported. It&rsquo;s the only thing you have to check when accepting <code>UserID</code> as a parameter.</p> <p>You can either use <code>if id == UserID{}</code> or write a dedicated <code>IsZero()</code> method that does it.</p> </p></div> </div> <h2 id="starting-with-the-database-schema">Starting with the database schema</h2> <p>Let&rsquo;s consider adding teams that users create and join.</p> <p>Following the relational approach, we would add a <code>teams</code> table and another one that joins it with <code>users</code>. Let&rsquo;s call it <code>membership</code>.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1110" height="514" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcommon-anti-patterns-in-go-web-applications%2Frelational-db_huea3833d1e20997daff395b58e2df507a_76459_1110x514_resize_q80_h2_lanczos_3.webp" alt="Relational database" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fcommon-anti-patterns-in-go-web-applications%5C%2Frelational-db_huea3833d1e20997daff395b58e2df507a_76459_1110x514_resize_lanczos_3.png"" /> <p>We already keep <code>UserStorage</code>, so it&rsquo;s natural to add two more structs: <code>TeamStorage</code> and <code>MembershipStorage</code>. They expose CRUD methods for each table.</p> <p>A snippet adding a new team could look like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">CreateTeam</span><span class="p">(</span><span class="nx">teamName</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">ownerID</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">teamID</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">teamStorage</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="nx">teamName</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">membershipStorage</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="nx">teamID</span><span class="p">,</span> <span class="nx">ownerID</span><span class="p">,</span> <span class="nx">MemberRoleOwner</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This approach has one issue: we don&rsquo;t create the team and the membership entry within a transaction. We might end up with a team without an owner assigned if things go wrong.</p> <p>The first solution that comes to mind is passing a transaction object between methods.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">CreateTeam</span><span class="p">(</span><span class="nx">teamName</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">ownerID</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">tx</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">Begin</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Commit</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">rollbackErr</span> <span class="o">:=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">rollbackErr</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="s">&#34;Rollback failed:&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">teamID</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">teamStorage</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="nx">tx</span><span class="p">,</span> <span class="nx">teamName</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">membershipStorage</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="nx">tx</span><span class="p">,</span> <span class="nx">teamID</span><span class="p">,</span> <span class="nx">ownerID</span><span class="p">,</span> <span class="nx">MemberRoleOwner</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>However, this approach <strong>leaks the implementation details</strong> (transaction handling) to the logic layer. It pollutes a readable function with <code>defer</code>-based error handling.</p> <p>Here&rsquo;s an exercise: consider how we would model this in a document database. For example, we could keep all Members inside the Team document.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1100" height="760" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcommon-anti-patterns-in-go-web-applications%2Fdocument-db_hub9ad7711b1a8fa6213d8a9028a8856bf_108468_1100x760_resize_q80_h2_lanczos_3.webp" alt="Document database" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fcommon-anti-patterns-in-go-web-applications%5C%2Fdocument-db_hub9ad7711b1a8fa6213d8a9028a8856bf_108468_1100x760_resize_lanczos_3.png"" /> <p>In this scenario, adding members would be done in <code>TeamStorage</code>. We wouldn&rsquo;t need a separate <code>MembershipStorage</code>. Isn&rsquo;t it weird that switching the database changes our assumption about the models?</p> <p>It&rsquo;s now clear we&rsquo;ve leaked the implementation details by introducing the &ldquo;membership&rdquo; concept. By saying &ldquo;create a new membership,&rdquo; we would only confuse our sales or customer service colleagues. <strong>It&rsquo;s a major red flag when you start speaking a different language than the rest of the company.</strong></p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Starting with the database schema</p> </div> <div class="notice-body"><p> Don&rsquo;t base your models on the database schema. You will end up exposing implementation details. </p></div> </div> <p><code>TeamStorage</code> stores teams, but it&rsquo;s not about the <code>teams</code> SQL table. It&rsquo;s about our product&rsquo;s team concept.</p> <p>When you start modeling from the domain, you see actual behaviors instead of CRUD methods. You also notice the transaction boundaries.</p> <p>Everyone understands creating a team requires an owner, and we can expose a method for it. The method executes all queries within a transaction.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">teamStorage</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="nx">teamName</span><span class="p">,</span> <span class="nx">ownerID</span><span class="p">,</span> <span class="nx">MemberRoleOwner</span><span class="p">)</span> </span></span></code></pre></div><p>Similarly, we could keep a method for joining a team.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">teamStorage</span><span class="p">.</span><span class="nf">JoinTeam</span><span class="p">(</span><span class="nx">teamID</span><span class="p">,</span> <span class="nx">memberID</span><span class="p">,</span> <span class="nx">MemberRoleGuest</span><span class="p">)</span> </span></span></code></pre></div><p>The <code>membership</code> table is still there, but it&rsquo;s an implementation detail hidden in the <code>TeamStorage</code>.</p> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Start with the domain</p> </div> <div class="notice-body"><p> Your storage methods should follow the product&rsquo;s behavior. Don&rsquo;t leak transactions out of them. </p></div> </div> <h2 id="your-web-application-is-not-a-crud">Your Web Application is not a CRUD</h2> <p>Tutorials often feature &ldquo;simple CRUDs&rdquo;, so they seem to be the building block of any web application. It&rsquo;s a myth. If all your product needs is a CRUD, you waste time and money writing it from scratch.</p> <p>Frameworks and no-code tools make it easy to bootstrap CRUDs, but we still pay developers to build custom software. Even GitHub&rsquo;s Copilot won&rsquo;t know how your product works besides the boilerplate.</p> <p><strong>It&rsquo;s the special rules and weird details that make your application different. It&rsquo;s not some logic you sprinkle on top of the four CRUD operations.</strong> It&rsquo;s the core of the product you sell.</p> <p>In the MVP stage, it&rsquo;s tempting to start with a CRUD to build a working version quickly. But it&rsquo;s like using a spreadsheet instead of dedicated software. You get similar results at first, but each new feature requires more hacks.</p> <div class="notice warning"> <div class="notice-head"><p class="fw-bold">❌ Anti-pattern: Starting with a CRUD</p> </div> <div class="notice-body"><p> Don&rsquo;t design your application around the idea of four CRUD operations. </p></div> </div> <div class="notice tip"> <div class="notice-head"><p>✅ Tactic: Understand your domain</p> </div> <div class="notice-body"><p> Spend time to understand how your product works and model it in the code. </p></div> </div> <p>Many of the tactics I&rsquo;ve described are ideas behind well-known patterns:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthings-to-know-about-dry%2F">Single Responsibility Principle</a> from SOLID (a single model responsible for one thing).</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F">Clean Architecture</a> (loosely coupled packages, separating logic from details).</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbasic-cqrs-in-go%2F">CQRS</a> (using different read models and write models).</li> </ul> <p>Some are even close to Domain-Driven Design:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F">Value Objects</a> (keeping structures always in a valid state).</li> <li>Aggregate and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F">Repository</a> (saving domain objects transactionally regardless of the number of database tables).</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2F">Ubiquitous Language</a> (using a language everyone understands).</li> </ul> <p>These patterns seem linked mostly to enterprise applications. But most of them are about straightforward core ideas, like the tactics from this article. They apply just as well in web applications, which often deal with complex business behaviors.</p> <p>You don&rsquo;t need to read heavy books or copy how things work in other languages to follow these patterns. It&rsquo;s possible to write idiomatic Go code together with battle-proven techniques. If you want to learn more about them, check out <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F">our free e-book</a>.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Let us know in the comments if you&rsquo;d like to see more examples added to the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fgo-web-app-antipatterns" target="_blank">anti-patterns repository</a> and on what topics. </p></div> </div>Software Dark Ageshttps://threedots.tech/post/software-dark-ages/Wed, 16 Jun 2021 00:00:00 +0200https://threedots.tech/post/software-dark-ages/<p>A couple of years ago, I worked in a SaaS company that suffered from <strong>probably all possible issues with software development</strong>. Code was so complex that adding simples changes could take months. All tasks and the scope of the project were defined by the project manager alone. Developers didn&rsquo;t understand what problem they were solving. Without an understanding the customer&rsquo;s expectations, many implemented functionalities were useless. The development team was also not able to propose better solutions.</p> <p>Even though we had microservices, introducing one change often required changes in most of the services. The architecture was so tightly coupled that we were not able to deploy these &ldquo;microservices&rdquo; independently. The business didn&rsquo;t understand why adding <em>&ldquo;one button&rdquo;</em> may take two months. In the end, stakeholders didn&rsquo;t trust the development team anymore. We all were very frustrated. <strong>But the situation was not hopeless.</strong></p> <p><strong>I was lucky enough to be a bit familiar with Domain-Driven Design.</strong> I was far from being an expert in that field at that time. But my knowledge was solid enough to help the company minimize and even eliminate a big part of mentioned problems.</p> <p>Some time has passed, and these problems are not gone in other companies. Even if the solution for these problems exists and is not arcane knowledge. People seem to not be aware of that. Maybe it&rsquo;s because old techniques like GRASP <em>(1997)</em>, SOLID <em>(2000)</em> or DDD (Domain-Driven Design) <em>(2003)</em> are often forgotten or considered obsolete? It reminds me of the situation that happened in the historical Dark Ages, when the ancient knowledge was forgotten. Similarly, we can use the old ideas. They&rsquo;re still valid and can solve present-day issues, but they&rsquo;re often ignored. <strong>It&rsquo;s like we live in Software Dark Ages.</strong></p> <p>Another similarity is focusing on the wrong things. In the historical Dark Ages, religion put down science. In Software Dark Ages, infrastructure is putting down important software design techniques. I&rsquo;m not claiming that religion is not important. Spiritual life is super important, but not if you are suffering from hunger and illness. It&rsquo;s the same case for the infrastructure. <strong>Having awesome Kubernetes cluster and most fancy microservices infrastructure will not help you if your software design sucks.</strong></p> <p> <img title="" loading="lazy" decoding="async" class="img " width="800" height="671" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fat-least-we-had-fun_hud0de6d7c92c241e313c44033530e0b4e_94429_800x671_resize_q80_h2_lanczos.webp" alt="At least we had fun" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fsoftware-dark-ages%5C%2Fat-least-we-had-fun_hud0de6d7c92c241e313c44033530e0b4e_94429_800x671_resize_q80_lanczos.jpg"" /> <div class="code-ref"> Let me try to put another flowered and useless tool in the production...<br> </div></p> <h2 id="software-dark-ages-as-a-system-problem">Software Dark Ages as a system problem</h2> <p><strong>Software Dark Ages is a very strong self-perpetuating</strong> system. You can&rsquo;t fix the systemic problem without understanding the big picture. <em>System thinking</em> is a technique that helps to analyze such complex issues set. I used this technique to visualize the Software Dark Ages.</p> <p> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fsystem-thinking.jpg" style="display: block;" class="glightbox"> <img title="" loading="lazy" decoding="async" class="img " width="2629" height="1750" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fsystem-thinking_hucac976cba599bbc61aa53d2ee5ac6745_1637618_2629x1750_resize_q80_h2_lanczos.webp" alt="System thinking" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fsoftware-dark-ages%5C%2Fsystem-thinking_hucac976cba599bbc61aa53d2ee5ac6745_1637618_2629x1750_resize_q80_lanczos.jpg"" /> </a> <div class="code-ref"> The ultimate guide to creating the worst team ever.<br><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fsystem-thinking.jpg">See the full version.</a> </div></p> <p>You can see one big loop of things that accelerate each other. Without intervention, problems become bigger and bigger. How can Domain-Driven Design fix that?</p> <p>Unlike most famous programming gurus, we don&rsquo;t want you to just believe in our story. We could just make it up. <strong>Fortunately, we can explain why it works by science.</strong> More precisely, with the excellent <em>Accelerate: The Science of Lean Software and DevOps</em> book based on scientific research. The research described in the book mentions the characteristics of the best, and worst-performing teams. One of the most critical factors is loosely coupled architecture.</p> <p><strong>If you think that microservices may give you loosely coupled architecture &ndash; you are very wrong.</strong> I&rsquo;ve seen microservices that are more coupled than a monolith multiple times. This is why we need something more than just infrastructure solutions. This is the time when Domain-Driven Design (DDD) comes into play.</p> <blockquote> <p><p>This key architectural property enables teams to easily test and deploy individual components or services even as the organization and the number of systems it operates grow—that is, it allows organizations to increase their productivity as they scale.</p> <p>(&hellip;) employing the latest whizzy microservices architecture deployed on containers is no guarantee of higher performance if you ignore these characteristics.</p> <p>(&hellip;)</p> <p>Architectural approaches that enable this strategy include the use of bounded contexts and APIs as a way to decouple large domains into smaller, more loosely coupled units, and the use of test doubles and virtualization as a way to test services or components in isolation.</p> </p> <footer> <strong></strong> <cite>Accelerate: The Science of Lean Software and DevOps</cite> </footer> </blockquote> <h2 id="ddd-doesnt-work">DDD doesn&rsquo;t work</h2> <p>Maybe you know someone who tried DDD, and it didn&rsquo;t work for them?</p> <p>Maybe you worked with a person who didn&rsquo;t understand it well, tried to force these techniques, and made everything too complex?</p> <p>Maybe you&rsquo;ve seen on Twitter that some famous software engineer said that DDD doesn&rsquo;t work?</p> <p>Maybe for you, it&rsquo;s a legendary Holy Grail that someone claims work for them &ndash; but nobody has seen it yet.</p> <p> <img title="" loading="lazy" decoding="async" class="img " width="700" height="569" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fwe-know-what-we-are-doing_hu6872db9f76f9ef92a7b17bf8cb46a106_329513_700x569_resize_q80_h2_lanczos.webp" alt="We know what we are doing" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fsoftware-dark-ages%5C%2Fwe-know-what-we-are-doing_hu6872db9f76f9ef92a7b17bf8cb46a106_329513_700x569_resize_q80_lanczos.jpg"" /> <div class="code-ref"> We know what we are doing... kinda </div></p> <p>Let&rsquo;s not forget that we are living in the Software Dark Ages. There&rsquo;s one problem with ideas from the previous epoch &ndash; there is a chance that some people may miss the initial point of DDD. That&rsquo;s not surprising in the context of 2003 when DDD was proposed for the first time.</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="700" height="393" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fddd-2003_hu2def33f789130ef58e00df3a6e571b7b_59218_700x393_resize_q80_h2_lanczos.webp" alt="DDD in 2003" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fsoftware-dark-ages%5C%2Fddd-2003_hu2def33f789130ef58e00df3a6e571b7b_59218_700x393_resize_q80_lanczos.jpg"" /> <div class="code-ref"> DDD in 2003 </div></p> <p>It&rsquo;s not easy to enter the DDD world. A lot of books and articles are missing the most important DDD points by oversimplifying them. They are also often explained in abstract examples that are detached from reality. It&rsquo;s also not rare to see too long and too complex examples that are impossible to understand.</p> <p>Let me try to explain DDD in the simplest way.</p> <h2 id="from-the-dark-ages-to-the-renaissance">From the Dark Ages to the Renaissance</h2> <p>DDD techniques can be split into two parts. Tactical and strategic patterns. Tactical Domain-Driven Design patterns are about <strong>how</strong> to implement the solution in the code. There is no rocket science in the Tactical DDD &ndash; it&rsquo;s all about Object-Oriented Programming good practices. But before writing the code, you need to know <strong>what</strong> to implement. That&rsquo;s where Strategic DDD patterns come into the game.</p> <p>Many sources describing DDD spend most of the time covering tactical patterns. Sometimes, they even skip strategic patterns. <strong>You can practice DDD by using just the strategic patterns</strong>. In some projects using tactical DDD patterns is even overkill. Unfortunetly, most <strong>people are doing the totally opposite thing. They use just tactical patterns without the strategic part. That&rsquo;s super horrifying.</strong></p> <p>If anybody asked me if any silver bullet exists in product software engineering, I&rsquo;d have only one candidate: <em>Domain-Driven Design Strategic Patterns</em>. Strategic DDD helps us to get the answer about:</p> <ul> <li><strong>what problem you are solving?</strong></li> <li><strong>will your solution meet stakeholders and users expectations?</strong></li> <li><strong>how complex is the project?</strong></li> <li><strong>what features are not necessary?</strong></li> <li><strong>how to separate services to support fast development in the long term?</strong></li> </ul> <p>These questions are essential while implementing a new project, adding new functionality, or doing refactoring. <strong>Strategic DDD patterns give us a way to answer these questions consistently and predictably.</strong></p> <p>Some engineers tell me they are “just engineers”. They don’t care too much about who uses their software or why. They are just implementing, say, JIRA tasks – building services with some bytes on the input and some bytes on the output. Such a mindset leads to a big disconnection between engineers and their clients whose problems we, as engineers, are trying to solve. Without proper communication, it’s much harder to create solutions that help clients in the right way. This is the goal in the end – not simply to process bytes.</p> <p>It&rsquo;s tempting to spend a small amount of time on the planning phase of the project. To start coding as soon as possible and finish earlier. When anybody has such doubt, I like to say that <em>&ldquo;With 5 days of coding, you can save 1 day of planning&rdquo;</em>. The unasked questions will not magically disappear.</p> <p>The best way to overcome the Software Dark Ages is to attack it from multiple angles. Let&rsquo;s see how DDD patterns can attack the system.</p> <p> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fsystem-thinking-solved.jpg" style="display: block;" class="glightbox"> <img title="" loading="lazy" decoding="async" class="img " width="3021" height="1799" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fsystem-thinking-solved_hu224c75cc83c584a997ba92a27a9454da_4555826_3021x1799_resize_q80_h2_lanczos.webp" alt="System thinking" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fsoftware-dark-ages%5C%2Fsystem-thinking-solved_hu224c75cc83c584a997ba92a27a9454da_4555826_3021x1799_resize_q80_lanczos.jpg"" /> </a> <div class="code-ref"> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fsystem-thinking-solved.jpg">See the full version.</a> </div></p> <h3 id="event-storming">Event Storming</h3> <p>Event Storming is a game-changer for Strategic DDD patterns and software development in general. <strong>I can&rsquo;t believe why it&rsquo;s not adopted by every team in the world yet.</strong></p> <p><strong>Event Storming is a workshop during which people with questions (often developers) meet with people with answers (often stakeholders).</strong> During the session, they can quickly explore complex business domains. In the beginning, you are focusing on building an entirely working flow based on <em>Domain Events</em> (orange sticky notes). Event Storming is a super flexible method. Thanks to that, you can verify if your solution meets expected requirements. You can also explore data flow, potential problems, or UX depending on the session&rsquo;s goal.</p> <video width="100%" autoplay muted playsinline loop oncontextmenu="return false;"> <source src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fstorming-timelapse.mp4" type="video/mp4"> Your browser does not support the video tag. </video> <p><strong>Verifying if the solution has no gaps and is about what users asked takes minutes. Introducing changes and verifying ideas in the developed and deployed code is hugely more expensive. Changing a sticky note on the board is extremely cheap.</strong></p> <p>A civil engineer or a rocket engineer can quickly see the result of a mistake. They can see that something is clearly wrong before finishing the building process. It is not so simple with software because it’s not easily seen. Most of our critical decisions will not hurt anybody. Problems with features development and maintenance will not appear in a day.</p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/software-dark-ages/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="ddd;domain-driven design;event-storming;strategic-ddd;bounded-context"> <input type="hidden" name="fields[blog_series]" value=""> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <p>Event Storming works when you are <strong>planning both a big project or just a single story.</strong> It&rsquo;s just a matter of how much time you would like to spend. When we are using it for one story, it can be something between 10 minutes to a couple of hours. We tend to spend between one day to a couple of days of sessions for a bigger functionality.</p> <p>After the session, you should have the correct answer for the questions about:</p> <ul> <li><strong>what problems are you trying to solve</strong> &ndash; instead of guessing what may be useful for the end-user or assuming that <em>&ldquo;we know everything better&rdquo;</em>,</li> <li><strong>if stakeholders are happy with a proposed solution</strong> &ndash; rather than verifying that half-year later with the implementation,</li> <li><strong>complexity of the problem is easily visible</strong> &ndash; it makes clear why adding one button may require a ton of work,</li> <li><strong>initial idea of how you can split your microservices by responsibilities</strong> &ndash; instead of blindly grouping &ldquo;similar things&rdquo;.</li> </ul> <p>In the end, you will end up with much more <strong>trust from stakeholders</strong> because you are planning a solution <strong>together</strong>. It&rsquo;s a much better approach than isolated coding in the basement.</p> <p>What&rsquo;s excellent about Event Storming is that the outcome of a properly done session can be mapped directly to the code. It should help you avoid many discussions during development and speed up your work a lot.</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="900" height="486" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fstorming-to-code_hu991f197a2c0da82fbea390daa13af22d_61870_900x486_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fsoftware-dark-ages%5C%2Fstorming-to-code_hu991f197a2c0da82fbea390daa13af22d_61870_900x486_resize_q80_lanczos.jpg"" /> <div class="code-ref"> Transforming Event Storming directly to the code. </div></p> <p>You have to start with a clear purpose. Time can fly on a project, and before you know it, you have spent half a year on a project only to find that it’s not useful to anyone. Have you experienced that? It happens more often than you might think, which is why some people lose trust in “engineers” and how we can end up as developers without any autonomy.</p> <p>It&rsquo;s common to fear how much time we need to &ldquo;lose&rdquo; for running the session. <strong>Thinking about time lost for running session is not the right approach. You should instead think about the benefits that you will lose if you will not run the session.</strong> I heard the story when running one Event Storming session stopped the implementation of the project for a couple months. It may sound bad, but during the session the team found that current assumptions are totally invalid. Continuation of the project would lead to a complete failure. Even if the session may look time-consuming in the short term, the company avoided a couple of months of useless development.</p> <h3 id="event-modeling">Event Modeling</h3> <p>In 2018 Adam Dymitruk proposed the Event Modeling technique. The notation and idea are heavily based on the Event Storming technique but adds a couple of new features. It also puts an extra emphasis on the UX part of the session.</p> <p>In general, these techniques are pretty compatible. Even if you stay with Event Storming, you may find some valuable approaches from Event Modeling that you may use.</p> <p>You can read more about the technique on <a href="proxy.php?url=https%3A%2F%2Feventmodeling.org%2Fposts%2Fwhat-is-event-modeling%2F" target="_blank">eventmodeling.org</a>.</p> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fevent-modeling.jpg" style="display: block;" class="glightbox"> <img title="" loading="lazy" decoding="async" class="img img-center" width="700" height="409" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fevent-modeling_hu6748116e7c57067ba85b249bc7df119c_48993_700x409_resize_q80_h2_lanczos.webp" alt="Event Modeling" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fsoftware-dark-ages%5C%2Fevent-modeling_hu6748116e7c57067ba85b249bc7df119c_48993_700x409_resize_q80_lanczos.jpg"" /> </a> <h3 id="bounded-context-and-transaction-boundaries-aggregates">Bounded Context and transaction boundaries (aggregates)</h3> <p><strong>Bounded Context is another Strategic DDD pattern that helps us split big models into smaller, logical pieces.</strong></p> <p>It&rsquo;s a key for achieving <strong>proper</strong> services separation. If you need to touch half of the system to implement and test new functionality, your separation is wrong.</p> <p>Alternative to the wrong separation is lack of separation. Often, a symptom of lack of separation is god objects (huge objects that know too much or do too much). In that case, changes will primarily affect one service. The cost of that approach is a higher risk of big system failure and higher complexity of changes.</p> <p>In other words &ndash; in both cases, it will be harder to develop your project.</p> <p>A great tool that helps with the discovery of <em>Bounded Context</em> and <em>Aggregates</em> is (of course) Event Storming.</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="900" height="310" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-or-monolith%2Fstorming_hue472c784f896f464215c02a04dfcf03e_397383_900x310_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-or-monolith%5C%2Fstorming_hue472c784f896f464215c02a04dfcf03e_397383_900x310_resize_lanczos_3.png"" /> <div class="code-ref"> Event Storming artifact </div></p> <p>As a result of the session, you can visually see how you should split your services and touchpoints between them.</p> <h3 id="ubiquitous-language">Ubiquitous Language</h3> <p><strong>Ubiquitous Language is a Strategic DDD pattern that covers building up a common language between developers, operations, stakeholders, and users.</strong> It is the <strong>most underestimated Strategic DDD pattern.</strong> Because who cares about the language, right?</p> <p>It took me time to see <strong>how many communication issues between developers and non-developers are because of using a different language.</strong> And how painful it is. I&rsquo;d encourage you to pay attention to that as well. Because of miscommunication, <strong>developers aren&rsquo;t solving the right problem as nobody understands what&rsquo;s expected of them</strong>.</p> <p>Would you be surprised if I told you that Event Storming will help you develop Ubiquitous Language? Running the session together with stakeholders forces you to talk with them. <strong>It&rsquo;s hard to build a solution together when you can&rsquo;t understand each other.</strong> That&rsquo;s why it&rsquo;s critical to not miss your stakeholders at the workshop!</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="700" height="350" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fstorming-brainstorm_hu3293e70649e9a8fe84b81b3d9283ec86_83146_700x350_resize_q80_h2_lanczos.webp" alt="Brainstorm." onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fsoftware-dark-ages%5C%2Fstorming-brainstorm_hu3293e70649e9a8fe84b81b3d9283ec86_83146_700x350_resize_q80_lanczos.jpg"" /> <img title="" loading="lazy" decoding="async" class="img img-center" width="700" height="350" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fsoftware-dark-ages%2Fstorming-flow_hud87e3995281766aa31bd9b0ad777bca9_58819_700x350_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fpost%5C%2Fsoftware-dark-ages%5C%2Fstorming-flow_hud87e3995281766aa31bd9b0ad777bca9_58819_700x350_resize_q80_lanczos.jpg"" /> <div class="code-ref"> From brainstorm to working flow </div> <h2 id="does-ddd-solve-all-problems">Does DDD solve all problems?</h2> <p>Even if DDD is great, it doesn&rsquo;t solve all problems that we have. It&rsquo;s important to manage your expectations. <strong>Even if we use these techniques on a high level in my team, we still have doubts if the created design is good enough.</strong> Sometimes we don&rsquo;t know how to approach the problem. Sometimes we go back from the code to the design phase. Sometimes we make bad decisions. <strong>All of these are perfectly fine situations. There is no team in the world without these issues. It&rsquo;s better to assume that it will happen and not be surprised.</strong> But we know that without DDD, these issues would be much more significant.</p> <p>While using DDD, you should pay special attention to avoid:</p> <ul> <li>Big Design Up Front,</li> <li>implementing code &ldquo;for the future&rdquo;,</li> <li>trying to create anything perfect,</li> </ul> <p>Instead, you should:</p> <ul> <li><strong>Focus on delivering the MVP to the user in a short time</strong> (by short, I mean rather 1 month than 6 months).</li> <li><strong>If you need to implement something &ldquo;for the future&rdquo; because it will be harder to add it later</strong> &ndash; that&rsquo;s a very bad sign. You should think about how to make it easy to add it later.</li> <li><strong>Reconcile that even if you do your best, your design will not be perfect from the beginning</strong> &ndash; it&rsquo;s much better to improve it over time.</li> </ul> <p>For some teams, it may need a lot of work to go to this level. But I can promise you from my experience that it&rsquo;s possible. And the fun that you will have from delivering software again is worth it!</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>If you feel that you should be a tech leader to propose such improvements &ndash; you&rsquo;re wrong! In the early days, when I was not a leader, I was already proposing many improvements in the teams in which I worked. You need to have good arguments with your teammates.</p> <p>We always explain &ldquo;why&rdquo; the techniques work in our articles. When you will use these arguments, they should be enough to convince them. If it will not work because your team is close-minded, it&rsquo;s a good reason to consider changing the job.</p> </p></div> </div> <h2 id="software-renaissance">Software Renaissance</h2> <p>It&rsquo;s hard to go very deep with presented techniques within one article. My goal was rather to inspire you to question the status quo. <strong>This is not how our industry should work. If you are not okay with the status quo, I hope that I inspired you to learn new techniques.</strong> This is the best way to fight with the Software Dark Ages.</p> <p>If you want to learn about Tactical DDD patterns, you should check our <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Ftags%2Fddd%2F">previous articles</a>.</p> <p>The examples are written in Go, but they can be easily ported to any language. Go has a very low entry point as well. Who knows, maybe you will find a new favorite language? &#x1f609;</p> <p>What about Strategic DDD patterns? This article is actually an introduction to the next part of our article series. <strong>In the following months, we will deeply cover the most important Strategic DDD patterns</strong> on a fully working example project.</p> <p><strong>Who are you in the Software Dark Ages?</strong></p> <p>Just an ordinary hick who blindly follows the rules imposed by others?</p> <p>Inquisition, who will try to hate and stifle any unusual approach?</p> <p>Alchemist, trying to create gold? Even if it&rsquo;s not scientifically backed?</p> <p>Or maybe you are secretly reading forbidden books in your cellar? Maybe you are looking with us to finish the Software Dark Ages and to begin the Software Renaissance?</p> Let us know in the comments!Running integration tests with docker-compose in Google Cloud Buildhttps://threedots.tech/post/running-integration-tests-on-google-cloud-build/Thu, 01 Apr 2021 00:00:00 +0200https://threedots.tech/post/running-integration-tests-on-google-cloud-build/<p>This post is a direct follow-up to <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-test-architecture%2F">Microservices test architecture</a> where I introduced new kinds of tests to our example project.</p> <p><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">Wild Workouts</a> uses Google Cloud Build as its CI/CD platform. It&rsquo;s configured for <strong>continuous deployment</strong>, meaning changes land on production as soon as the pipeline passes.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="state-of-this-article-in-2026">State of this article in 2026</h4> <p>This article is kept as an archive. As of 2026, <strong>GitHub Actions is our default choice for CI/CD</strong>. It&rsquo;s simpler to set up, more widely adopted, and integrates seamlessly with GitHub repositories.</p> <p><strong>The core concepts in this article—running integration tests with docker-compose, testing against real dependencies, and structuring your test pipeline—remain relevant regardless of which CI/CD platform you choose.</strong> The patterns shown here translate directly to GitHub Actions or any other CI system that supports Docker.</p> </p></div> </div> <p>If you consider our current setup, it&rsquo;s both brave and naive. We have no tests running that could save us from obvious mistakes (the not-so-obvious mistakes can rarely be caught by tests, anyway).</p> <p>In this article I will show how to run integration, component, and end-to-end tests on Google Cloud Build using docker-compose.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="this-is-not-just-another-article-with-random-code-snippets">This is not just another article with random code snippets.</h4> <p>This post is part of a bigger series where we show how to build <strong>Go applications that are easy to develop, maintain, and fun to work with in the long term.</strong> We are doing it by sharing proven techniques based on many experiments we did with teams we lead and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%3Futm_source%3Dabout-wild-workouts%23thats-great-but-do-you-have-any-evidence-it-works">scientific research</a>.</p> <p>You can learn these patterns by building with us a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%3Futm_source%3Dabout-wild-workouts%23what-wild-workouts-can-do">fully functional</a> example Go web application &ndash; <strong>Wild Workouts</strong>.</p> <p>We did one thing differently &ndash; <strong>we included some subtle issues to the initial Wild Workouts implementation</strong>. Have we lost our minds to do that? Not yet. 😉 These issues are common for many Go projects. <strong>In the long term, these small issues become critical and stop adding new features.</strong></p> <p><strong>It&rsquo;s one of the essential skills of a senior or lead developer; you always need to keep long-term implications in mind.</strong></p> <p>We will fix them by <strong>refactoring</strong> Wild Workouts. In that way, you will quickly understand the techniques we share.</p> <p>Do you know that feeling after reading an article about some technique and trying implement it only to be blocked by some issues skipped in the guide? Cutting these details makes articles shorter and increases page views, but this is not our goal. Our goal is to create content that provides enough know-how to apply presented techniques. If you did not read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">previous articles from the series</a> yet, we highly recommend doing that.</p> <p>We believe that in some areas, there are no shortcuts. If you want to build complex applications in a fast and efficient way, you need to spend some time learning that. If it was simple, we wouldn&rsquo;t have large amounts of scary legacy code.</p> <p>Here&rsquo;s <strong><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">the full list of 14 articles</a></strong> released so far.</p> <p><strong>The full source code</strong> of Wild Workouts is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%3Futm_source%3Dabout-wild-workouts" target="_blank">GitHub</a>. Don&rsquo;t forget to leave a star for our project! ⭐</p> </p></div> </div> <h2 id="the-current-config">The current config</h2> <p>Let&rsquo;s look at the current <code>cloudbuild.yaml</code> file. While it&rsquo;s simple, most steps run several times because we keep three microservices in a single repository. I&rsquo;ll focus on the backend part and skip all config related to frontend deployment.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">steps</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainer-lint</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">golang</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/lint.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainings-lint</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">golang</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/lint.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainings]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">users-lint</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">golang</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/lint.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">users]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainer-docker</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloud-builders/docker</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/build-docker.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;trainer&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;$PROJECT_ID&#34;</span><span class="p">]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer-lint]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainings-docker</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloud-builders/docker</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/build-docker.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;trainings&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;$PROJECT_ID&#34;</span><span class="p">]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainings-lint]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">users-docker</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloud-builders/docker</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/build-docker.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;users&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;$PROJECT_ID&#34;</span><span class="p">]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">users-lint]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainer-http-deploy</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloud-builders/gcloud</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/deploy.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer, http, &#34;$PROJECT_ID&#34;]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer-docker]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainer-grpc-deploy</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloud-builders/gcloud</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/deploy.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer, grpc, &#34;$PROJECT_ID&#34;]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer-docker]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainings-http-deploy</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloud-builders/gcloud</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/deploy.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainings, http, &#34;$PROJECT_ID&#34;]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainings-docker]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">users-http-deploy</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloud-builders/gcloud</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/deploy.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">users, http, &#34;$PROJECT_ID&#34;]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">users-docker]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">users-grpc-deploy</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloud-builders/gcloud</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/deploy.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">users, grpc, &#34;$PROJECT_ID&#34;]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">users-docker]</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F227e540bb22d0265ed88286d1eb7714d02dc45be%2Fcloudbuild.yaml" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/cloudbuild.yaml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F227e540bb22d0265ed88286d1eb7714d02dc45be%2Fcloudbuild.yaml" target="_blank">Full source</a> </div> <p>Notice the <code>waitFor</code> key. It makes a step wait only for other specified steps. Some jobs can run in parallel this way.</p> <p>Here&rsquo;s a more readable version of what&rsquo;s going on:</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="4974" height="3216" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Frunning-integration-tests-on-google-cloud-build%2Finitial-pipeline_hu7a5b5d5cc96d9ada230aeb87ed5b9159_990727_4974x3216_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Frunning-integration-tests-on-google-cloud-build%5C%2Finitial-pipeline_hu7a5b5d5cc96d9ada230aeb87ed5b9159_990727_4974x3216_resize_q80_lanczos.jpg"" /> <p>We have a similar workflow for each service: lint (static analysis), build the Docker image, and deploy it as one or two Cloud Run services.</p> <p>Since our test suite is ready and works locally, we need to figure out how to plug it into the pipeline.</p> <h2 id="docker-compose">Docker Compose</h2> <p>We already have one docker-compose definition, and I would like to keep it this way. We will use it for:</p> <ul> <li>running the application locally,</li> <li>running tests locally,</li> <li>running tests in the CI.</li> </ul> <p>These three targets have different needs. For example, when running the application locally, we want hot code reloading. But that&rsquo;s pointless in CI. On the other hand, we can&rsquo;t expose ports on <code>localhost</code> in CI, which is the easiest way to reach the application in the local environment.</p> <p>Luckily, docker-compose is flexible enough to support all these use cases. We&rsquo;ll use a base <code>docker-compose.yml</code> file and an additional <code>docker-compose.ci.yml</code> file with overrides just for CI. You can run it by passing both files using the <code>-f</code> flag (notice there&rsquo;s one flag for each file). Keys from the files merge in the order provided.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d </span></span></code></pre></div><div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Typically, docker-compose looks for the <code>docker-compose.yml</code> file in the current directory or parent directories. Using the <code>-f</code> flag disables this behavior, so only specified files are parsed. </p></div> </div> <p>To run this on Cloud Build, we can use the <code>docker/compose</code> image.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">docker-compose</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;docker/compose:1.19.0&#39;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;-f&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;docker-compose.yml&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-f&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;docker-compose.ci.yml&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;up&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-d&#39;</span><span class="p">]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">env</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s1">&#39;PROJECT_ID=$PROJECT_ID&#39;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer-docker, trainings-docker, users-docker]</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2Fcloudbuild.yaml%23L31" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/cloudbuild.yaml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2Fcloudbuild.yaml%23L31" target="_blank">Full source</a> </div> <p>Since we filled <code>waitFor</code> with proper step names, we can be sure the correct images are present. Here&rsquo;s what we just added:</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="5306" height="3015" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Frunning-integration-tests-on-google-cloud-build%2Fdocker-compose-up_hu7a5b5d5cc96d9ada230aeb87ed5b9159_772584_5306x3015_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Frunning-integration-tests-on-google-cloud-build%5C%2Fdocker-compose-up_hu7a5b5d5cc96d9ada230aeb87ed5b9159_772584_5306x3015_resize_q80_lanczos.jpg"" /> <p>The first override we add to <code>docker-compose.ci.yml</code> makes each service use Docker images by tag instead of building from <code>docker/app/Dockerfile</code>. This ensures our tests check the same images we&rsquo;re going to deploy.</p> <p>Note the <code>${PROJECT_ID}</code> variable in the <code>image</code> keys. This needs to be the production project, so we can&rsquo;t hardcode it in the repository. Cloud Build provides this variable in each step, so we pass it to the <code>docker-compose up</code> command (see the definition above).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">trainer-http</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;gcr.io/${PROJECT_ID}/trainer&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">trainer-grpc</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;gcr.io/${PROJECT_ID}/trainer&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">trainings-http</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;gcr.io/${PROJECT_ID}/trainings&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">users-http</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;gcr.io/${PROJECT_ID}/users&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">users-grpc</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;gcr.io/${PROJECT_ID}/users&#34;</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2Fdocker-compose.ci.yml%23L2" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/docker-compose.ci.yml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2Fdocker-compose.ci.yml%23L2" target="_blank">Full source</a> </div> <h2 id="network">Network</h2> <p>Many CI systems use Docker today, typically running each step inside a container with the chosen image. Using docker-compose in CI is a bit trickier, as it usually means running Docker containers from within a Docker container.</p> <p>On Google Cloud Build, all containers live inside the <a href="proxy.php?url=https%3A%2F%2Fcloud.google.com%2Fbuild%2Fdocs%2Fbuild-config%23network" target="_blank"><code>cloudbuild</code> network</a>. Adding this network as the default for our <code>docker-compose.ci.yml</code> is enough for CI steps to connect to the docker-compose services.</p> <p>Here&rsquo;s the second part of our override file:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">networks</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="k">default</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="nx">external</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="nx">name</span><span class="p">:</span> <span class="nx">cloudbuild</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2Fdocker-compose.ci.yml%23L17" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/docker-compose.ci.yml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2Fdocker-compose.ci.yml%23L17" target="_blank">Full source</a> </div> <h2 id="environment-variables">Environment variables</h2> <p>Using environment variables for configuration seems simple at first, but it quickly becomes complex considering how many scenarios we need to handle. Let&rsquo;s list all of them:</p> <ul> <li>Running the application locally</li> <li>Running component tests locally</li> <li>Running component tests in CI</li> <li>Running end-to-end tests locally</li> <li>Running end-to-end tests in CI</li> </ul> <p>I didn&rsquo;t include running the application on production, as it doesn&rsquo;t use docker-compose.</p> <p>Why are component and end-to-end tests separate scenarios? Component tests spin up services on demand, while end-to-end tests communicate with services already running within docker-compose. This means both types use different endpoints to reach the services.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> For more details on component and end-to-end tests, see <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-test-architecture%2F">the previous article</a>. The short version: we focus coverage on component tests, which don't include external services. End-to-end tests just confirm the contract isn't broken at a high level, and only for the most critical path. This is the key to decoupled services. </p></div> </div> <p>We already keep a base <code>.env</code> file that holds most variables. It&rsquo;s passed to each service in the docker-compose definition.</p> <p>Additionally, docker-compose loads this file automatically when it finds it in the working directory. This lets us use the variables inside the YAML definition as well.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">trainer-http</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="l">docker/app</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># The $PORT variable comes from the .env file</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;127.0.0.1:3000:$PORT&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">env_file</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># All variables from .env are passed to the service</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">.env</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="c"># (part of the definition omitted)</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa3f4a35ff14a33950eb5caebe54d27b25bcf69e3%2Fdocker-compose.yml%23L23" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/docker-compose.yml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa3f4a35ff14a33950eb5caebe54d27b25bcf69e3%2Fdocker-compose.yml%23L23" target="_blank">Full source</a> </div> <p>We also need these variables loaded when running tests. That&rsquo;s pretty easy to do in bash:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">source</span> .env </span></span><span class="line"><span class="cl"><span class="c1"># exactly the same thing</span> </span></span><span class="line"><span class="cl">. .env </span></span></code></pre></div><p>However, the variables in our <code>.env</code> file have no <code>export</code> prefix, so they won&rsquo;t be passed to applications running in the shell. We can&rsquo;t use the prefix because it&rsquo;s incompatible with the syntax docker-compose expects.</p> <p>Additionally, we can&rsquo;t use a single file for all scenarios. We need variable overrides, just as we did with the docker-compose definition. My idea is to keep one additional file for each scenario, loaded together with the base <code>.env</code> file.</p> <p>Let&rsquo;s see the differences between all scenarios. For clarity, I&rsquo;ve included only users-http, but the idea applies to all services.</p> <table> <thead> <tr> <th>Scenario</th> <th>MySQL host</th> <th>Firestore host</th> <th>users-http address</th> <th>File</th> </tr> </thead> <tbody> <tr> <td>Running locally</td> <td>localhost</td> <td>localhost</td> <td>localhost:3002</td> <td><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2F.env" target="_blank">.env</a></td> </tr> <tr> <td>Local component tests</td> <td>localhost</td> <td>localhost</td> <td>localhost:5002</td> <td><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2F.test.env" target="_blank">.test.env</a></td> </tr> <tr> <td>CI component tests</td> <td>mysql</td> <td>firestore-component-tests</td> <td>localhost:5002</td> <td><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2F.test.ci.env" target="_blank">.test.ci.env</a></td> </tr> <tr> <td>Local end-to-end tests</td> <td>-</td> <td>-</td> <td>localhost:3002</td> <td><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2F.e2e.env" target="_blank">.e2e.env</a></td> </tr> <tr> <td>CI end-to-end tests</td> <td>-</td> <td>-</td> <td>users-http:3000</td> <td><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2F.e2e.ci.env" target="_blank">.e2e.ci.env</a></td> </tr> </tbody> </table> <p>Services run by docker-compose use ports 3000+, and component tests start services on ports 5000+. This way, both can run at the same time.</p> <p>I created a bash script that reads the variables and runs tests. <strong>Please don&rsquo;t try to define such a complex scenario directly in the Makefile. Make is terrible at managing environment variables.</strong> Your mental health is at stake.</p> <p>Another reason for creating a dedicated script is that we keep three services in one repository and end-to-end tests in a separate directory. If I need to run the same command multiple times, I prefer calling a script with two variables rather than a long incantation of flags and arguments.</p> <p>A third argument in favor of separate bash scripts: they can be linted with <a href="proxy.php?url=https%3A%2F%2Fwww.shellcheck.net%2F" target="_blank">shellcheck</a>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash </span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="nb">set</span> -e </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">readonly</span> <span class="nv">service</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"><span class="nb">readonly</span> <span class="nv">env_file</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">cd</span> <span class="s2">&#34;./internal/</span><span class="nv">$service</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl">env <span class="k">$(</span>cat <span class="s2">&#34;../../.env&#34;</span> <span class="s2">&#34;../../</span><span class="nv">$env_file</span><span class="s2">&#34;</span> <span class="p">|</span> grep -Ev <span class="s1">&#39;^#&#39;</span> <span class="p">|</span> xargs<span class="k">)</span> go <span class="nb">test</span> -count<span class="o">=</span><span class="m">1</span> ./... </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2Fscripts%2Ftest.sh" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/scripts/test.sh</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2Fscripts%2Ftest.sh" target="_blank">Full source</a> </div> <p>The script runs <code>go test</code> in the given directory with environment variables loaded from <code>.env</code> and the specified file. The <code>env / xargs</code> trick passes all variables to the following command. Notice how we remove comments from the file with <code>grep</code>.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h3 id="testing-cache">Testing cache</h3> <p><code>go test</code> caches successful results, as long as the related files are not modified.</p> <p>With tests that use Docker, you might change something at the infrastructure level, like the <code>docker-compose</code> definition or some environment variables. <code>go test</code> won&rsquo;t detect this, and you can mistake a cached test for a successful one.</p> <p>It&rsquo;s easy to get confused by this, and since our tests are fast anyway, we can disable the cache. The <code>-count=1</code> flag is an idiomatic (though not obvious) way to do it.</p> </p></div> </div> <h2 id="running-tests">Running tests</h2> <p>I have the end-to-end tests running after tests for all services pass. This should resemble how you would typically run them. <strong>Remember, end-to-end tests should serve as a double-check, and each service&rsquo;s own tests should have the most coverage.</strong></p> <p>Because our end-to-end tests are small in scope, we can run them before deploying the services. If they ran for a long time, this could block our deployments. A better approach in that scenario would be to rely on each service&rsquo;s component tests and run the end-to-end suite in parallel.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainer-tests</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">golang</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/test.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;trainer&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;.test.ci.env&#34;</span><span class="p">]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">docker-compose]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainings-tests</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">golang</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/test.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;trainings&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;.test.ci.env&#34;</span><span class="p">]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">docker-compose]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">users-tests</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">golang</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/test.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;users&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;.test.ci.env&#34;</span><span class="p">]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">docker-compose]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">e2e-tests</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">golang</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/test.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;common&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;.e2e.ci.env&#34;</span><span class="p">]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer-tests, trainings-tests, users-tests]</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2Fcloudbuild.yaml%23L38" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/cloudbuild.yaml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2Fcloudbuild.yaml%23L38" target="_blank">Full source</a> </div> <p>The last thing we add is running <code>docker-compose down</code> after all tests pass. This is just cleanup.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">docker-compose-down</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;docker/compose:1.19.0&#39;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;-f&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;docker-compose.yml&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-f&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;docker-compose.ci.yml&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;down&#39;</span><span class="p">]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">env</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s1">&#39;PROJECT_ID=$PROJECT_ID&#39;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">e2e-tests]</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2Fcloudbuild.yaml%23L59" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/cloudbuild.yaml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2Fcloudbuild.yaml%23L59" target="_blank">Full source</a> </div> <p>The second part of our pipeline looks like this now:</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="6170" height="2593" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Frunning-integration-tests-on-google-cloud-build%2Ffinal-pipeline_hu7a5b5d5cc96d9ada230aeb87ed5b9159_991562_6170x2593_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Frunning-integration-tests-on-google-cloud-build%5C%2Ffinal-pipeline_hu7a5b5d5cc96d9ada230aeb87ed5b9159_991562_6170x2593_resize_q80_lanczos.jpg"" /> <p>Here&rsquo;s how running the tests locally looks (I introduced this <code>make</code> target in the previous article). The commands are exactly the same as in CI, just with different <code>.env</code> files.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Makefile" data-lang="Makefile"><span class="line"><span class="cl"><span class="nf">test</span><span class="o">:</span> </span></span><span class="line"><span class="cl"> @./scripts/test.sh common .e2e.env </span></span><span class="line"><span class="cl"> @./scripts/test.sh trainer .test.env </span></span><span class="line"><span class="cl"> @./scripts/test.sh trainings .test.env </span></span><span class="line"><span class="cl"> @./scripts/test.sh users .test.env </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2FMakefile%23L60" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/Makefile</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fd6077806771e0d22d62f8dba5ae6b64d554d6ea0%2FMakefile%23L60" target="_blank">Full source</a> </div> <h2 id="separating-tests">Separating tests</h2> <p>Looking at the table from the previous article, we could split tests into two groups based on whether they use Docker.</p> <table> <thead> <tr> <th>Feature</th> <th>Unit</th> <th>Integration</th> <th>Component</th> <th>End-to-End</th> </tr> </thead> <tbody> <tr> <td><strong>Docker database</strong></td> <td><strong>No</strong></td> <td><strong>Yes</strong></td> <td><strong>Yes</strong></td> <td><strong>Yes</strong></td> </tr> </tbody> </table> <p>Unit tests are the only category not using a Docker database, while integration, component, and end-to-end tests do.</p> <p>Even though we made all our tests fast and stable, setting up Docker infrastructure adds some overhead. It&rsquo;s helpful to run all unit tests separately as a first guard against mistakes.</p> <p>We could use build tags to separate tests that don&rsquo;t use Docker. You can define the build tags in the first line of a file.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// +build docker </span></span></span></code></pre></div><p>We could now run unit tests separately from all tests. For example, the command below would run only tests that need Docker services:</p> <pre tabindex="0"><code>go test -tags=docker ./... </code></pre><p>Another way to separate tests is using the <code>-short</code> flag and checking <a href="proxy.php?url=https%3A%2F%2Fgolang.org%2Fpkg%2Ftesting%2F%23Short" target="_blank"><code>testing.Short()</code></a> in each test case.</p> <p>In the end, I decided not to introduce this separation. Our tests are stable and fast enough that running them all at once is not an issue. However, our project is small, and the test suite covers just the critical paths. As it grows, it might be a good idea to introduce build tags in component tests.</p> <h2 id="digression-a-short-story-about-ci-debugging">Digression: A short story about CI debugging</h2> <p>While I was making changes for this post, the initial test runs on Cloud Build kept failing. According to the logs, tests couldn&rsquo;t reach services from docker-compose.</p> <p>I started debugging and added a simple bash script that would connect to the services via <code>telnet</code>.</p> <p>To my surprise, connecting to <code>mysql:3306</code> worked correctly, but <code>firestore:8787</code> didn&rsquo;t, and the same for all Wild Workouts services.</p> <p>I thought this was because docker-compose takes a long time to start, but any number of retries didn&rsquo;t help. Finally, I decided to try something crazy and set up a reverse SSH tunnel from one of the containers in docker-compose.</p> <p>This let me SSH into one of the containers while the build was still running. I then tried <code>telnet</code> and <code>curl</code>, and they worked correctly for all services.</p> <p>Finally, I spotted a bug in the bash script I used.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">readonly</span> <span class="nv">host</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"><span class="nb">readonly</span> <span class="nv">port</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># (some retries code)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">telnet <span class="s2">&#34;</span><span class="nv">$host</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$port</span><span class="s2">&#34;</span> </span></span></code></pre></div><p>The typo in the variable definition caused the <code>telnet</code> command to run like this: <code>telnet $host $host</code>. So why did it work for MySQL? It turns out telnet recognizes ports defined in <code>/etc/services</code>. So <code>telnet mysql mysql</code> got translated to <code>telnet mysql 3306</code> and worked fine, but it failed for any other service.</p> <p>But why did the tests fail? Well, it turned out to be a totally different reason.</p> <p>Originally, we connected to MySQL like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">config</span> <span class="o">:=</span> <span class="nx">mysql</span><span class="p">.</span><span class="nx">Config</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Addr</span><span class="p">:</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;MYSQL_ADDR&#34;</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">User</span><span class="p">:</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;MYSQL_USER&#34;</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">Passwd</span><span class="p">:</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;MYSQL_PASSWORD&#34;</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">DBName</span><span class="p">:</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;MYSQL_DATABASE&#34;</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">ParseTime</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="c1">// with that parameter, we can use time.Time in mysqlHour.Hour </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">db</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">sqlx</span><span class="p">.</span><span class="nf">Connect</span><span class="p">(</span><span class="s">&#34;mysql&#34;</span><span class="p">,</span> <span class="nx">config</span><span class="p">.</span><span class="nf">FormatDSN</span><span class="p">())</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;cannot connect to MySQL&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0e3e9d80eb14639bc42935795f7ca3b73da36304%2Finternal%2Ftrainer%2Fadapters%2Fhour_mysql_repository.go%23L184" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/adapters/hour_mysql_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0e3e9d80eb14639bc42935795f7ca3b73da36304%2Finternal%2Ftrainer%2Fadapters%2Fhour_mysql_repository.go%23L184" target="_blank">Full source</a> </div> <p>I looked into environment variables, and all of them were filled correctly. After adding some <code>fmt.Println()</code> debugs, <strong>I found that the config&rsquo;s <code>Addr</code> part is completely ignored by the MySQL client because we didn&rsquo;t specify the <code>Net</code> field.</strong> Why did it work for local tests? Because MySQL was exposed on <code>localhost</code>, which is the default address.</p> <p>The other test failed to connect to one of the Wild Workouts services, and it turned out to be because I used an incorrect port in the .env file.</p> <p>Why am I sharing this at all? I think it&rsquo;s a great example of how working with CI systems can often look. <strong>When multiple things can fail, it&rsquo;s easy to draw wrong conclusions and dig deep in the wrong direction.</strong></p> <p>When in doubt, I like to reach for basic tools for investigating Linux issues, like <code>strace</code>, <code>curl</code>, or <code>telnet</code>. That&rsquo;s also why I set up the reverse SSH tunnel, and I&rsquo;m glad I did because it seems like a great way to debug issues inside CI. I feel I&rsquo;ll use it again sometime. &#x1f604;</p> <h2 id="summary">Summary</h2> <p>We managed to keep a single docker-compose definition for running tests both locally and in the pipeline. The entire Cloud Build run from <code>git push</code> to production takes 4 minutes.</p> <p>We used a few clever hacks, but that&rsquo;s normal when dealing with CI. Sometimes you just can&rsquo;t avoid adding some bash magic to make things work.</p> <p>Unlike domain code, hacks in your CI setup shouldn&rsquo;t hurt too much, as long as there are only a few of them. Just make sure it&rsquo;s easy to understand what&rsquo;s going on, so you&rsquo;re not the only person with all the knowledge.</p> With this post, we wrap up the first refactoring session of Wild Workouts. We&rsquo;re going to move on to strategic patterns now. Thanks for reading, and see you soon! 👋Repository secure by design: how to sleep better without fear of security vulnerabilitieshttps://threedots.tech/post/repository-secure-by-design/Tue, 09 Feb 2021 00:00:00 +0100https://threedots.tech/post/repository-secure-by-design/<p>Thanks to tests and code review, you can make your project bug-free. Right? Well&hellip; actually, probably not. That would be too easy. &#x1f609; These techniques lower the chance of bugs, but they can&rsquo;t eliminate them entirely. Does that mean we need to live with the risk of bugs until the end of our lives?</p> <p>Over a year ago, I found a pretty <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgoharbor%2Fharbor%2Fpull%2F8917%2Ffiles" target="_blank">interesting PR</a> in the <code>harbor</code> project. It was a fix for an issue that <strong>allowed a regular user to create an admin user. This was obviously a severe security issue.</strong> Of course, automated tests didn&rsquo;t find this bug earlier.</p> <p>This is what the bugfix looks like:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> ua.RenderError(http.StatusBadRequest, &#34;register error:&#34;+err.Error()) </span></span><span class="line"><span class="cl"> return </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"><span class="gi">+ </span></span></span><span class="line"><span class="cl"><span class="gi">+ if !ua.IsAdmin &amp;&amp; user.HasAdminRole { </span></span></span><span class="line"><span class="cl"><span class="gi">+ msg := &#34;Non-admin cannot create an admin user.&#34; </span></span></span><span class="line"><span class="cl"><span class="gi">+ log.Errorf(msg) </span></span></span><span class="line"><span class="cl"><span class="gi">+ ua.SendForbiddenError(errors.New(msg)) </span></span></span><span class="line"><span class="cl"><span class="gi">+ return </span></span></span><span class="line"><span class="cl"><span class="gi">+ } </span></span></span><span class="line"><span class="cl"><span class="gi">+ </span></span></span><span class="line"><span class="cl"><span class="gi"></span> userExist, err := dao.UserExists(user, &#34;username&#34;) </span></span><span class="line"><span class="cl"> if err != nil { </span></span></code></pre></div><p>One <code>if</code> statement fixed the bug. Adding new tests also should ensure that there will be no regression in the future. Is it enough? <strong>Did it secure the application from a similar bug in the future? I&rsquo;m pretty sure it didn&rsquo;t.</strong></p> <p>The problem becomes bigger in complex systems with a large team working on them. What if someone is new to the project and forgets to add this <code>if</code> statement? Even if you aren&rsquo;t hiring new people now, you may hire them in the future. <strong>You&rsquo;ll probably be surprised how long the code you&rsquo;ve written will live.</strong> We should not trust people to use our code the way we intended: they won&rsquo;t.</p> <p><strong>In some cases, the solution that protects us from issues like this is good design. Good design should not allow our code to be used in an invalid way.</strong> Good design should guarantee that you can modify existing code without fear. People new to the project will feel safer introducing changes.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="this-is-not-just-another-article-with-random-code-snippets">This is not just another article with random code snippets.</h4> <p>This post is part of a bigger series where we show how to build <strong>Go applications that are easy to develop, maintain, and fun to work with in the long term.</strong> We are doing it by sharing proven techniques based on many experiments we did with teams we lead and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%3Futm_source%3Dabout-wild-workouts%23thats-great-but-do-you-have-any-evidence-it-works">scientific research</a>.</p> <p>You can learn these patterns by building with us a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%3Futm_source%3Dabout-wild-workouts%23what-wild-workouts-can-do">fully functional</a> example Go web application &ndash; <strong>Wild Workouts</strong>.</p> <p>We did one thing differently &ndash; <strong>we included some subtle issues to the initial Wild Workouts implementation</strong>. Have we lost our minds to do that? Not yet. 😉 These issues are common for many Go projects. <strong>In the long term, these small issues become critical and stop adding new features.</strong></p> <p><strong>It&rsquo;s one of the essential skills of a senior or lead developer; you always need to keep long-term implications in mind.</strong></p> <p>We will fix them by <strong>refactoring</strong> Wild Workouts. In that way, you will quickly understand the techniques we share.</p> <p>Do you know that feeling after reading an article about some technique and trying implement it only to be blocked by some issues skipped in the guide? Cutting these details makes articles shorter and increases page views, but this is not our goal. Our goal is to create content that provides enough know-how to apply presented techniques. If you did not read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">previous articles from the series</a> yet, we highly recommend doing that.</p> <p>We believe that in some areas, there are no shortcuts. If you want to build complex applications in a fast and efficient way, you need to spend some time learning that. If it was simple, we wouldn&rsquo;t have large amounts of scary legacy code.</p> <p>Here&rsquo;s <strong><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">the full list of 14 articles</a></strong> released so far.</p> <p><strong>The full source code</strong> of Wild Workouts is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%3Futm_source%3Dabout-wild-workouts" target="_blank">GitHub</a>. Don&rsquo;t forget to leave a star for our project! ⭐</p> </p></div> </div> <p>In this article, I&rsquo;ll show how I ensured that only authorized people can see and edit a training. In our case, a training can only be seen by the training owner (the attendee) and the trainer. I will implement it in a way that prevents our code from being misused. By design.</p> <p>Our current application assumes that a repository is the only way to access data. Because of that, I will add authorization at the repository level. <strong>This ensures that unauthorized users cannot access this data.</strong></p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> #### What is Repository (tl;dr) If you haven't had a chance to read our previous articles, a repository is a pattern that helps us abstract database implementation from our application logic. If you want to know more about its advantages and learn how to apply it in your project, read my previous article: <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F">The Repository pattern: a painless way to simplify your Go service logic</a>. </p></div> </div> <p>But wait, is the repository the right place to manage authorization? Well, I can imagine some people may be skeptical about this approach. Of course, we could start a philosophical discussion on what belongs in the repository and what doesn&rsquo;t. Also, the actual logic of who can see the training will be placed in the <em>domain</em> layer. I don&rsquo;t see any significant downsides, and the advantages are clear. In my opinion, pragmatism should win here.</p> <div class="notice tip"> <div class="notice-head"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillrule="evenodd" cliprule="evenodd" d="M12 0c6.6274.0 12 5.37258 12 12 0 6.6274-5.3726 12-12 12C5.37258 24 0 18.6274.0 12 0 5.37258 5.37258.0 12 0zm0 2.4C6.69807 2.4 2.4 6.69807 2.4 12c0 5.3019 4.29807 9.6 9.6 9.6 5.3019.0 9.6-4.2981 9.6-9.6.0-5.30193-4.2981-9.6-9.6-9.6zm3.9515 5.15147L9.6 13.9029 8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515c-.46863.4686-.46863 1.2284.0 1.697l2.4 2.4C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485l7.2-7.19997C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147c-.468599999999999-.46863-1.2284-.46863-1.697.0z" fill="currentcolor"></path> </svg> <p>Tip</p></div> <div class="notice-body"> <p> <p>What&rsquo;s also interesting in this series is that we focus on <em>business-oriented applications</em>. But even though the <em>Harbor</em> project is a pure system application, most of the presented patterns can be applied as well.</p> <p>After introducing <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F">Clean Architecture</a> to our team, our teammate used this approach in his game to abstract the rendering engine. &#x1f609;</p> <p><em>(Cheers, Mariusz, if you are reading that!)</em></p> </p> </div> </div> <h3 id="show-me-the-code-please">Show me the code, please!</h3> <p>To achieve our robust design, we need to implement three things:</p> <ol> <li>Logic for who can see the training (<a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Ftree%2Fv2.6%2Finternal%2Ftrainer%2Fdomain%2Fhour" target="_blank">domain layer</a>),</li> <li>Functions to get the training (<code>GetTraining</code> in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F186a2c4a912e485ac7bb4d18c2892df7617e9ec9%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L57" target="_blank">repository</a>),</li> <li>Functions to update the training (<code>UpdateTraining</code> in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F186a2c4a912e485ac7bb4d18c2892df7617e9ec9%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L83" target="_blank">repository</a>).</li> </ol> <h4 id="domain-layer">Domain layer</h4> <p>The first part is the logic responsible for deciding if someone can see the training. Because it is part of the domain logic (you can discuss who can see the training with your business or product team), it should go in the <code>domain</code> layer. It&rsquo;s implemented with the <code>CanUserSeeTraining</code> function.</p> <p>It is also acceptable to keep it at the repository level, but it&rsquo;s harder to reuse. I don&rsquo;t see any advantage to that approach: especially since putting it in the <code>domain</code> doesn&rsquo;t cost anything. &#x1f609;</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">training</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">User</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">userUUID</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">userType</span> <span class="nx">UserType</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">ForbiddenToSeeTrainingError</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">RequestingUserUUID</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">TrainingOwnerUUID</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">f</span> <span class="nx">ForbiddenToSeeTrainingError</span><span class="p">)</span> <span class="nf">Error</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;user &#39;%s&#39; can&#39;t see user &#39;%s&#39; training&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">f</span><span class="p">.</span><span class="nx">RequestingUserUUID</span><span class="p">,</span> <span class="nx">f</span><span class="p">.</span><span class="nx">TrainingOwnerUUID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">CanUserSeeTraining</span><span class="p">(</span><span class="nx">user</span> <span class="nx">User</span><span class="p">,</span> <span class="nx">training</span> <span class="nx">Training</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">user</span><span class="p">.</span><span class="nf">Type</span><span class="p">()</span> <span class="o">==</span> <span class="nx">Trainer</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">user</span><span class="p">.</span><span class="nf">UUID</span><span class="p">()</span> <span class="o">==</span> <span class="nx">training</span><span class="p">.</span><span class="nf">UserUUID</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ForbiddenToSeeTrainingError</span><span class="p">{</span><span class="nx">user</span><span class="p">.</span><span class="nf">UUID</span><span class="p">(),</span> <span class="nx">training</span><span class="p">.</span><span class="nf">UserUUID</span><span class="p">()}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Fuser.go%23L92" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/domain/training/user.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Fuser.go%23L92" target="_blank">Full source</a> </div> <h4 id="repository">Repository</h4> <p>Now that we have the <code>CanUserSeeTraining</code> function, we need to use it. Simple as that.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">func (r TrainingsFirestoreRepository) GetTraining( </span></span><span class="line"><span class="cl"> ctx context.Context, </span></span><span class="line"><span class="cl"> trainingUUID string, </span></span><span class="line"><span class="cl"><span class="gi">+ user training.User, </span></span></span><span class="line"><span class="cl"><span class="gi"></span>) (*training.Training, error) { </span></span><span class="line"><span class="cl"> firestoreTraining, err := r.trainingsCollection().Doc(trainingUUID).Get(ctx) </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> if status.Code(err) == codes.NotFound { </span></span><span class="line"><span class="cl"> return nil, training.NotFoundError{trainingUUID} </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> if err != nil { </span></span><span class="line"><span class="cl"> return nil, errors.Wrap(err, &#34;unable to get actual docs&#34;) </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> tr, err := r.unmarshalTraining(firestoreTraining) </span></span><span class="line"><span class="cl"> if err != nil { </span></span><span class="line"><span class="cl"> return nil, err </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"><span class="gi">+ </span></span></span><span class="line"><span class="cl"><span class="gi">+ if err := training.CanUserSeeTraining(user, *tr); err != nil { </span></span></span><span class="line"><span class="cl"><span class="gi">+ return nil, err </span></span></span><span class="line"><span class="cl"><span class="gi">+ } </span></span></span><span class="line"><span class="cl"><span class="gi">+ </span></span></span><span class="line"><span class="cl"><span class="gi"></span> return tr, nil </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F186a2c4a912e485ac7bb4d18c2892df7617e9ec9%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L57" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/adapters/trainings_firestore_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F186a2c4a912e485ac7bb4d18c2892df7617e9ec9%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L57" target="_blank">Full source</a> </div> <p>Isn&rsquo;t this too simple? Our goal is to create simple, not complex, design and code. <strong>This is an excellent sign that it&rsquo;s dead simple.</strong></p> <p>We are changing <code>UpdateTraining</code> in the same way.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">func (r TrainingsFirestoreRepository) UpdateTraining( </span></span><span class="line"><span class="cl"> ctx context.Context, </span></span><span class="line"><span class="cl"> trainingUUID string, </span></span><span class="line"><span class="cl"><span class="gi">+ user training.User, </span></span></span><span class="line"><span class="cl"><span class="gi"></span> updateFn func(ctx context.Context, tr *training.Training) (*training.Training, error), </span></span><span class="line"><span class="cl">) error { </span></span><span class="line"><span class="cl"> trainingsCollection := r.trainingsCollection() </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> return r.firestoreClient.RunTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction) error { </span></span><span class="line"><span class="cl"> documentRef := trainingsCollection.Doc(trainingUUID) </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> firestoreTraining, err := tx.Get(documentRef) </span></span><span class="line"><span class="cl"> if err != nil { </span></span><span class="line"><span class="cl"> return errors.Wrap(err, &#34;unable to get actual docs&#34;) </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> tr, err := r.unmarshalTraining(firestoreTraining) </span></span><span class="line"><span class="cl"> if err != nil { </span></span><span class="line"><span class="cl"> return err </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"><span class="gi">+ </span></span></span><span class="line"><span class="cl"><span class="gi">+ if err := training.CanUserSeeTraining(user, *tr); err != nil { </span></span></span><span class="line"><span class="cl"><span class="gi">+ return err </span></span></span><span class="line"><span class="cl"><span class="gi">+ } </span></span></span><span class="line"><span class="cl"><span class="gi">+ </span></span></span><span class="line"><span class="cl"><span class="gi"></span> updatedTraining, err := updateFn(ctx, tr) </span></span><span class="line"><span class="cl"> if err != nil { </span></span><span class="line"><span class="cl"> return err </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> return tx.Set(documentRef, r.marshalTraining(updatedTraining)) </span></span><span class="line"><span class="cl"> }) </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F186a2c4a912e485ac7bb4d18c2892df7617e9ec9%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L83" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/adapters/trainings_firestore_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F186a2c4a912e485ac7bb4d18c2892df7617e9ec9%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L83" target="_blank">Full source</a> </div> <p>And&hellip; that&rsquo;s all! Is there any way someone can misuse this? As long as the <code>User</code> is valid: no.</p> <p>This approach is similar to the method presented in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F">the DDD Lite introduction article</a>. It&rsquo;s all about creating code that can&rsquo;t be misused.</p> <p>This is what using <code>UpdateTraining</code> now looks like:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">ApproveTrainingRescheduleHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">ApproveTrainingReschedule</span><span class="p">)</span> <span class="p">(</span><span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">logs</span><span class="p">.</span><span class="nf">LogCommandExecution</span><span class="p">(</span><span class="s">&#34;ApproveTrainingReschedule&#34;</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">.</span><span class="nx">repo</span><span class="p">.</span><span class="nf">UpdateTraining</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">TrainingUUID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">tr</span> <span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">originalTrainingTime</span> <span class="o">:=</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">Time</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">ApproveReschedule</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nf">Type</span><span class="p">());</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">trainerService</span><span class="p">.</span><span class="nf">MoveTraining</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">Time</span><span class="p">(),</span> <span class="nx">originalTrainingTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">tr</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Fapprove_training_reschedule.go%23L39" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/command/approve_training_reschedule.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Fapprove_training_reschedule.go%23L39" target="_blank">Full source</a> </div> <p>Of course, there are still some rules if <code>Training</code> can be rescheduled, but this is handled by the <code>Training</code> domain type. It&rsquo;s covered in details in the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F">DDD Lite introduction article</a>. &#x1f609;</p> <h3 id="handling-collections">Handling collections</h3> <p>Even though this approach works perfectly for operating on a single training, you need to ensure that access to a collection of trainings is properly secured. There&rsquo;s no magic here:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="nx">TrainingsFirestoreRepository</span><span class="p">)</span> <span class="nf">FindTrainingsForUser</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userUUID</span> <span class="kt">string</span><span class="p">)</span> <span class="p">([]</span><span class="nx">query</span><span class="p">.</span><span class="nx">Training</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">query</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">trainingsCollection</span><span class="p">().</span><span class="nx">Query</span><span class="p">.</span> </span></span><span class="line"><span class="cl"> <span class="nf">Where</span><span class="p">(</span><span class="s">&#34;Time&#34;</span><span class="p">,</span> <span class="s">&#34;&gt;=&#34;</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">().</span><span class="nf">Add</span><span class="p">(</span><span class="o">-</span><span class="nx">time</span><span class="p">.</span><span class="nx">Hour</span><span class="o">*</span><span class="mi">24</span><span class="p">)).</span> </span></span><span class="line"><span class="cl"> <span class="nf">Where</span><span class="p">(</span><span class="s">&#34;UserUuid&#34;</span><span class="p">,</span> <span class="s">&#34;==&#34;</span><span class="p">,</span> <span class="nx">userUUID</span><span class="p">).</span> </span></span><span class="line"><span class="cl"> <span class="nf">Where</span><span class="p">(</span><span class="s">&#34;Canceled&#34;</span><span class="p">,</span> <span class="s">&#34;==&#34;</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">iter</span> <span class="o">:=</span> <span class="nx">query</span><span class="p">.</span><span class="nf">Documents</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">r</span><span class="p">.</span><span class="nf">trainingModelsToQuery</span><span class="p">(</span><span class="nx">iter</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F186a2c4a912e485ac7bb4d18c2892df7617e9ec9%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L182" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/adapters/trainings_firestore_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F186a2c4a912e485ac7bb4d18c2892df7617e9ec9%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L182" target="_blank">Full source</a> </div> <p>Doing this at the application layer with the <code>CanUserSeeTraining</code> function would be very expensive and slow. It&rsquo;s better to create a bit of logic duplication.</p> <p>If this logic is more complex in your application, you can try abstracting it in the domain layer to a format that you can convert to query parameters in your database driver. I did this once, and it worked pretty nicely.</p> <p>But in Wild Workouts, that would add unnecessary complexity. Let&rsquo;s <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FKISS_principle" target="_blank">Keep It Simple, Stupid</a>.</p> <h3 id="handling-internal-updates">Handling internal updates</h3> <p>We often want to have endpoints that allow a developer or your company&rsquo;s operations department to make some &ldquo;backdoor&rdquo; changes. The worst thing you can do in this case is create any kind of &ldquo;fake user&rdquo; and hacks.</p> <p>From my experience, this ends with a lot of <code>if</code> statements added to the code. It also obfuscates the audit log (if you have one). Instead of a &ldquo;fake user&rdquo;, it&rsquo;s better to create a special role and explicitly define that role&rsquo;s permissions.</p> <p>If you need repository methods that don&rsquo;t require any user (for Pub/Sub message handlers or migrations), it&rsquo;s better to create separate repository methods. In that case, naming is essential: we need to be sure that the person who uses that method understands the security implications.</p> <p>From my experience, if updates are becoming much different for different actors, it&rsquo;s worth introducing separate <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbasic-cqrs-in-go%2F">CQRS Commands</a> per actor. In our case, it might be <code>UpdateTrainingByOperations</code>.</p> <h3 id="passing-authentication-via-contextcontext">Passing authentication via <code>context.Context</code></h3> <p>As far as I know, some people are passing authentication details via <code>context.Context</code>.</p> <p>I highly recommend not passing anything required by your application to work correctly via <code>context.Context</code>. The reason is simple: when passing values via <code>context.Context</code>, we lose one of Go&rsquo;s most significant advantages: static typing. It also hides what exactly the input for your functions is.</p> <p>If you need to pass values via context for some reason, it may be a symptom of bad design somewhere in your service. Maybe the function is doing too much, and it&rsquo;s hard to pass all arguments there? Perhaps it&rsquo;s time to decompose it?</p> <h3 id="and-thats-all-for-today">And that&rsquo;s all for today!</h3> <p>As you can see, the presented approach is straightforward to implement quickly.</p> <p>I hope that it will help you with your project and give you more confidence in future development.</p> <p>Do you see that it can help in your project? Do you think that it may help your colleagues? Don&rsquo;t forget to share it with them!</p>Microservices test architecture. Can you sleep well without end-to-end tests?https://threedots.tech/post/microservices-test-architecture/Thu, 28 Jan 2021 00:00:00 +0100https://threedots.tech/post/microservices-test-architecture/<p>Do you know the rare feeling when you develop a new application from scratch and can cover all lines with proper tests?</p> <p>I said &ldquo;rare&rdquo; because most of the time, you work with software that has a long history, multiple contributors, and a less-than-obvious testing approach. Even if the code uses good patterns, the test suite doesn&rsquo;t always follow.</p> <p>Some projects have no modern development environment set up, so there are only unit tests for things that are easy to test. For example, they test single functions separately because testing the public API is hard. The team needs to manually verify all changes, probably on some kind of staging environment. You know what happens when someone introduces changes and doesn&rsquo;t know they need to test them manually.</p> <p>Other projects have no tests from the beginning. This allows quicker development by taking shortcuts, for example, keeping dependencies in the global state. When the team realizes the lack of tests causes bugs and slows them down, they decide to add them. But now, it&rsquo;s impossible to do so reasonably. So the team writes an end-to-end test suite with proper infrastructure in place.</p> <p>End-to-end tests might give you some confidence, but <strong>you don&rsquo;t want to maintain such a test suite</strong>. It&rsquo;s hard to debug, takes a long time to test even the simplest change, and releasing the application takes hours. Introducing new tests is also not trivial in this scenario, so developers avoid it if they can.</p> <p>I want to introduce some ideas that have worked for us so far and should help you avoid the scenarios above.</p> <p>This post is not about which testing library is best or what tricks you can use (although I will show a few tips). It&rsquo;s closer to something I would call &ldquo;test architecture&rdquo;. It&rsquo;s not only about &ldquo;how&rdquo;, but also &ldquo;where&rdquo;, &ldquo;what&rdquo;, and &ldquo;why&rdquo;.</p> <p>There&rsquo;s been a lot of discussion on different types of tests, for example, the <a href="proxy.php?url=https%3A%2F%2Fmartinfowler.com%2Fbliki%2FTestPyramid.html" target="_blank">&ldquo;test pyramid&rdquo;</a> (Robert mentioned it in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-integration-testing%2F%232-testing-enough-scenarios-on-all-levels">this section about testing scenarios</a>). It&rsquo;s a helpful model to keep in mind. However, it&rsquo;s also abstract, and you can&rsquo;t easily measure it. I want to take a more practical approach and show how to introduce a few kinds of tests in a Go project.</p> <h2 id="why-bother-about-tests">Why bother about tests?</h2> <p>But isn&rsquo;t test code less important than the rest of the application? Can&rsquo;t we just accept that keeping tests in good shape is hard and move on? Wouldn&rsquo;t it speed up development?</p> <p>If you&rsquo;ve been following this series, you know we base all posts on the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">Wild Workouts</a> application.</p> <p>When I started writing this article, running tests locally didn&rsquo;t even work correctly for me, and this is a relatively new project. There&rsquo;s one reason this happened: we weren&rsquo;t running tests in the CI pipeline.</p> <p>It&rsquo;s a shock, but it seems <strong>even a serverless, cloud-native application using the most popular, cutting-edge technologies can be a mess in disguise.</strong></p> <p>We know we should now add tests to the pipeline. It&rsquo;s common knowledge that this gives you the confidence to deploy changes to production safely. However, there&rsquo;s also a cost.</p> <p>Running tests will likely take a significant part of your pipeline&rsquo;s duration. If you don&rsquo;t approach their design and implementation with the same quality as the application code, you can realize it too late, with the pipeline taking one hour to pass and randomly failing on you. <strong>Even if your application code is well designed, the tests can become a bottleneck of delivering the changes</strong>.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="this-is-not-just-another-article-with-random-code-snippets">This is not just another article with random code snippets.</h4> <p>This post is part of a bigger series where we show how to build <strong>Go applications that are easy to develop, maintain, and fun to work with in the long term.</strong> We are doing it by sharing proven techniques based on many experiments we did with teams we lead and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%3Futm_source%3Dabout-wild-workouts%23thats-great-but-do-you-have-any-evidence-it-works">scientific research</a>.</p> <p>You can learn these patterns by building with us a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%3Futm_source%3Dabout-wild-workouts%23what-wild-workouts-can-do">fully functional</a> example Go web application &ndash; <strong>Wild Workouts</strong>.</p> <p>We did one thing differently &ndash; <strong>we included some subtle issues to the initial Wild Workouts implementation</strong>. Have we lost our minds to do that? Not yet. 😉 These issues are common for many Go projects. <strong>In the long term, these small issues become critical and stop adding new features.</strong></p> <p><strong>It&rsquo;s one of the essential skills of a senior or lead developer; you always need to keep long-term implications in mind.</strong></p> <p>We will fix them by <strong>refactoring</strong> Wild Workouts. In that way, you will quickly understand the techniques we share.</p> <p>Do you know that feeling after reading an article about some technique and trying implement it only to be blocked by some issues skipped in the guide? Cutting these details makes articles shorter and increases page views, but this is not our goal. Our goal is to create content that provides enough know-how to apply presented techniques. If you did not read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">previous articles from the series</a> yet, we highly recommend doing that.</p> <p>We believe that in some areas, there are no shortcuts. If you want to build complex applications in a fast and efficient way, you need to spend some time learning that. If it was simple, we wouldn&rsquo;t have large amounts of scary legacy code.</p> <p>Here&rsquo;s <strong><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">the full list of 14 articles</a></strong> released so far.</p> <p><strong>The full source code</strong> of Wild Workouts is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%3Futm_source%3Dabout-wild-workouts" target="_blank">GitHub</a>. Don&rsquo;t forget to leave a star for our project! ⭐</p> </p></div> </div> <h2 id="the-layers">The Layers</h2> <p>We&rsquo;re now a few refactoring sessions into the project. We introduced patterns like <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F">Repository</a>, <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F">Clean Architecture</a>, and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbasic-cqrs-in-go%2F">CQRS</a>. With solid separation of concerns, we can reason about particular parts of the project much more easily.</p> <p>Let&rsquo;s revisit the concept of layers we introduced in previous posts. If you didn&rsquo;t have a chance to read these earlier, I recommend doing so before you continue: it will help you better understand this article.</p> <p>Take a look at a diagram that will help us understand the project&rsquo;s structure. Below is a generic service built with the approach used in Wild Workouts.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="6724" height="2379" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-test-architecture%2Fbase_hu7a5b5d5cc96d9ada230aeb87ed5b9159_868251_6724x2379_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-test-architecture%5C%2Fbase_hu7a5b5d5cc96d9ada230aeb87ed5b9159_868251_6724x2379_resize_q80_lanczos.jpg"" /> <p>All external inputs start on the left. The only entry point to the application is through the <strong>Ports</strong> layer (HTTP handlers, Pub/Sub message handlers). Ports execute relevant handlers in the <strong>App</strong> layer. Some of these will call the <strong>Domain</strong> code, and some will use <strong>Adapters</strong>, which are the only way out of the service. The adapters layer is where your database queries and HTTP clients live.</p> <p>The diagram below shows the layers and flow of a part of the trainer service in Wild Workouts.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="6329" height="2527" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-test-architecture%2Fexample_hu7a5b5d5cc96d9ada230aeb87ed5b9159_1059217_6329x2527_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-test-architecture%5C%2Fexample_hu7a5b5d5cc96d9ada230aeb87ed5b9159_1059217_6329x2527_resize_q80_lanczos.jpg"" /> <p>Let&rsquo;s now see what types of tests we would need to cover all of it.</p> <h2 id="unit-tests">Unit tests</h2> <p>We kick off with the inner layers and something everyone is familiar with: unit tests.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="5658" height="2827" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-test-architecture%2Funit-tests_hu7a5b5d5cc96d9ada230aeb87ed5b9159_810131_5658x2827_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-test-architecture%5C%2Funit-tests_hu7a5b5d5cc96d9ada230aeb87ed5b9159_810131_5658x2827_resize_q80_lanczos.jpg"" /> <p>The domain layer is where the most complex logic of your service lives. However, <strong>the tests here should be some of the simplest to write and run super fast.</strong> There are no external dependencies in the domain, so you don&rsquo;t need any special infrastructure or mocks (except for really complex scenarios, but let&rsquo;s leave that for now).</p> <p>As a rule of thumb, you should aim for high test coverage in the domain layer. Make sure you test only the exported code <em>(black-box testing)</em>. Adding the <code>_test</code> suffix to the package name is a great practice to enforce this.</p> <p>The domain code is pure logic and straightforward to test, so it&rsquo;s the best place to check all corner cases. Table-driven tests are especially great for this.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestFactoryConfig_Validate</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">testCases</span> <span class="o">:=</span> <span class="p">[]</span><span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">Config</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">FactoryConfig</span> </span></span><span class="line"><span class="cl"> <span class="nx">ExpectedErr</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="p">}{</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;valid&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Config</span><span class="p">:</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">FactoryConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">MaxWeeksInTheFutureToSet</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">MinUtcHour</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">MaxUtcHour</span><span class="p">:</span> <span class="mi">12</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">ExpectedErr</span><span class="p">:</span> <span class="s">&#34;&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;equal_min_and_max_hour&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Config</span><span class="p">:</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">FactoryConfig</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">MaxWeeksInTheFutureToSet</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">MinUtcHour</span><span class="p">:</span> <span class="mi">12</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">MaxUtcHour</span><span class="p">:</span> <span class="mi">12</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">ExpectedErr</span><span class="p">:</span> <span class="s">&#34;&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">c</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">testCases</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nx">Config</span><span class="p">.</span><span class="nf">Validate</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">c</span><span class="p">.</span><span class="nx">ExpectedErr</span> <span class="o">!=</span> <span class="s">&#34;&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">assert</span><span class="p">.</span><span class="nf">EqualError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">,</span> <span class="nx">c</span><span class="p">.</span><span class="nx">ExpectedErr</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">assert</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F6954ccbd8099648fa12120632102e792ff2377ad%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Fhour_test.go%23L137" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour/hour_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F6954ccbd8099648fa12120632102e792ff2377ad%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Fhour_test.go%23L137" target="_blank">Full source</a> </div> <p>We leave the domain and enter the application layer. After <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbasic-cqrs-in-go%2F">introducing CQRS</a>, we split it further into Commands and Queries.</p> <p>Depending on your project, there could be nothing to test or some complex scenarios to cover. Most of the time, especially in queries, this code just glues together other layers. Testing it doesn&rsquo;t add any value. But if there&rsquo;s any complex orchestration in commands, it&rsquo;s another good case for unit tests.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>Watch out for complex logic living in the application layer. If you start testing business scenarios here, it&rsquo;s worth considering introducing the domain layer.</p> <p>On the other hand, it&rsquo;s the perfect place for orchestration: calling adapters and services in a particular order and passing return values around. If you separate it like that, application tests should not break every time you change the domain code.</p> </p></div> </div> <p><strong>The application&rsquo;s commands and queries have many external dependencies</strong>, as opposed to the domain code. They will be trivial to mock if you follow <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F">Clean Architecture</a> and use the Dependency Inversion Principle (your code depends on interfaces, not on structs). In most cases, a struct with a single method will make a perfect mock here.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If you prefer to use mocking libraries or code-generated mocks, you can use them as well. Go lets you define and implement small interfaces, so we choose to define the mocks ourselves, as it&rsquo;s the simplest way. </p></div> </div> <p>The snippet below shows how an application command is created with injected mocks.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">newDependencies</span><span class="p">()</span> <span class="nx">dependencies</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">repository</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">repositoryMock</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainerService</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">trainerServiceMock</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="nx">userService</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">userServiceMock</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">dependencies</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">repository</span><span class="p">:</span> <span class="nx">repository</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainerService</span><span class="p">:</span> <span class="nx">trainerService</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">userService</span><span class="p">:</span> <span class="nx">userService</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">handler</span><span class="p">:</span> <span class="nx">command</span><span class="p">.</span><span class="nf">NewCancelTrainingHandler</span><span class="p">(</span><span class="nx">repository</span><span class="p">,</span> <span class="nx">userService</span><span class="p">,</span> <span class="nx">trainerService</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="nx">deps</span> <span class="o">:=</span> <span class="nf">newDependencies</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">tr</span> <span class="o">:=</span> <span class="nx">tc</span><span class="p">.</span><span class="nf">TrainingConstructor</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nx">deps</span><span class="p">.</span><span class="nx">repository</span><span class="p">.</span><span class="nx">Trainings</span> <span class="p">=</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingUUID</span><span class="p">:</span> <span class="o">*</span><span class="nx">tr</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">deps</span><span class="p">.</span><span class="nx">handler</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="nx">command</span><span class="p">.</span><span class="nx">CancelTraining</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">TrainingUUID</span><span class="p">:</span> <span class="nx">trainingUUID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">User</span><span class="p">:</span> <span class="nx">training</span><span class="p">.</span><span class="nf">MustNewUser</span><span class="p">(</span><span class="nx">requestingUserID</span><span class="p">,</span> <span class="nx">tc</span><span class="p">.</span><span class="nx">UserType</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Fcancel_training_test.go%23L73" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/command/cancel_training_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Fcancel_training_test.go%23L73" target="_blank">Full source</a> </div> <p>Watch out for adding tests that don&rsquo;t check anything relevant: you don&rsquo;t want to end up testing the mocks. Focus on the logic, and if there&rsquo;s none, skip the test altogether.</p> <p>We&rsquo;ve covered the two inner layers. I guess this didn&rsquo;t seem novel so far, as we&rsquo;re all familiar with unit tests. However, the <a href="proxy.php?url=https%3A%2F%2Fwww.infoq.com%2Farticles%2FContinuous-Delivery-Maturity-Model%2F" target="_blank">Continuous Delivery Maturity Model</a> lists them only on the &ldquo;base&rdquo; maturity level. Let&rsquo;s now look into integration testing.</p> <h2 id="integration-tests">Integration tests</h2> <p>After reading this header, did you just imagine a long-running test that you have to retry several times to pass? And it&rsquo;s because of that 30-second sleep someone added that turns out to be too short when Jenkins is running under load?</p> <p><strong>There&rsquo;s no reason for integration tests to be slow and flaky. And practices like automatic retries and increasing sleep times should be absolutely out of the question.</strong></p> <p>In our context, <strong>an integration test checks if an adapter works correctly with external infrastructure.</strong> Most of the time, this means testing database repositories.</p> <p>These tests don&rsquo;t check whether the database works correctly, but whether you use it correctly (the <strong>integration</strong> part). It&rsquo;s also an excellent way to verify that you know how to use the database internals, like handling transactions.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="5368" height="2980" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-test-architecture%2Fintegration-tests_hu7a5b5d5cc96d9ada230aeb87ed5b9159_799040_5368x2980_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-test-architecture%5C%2Fintegration-tests_hu7a5b5d5cc96d9ada230aeb87ed5b9159_799040_5368x2980_resize_q80_lanczos.jpg"" /> <p>Because we need real infrastructure, <strong>integration tests are more challenging than unit tests to write and maintain.</strong> Usually, we can use docker-compose to spin up all dependencies.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>Should we test our application with the Docker version of a database? The Docker image will almost always be slightly different than what we run on production. In some cases, like Firestore, there&rsquo;s only an emulator available, not the real database.</p> <p>Indeed, Docker doesn&rsquo;t reflect the exact infrastructure you run in production. However, you&rsquo;re much more likely to mess up an SQL query in the code than to stumble on issues because of a minor configuration difference.</p> <p>A good practice is to pin the image version to the same one running in production. Using Docker won&rsquo;t give you 100% production parity, but it eliminates the &ldquo;works on my machine&rdquo; issues and tests your code with proper infrastructure.</p> </p></div> </div> <p>Robert covered integration tests for databases in depth in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-integration-testing%2F%232-testing-enough-scenarios-on-all-levels">4 practical principles of high-quality database integration tests in Go</a>.</p> <h3 id="keeping-integration-tests-stable-and-fast">Keeping integration tests stable and fast</h3> <p>When dealing with network calls and databases, test speed becomes super important. It&rsquo;s crucial to run tests in parallel, which you can enable in Go by calling <code>t.Parallel()</code>. <strong>It seems simple to do, but we have to make sure our tests support this behavior.</strong></p> <p>For example, consider this trivial test scenario:</p> <ol> <li>Check if the <code>trainings</code> collection is empty.</li> <li>Call repository method that adds a training.</li> <li>Check if there&rsquo;s one training in the collection.</li> </ol> <p>If another test uses the same collection, you will get random fails because of the race condition. Sometimes, the collection will contain more than one training we&rsquo;ve just added.</p> <p>The simplest way out of this is never to assert things like list length, but to check for the exact thing we&rsquo;re testing. For example, we could get all trainings, then iterate over the list to check if the expected ID is present.</p> <p>Another approach is to isolate the tests somehow, so they can&rsquo;t interfere with each other. For example, each test case can work within a unique user&rsquo;s context (see component tests below).</p> <p>Of course, both patterns are more complex than a simple length assertion. <strong>When you stumble upon this issue for the first time, it may be tempting to give up and decide that &ldquo;our integration tests don&rsquo;t need to run in parallel&rdquo;. Don&rsquo;t do this.</strong> You will need to get creative sometimes, but it&rsquo;s not that much effort in the end. In return, your integration tests will be stable and run as fast as unit tests.</p> <p>If you find yourself creating a new database before each run, it&rsquo;s another sign that you could rework tests to not interfere with each other.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h3 id="a-common-gotcha-in-go-121-and-earlier">A common gotcha in Go 1.21 and earlier</h3> <p><em>(You can skip this section if running Go 1.22 or later.)</em></p> <p>When working with table-driven tests, you can see code like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">c</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">testCases</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This is an idiomatic way to run tests over a slice of test cases. Let&rsquo;s say you now want to run each test case in parallel. The solution seems trivial:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">c</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">testCases</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Sadly, this won&rsquo;t work as expected.</p> <p>The <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgolang%2Fgo%2Fwiki%2FCommonMistakes" target="_blank">Common Mistakes</a> page on Go&rsquo;s GitHub wiki lists just two items, and both are actually about the same thing. So it seems there&rsquo;s only one mistake you should worry about in Go. However, this one is really hard to spot sometimes.</p> <p>It&rsquo;s not obvious initially, but adding the parallel switch makes the parent test function not wait for the subtests spawned by <code>t.Run</code>. Because of this, you can&rsquo;t safely use the <code>c</code> loop variable inside the <code>func</code> closure.</p> <p>Running tests like this will usually cause all subtests to work with the last test case, ignoring all others. <strong>The worst part is the tests will pass, and you will see correct subtest names when running <code>go test</code> with the <code>-v</code> flag.</strong> The only way to notice this issue is to change the code expecting tests to fail and see them pass instead.</p> <p>As mentioned in the wiki, one way to fix this is to introduce a new scoped variable:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">c</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">testCases</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">c</span> <span class="o">:=</span> <span class="nx">c</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>It&rsquo;s just a matter of taste, but we don&rsquo;t like this approach, as it looks like a magic spell to anyone who doesn&rsquo;t know what it means. Instead, we choose the more verbose but obvious approach:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">testCases</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">c</span> <span class="o">:=</span> <span class="nx">testCases</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">c</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Even if you know about this behavior, it&rsquo;s dangerously easy to misuse it. We made this mistake in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill" target="_blank">Watermill</a> library, which caused some tests not to run at all. You can see the fix in <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fcommit%2Fc72e26a67cb763ab3dd93ecf57a2b298fc81dd19" target="_blank">this commit</a>.</p> </p></div> </div> <p>We covered the database repository with tests, but we also have a gRPC client adapter. How should we test this one?</p> <p>It&rsquo;s similar to the application layer in this regard. If your test would just duplicate the code it checks, it probably makes no sense to add it. It becomes additional work when changing the code.</p> <p>Let&rsquo;s consider the users service gRPC adapter:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="nx">UsersGrpc</span><span class="p">)</span> <span class="nf">UpdateTrainingBalance</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">amountChange</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">s</span><span class="p">.</span><span class="nx">client</span><span class="p">.</span><span class="nf">UpdateTrainingBalance</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">users</span><span class="p">.</span><span class="nx">UpdateTrainingBalanceRequest</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserId</span><span class="p">:</span> <span class="nx">userID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">AmountChange</span><span class="p">:</span> <span class="nb">int64</span><span class="p">(</span><span class="nx">amountChange</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fadapters%2Fusers_grpc.go%23L17" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/adapters/users_grpc.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fadapters%2Fusers_grpc.go%23L17" target="_blank">Full source</a> </div> <p>There&rsquo;s nothing interesting to test here. We could inject a mock <code>client</code> and check whether a proper method has been called. But this won&rsquo;t verify anything, and each change in the code will require a corresponding change in the test.</p> <h2 id="component-tests">Component tests</h2> <p>So far, we&rsquo;ve created mostly narrow, specialized tests for isolated parts of the application. Such tests work great for checking corner cases and specific scenarios, <strong>but that doesn&rsquo;t mean each service works correctly</strong>. It&rsquo;s easy enough to forget to call an application handler from a port. Also, unit tests alone won&rsquo;t help us make sure the application still works after a major refactoring.</p> <p>Is it now the time to run end-to-end tests across all our services? Not yet.</p> <p>Since there is no standard naming for test types, I encourage you to follow <a href="proxy.php?url=https%3A%2F%2Ftesting.googleblog.com%2F2010%2F12%2Ftest-sizes.html" target="_blank">Simon Stewart&rsquo;s advice from his Test Sizes post</a>. Create a table that makes it obvious to everyone on the team what to expect from a particular test. You can then cut all (unproductive) discussions on the topic.</p> <p>In our case, the table could look like this:</p> <table> <thead> <tr> <th>Feature</th> <th>Unit</th> <th>Integration</th> <th>Component</th> <th>End-to-End</th> </tr> </thead> <tbody> <tr> <td><strong>Docker database</strong></td> <td>No</td> <td>Yes</td> <td>Yes</td> <td>Yes</td> </tr> <tr> <td><strong>Use external systems</strong></td> <td>No</td> <td>No</td> <td>No</td> <td>Yes</td> </tr> <tr> <td><strong>Focused on business cases</strong></td> <td>Depends on the tested code</td> <td>No</td> <td>Yes</td> <td>Yes</td> </tr> <tr> <td><strong>Used mocks</strong></td> <td>Most dependencies</td> <td>Usually none</td> <td>External systems</td> <td>None</td> </tr> <tr> <td><strong>Tested API</strong></td> <td>Go package</td> <td>Go package</td> <td>HTTP and gRPC</td> <td>HTTP</td> </tr> </tbody> </table> <p>To ensure each service works correctly internally, <strong>we introduce component tests to check all layers working together.</strong> A component test covers a single service, isolated from other services in the application.</p> <p>We will call real port handlers and use the infrastructure provided by Docker. However, we will <strong>mock all adapters reaching external services</strong>.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="5543" height="2886" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-test-architecture%2Fcomponent-tests_hu7a5b5d5cc96d9ada230aeb87ed5b9159_836003_5543x2886_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-test-architecture%5C%2Fcomponent-tests_hu7a5b5d5cc96d9ada230aeb87ed5b9159_836003_5543x2886_resize_q80_lanczos.jpg"" /> <p>You might be wondering, why not test external services as well? After all, we could use Docker containers and test all of them together.</p> <p>The issue is the complexity of testing several connected services. If you have just a couple of them, that can work well enough. But remember, you need proper infrastructure in place for each service you spin up, including all databases it uses and all external services it calls. It can easily be tens of services in total, usually owned by multiple teams.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="5351" height="2989" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-test-architecture%2Fsetting-up-end-to-end_hu7a5b5d5cc96d9ada230aeb87ed5b9159_687335_5351x2989_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-test-architecture%5C%2Fsetting-up-end-to-end_hu7a5b5d5cc96d9ada230aeb87ed5b9159_687335_5351x2989_resize_q80_lanczos.jpg"" /> <div class="code-ref"> You don't want this as your primary testing approach. </div> <p>We&rsquo;ll cover this next in end-to-end tests. But for now, we add component tests because we need a fast way to know if a service works correctly.</p> <p>To better understand why it&rsquo;s essential to have component tests in place, I suggest looking at some quotes from <em><a href="proxy.php?url=https%3A%2F%2Fitrevolution.com%2Fbook%2Faccelerate%2F" target="_blank">Accelerate</a></em>.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If you haven&rsquo;t heard about <em>Accelerate</em> yet, it&rsquo;s a book describing research on high-performing software teams. I recommend reading it to learn what can help your team get up to speed. </p></div> </div> <p>According to the book, this is what the best software teams said about testability.</p> <blockquote> <p>We can do most of our testing without requiring an integrated environment. We can and do deploy or release our application independently of other applications/services it depends on.</p> <footer> <strong></strong> <cite> <a href="proxy.php?url=https%3A%2F%2Fitrevolution.com%2Fbook%2Faccelerate%2F" title="https://itrevolution.com/book/accelerate/" target="_blank">Accelerate</a> </cite> </footer> </blockquote> <p>Wait, weren&rsquo;t microservices supposed to fix teams being dependent on each other? If you think it&rsquo;s impossible to achieve this in the application you work on, it&rsquo;s likely because of poor architecture choices. You can fix it by applying strategic DDD patterns that we plan to introduce in future posts.</p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/microservices-test-architecture/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;ci;testing;microservices;clean-architecture;building-business-applications"> <input type="hidden" name="fields[blog_series]" value="Modern Business Software in Go"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <p><em>Accelerate</em> follows up with:</p> <blockquote> <p>Unfortunately, in real life, many so-called service-oriented architectures don&rsquo;t permit testing and deploying services independently of each other, and thus will not enable teams to achieve higher performance.</p> <footer> <strong></strong> <cite> <a href="proxy.php?url=https%3A%2F%2Fitrevolution.com%2Fbook%2Faccelerate%2F" title="https://itrevolution.com/book/accelerate/" target="_blank">Accelerate</a> </cite> </footer> </blockquote> <p>We raise this point throughout the series: <strong>using microservices doesn&rsquo;t make your application and teams less coupled by itself. Decoupling takes conscious design of both the application&rsquo;s architecture and the system level.</strong></p> <p><strong>In component tests, our goal is to check the completeness of a single service in isolation, with all infrastructure it needs.</strong> We make sure the service accepts the API we agreed on and responds with expected results.</p> <p>These tests are more challenging technically, but still relatively straightforward. We won&rsquo;t be running a real service binary because we need to mock some dependencies. We have to modify the way we start the service to make it possible.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Once again, if you follow the Dependency Inversion Principle (just a reminder, it&rsquo;s part of SOLID), injecting mocks at the service level should be trivial. </p></div> </div> <p>I introduced two constructors for our <code>app.Application</code> struct, which holds all commands and queries. The first one works just like before, setting up real gRPC clients and injecting them. The second replaces them with mocks.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewApplication</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">(</span><span class="nx">app</span><span class="p">.</span><span class="nx">Application</span><span class="p">,</span> <span class="kd">func</span><span class="p">())</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="nx">trainerGrpc</span> <span class="o">:=</span> <span class="nx">adapters</span><span class="p">.</span><span class="nf">NewTrainerGrpc</span><span class="p">(</span><span class="nx">trainerClient</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">usersGrpc</span> <span class="o">:=</span> <span class="nx">adapters</span><span class="p">.</span><span class="nf">NewUsersGrpc</span><span class="p">(</span><span class="nx">usersClient</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">newApplication</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">trainerGrpc</span><span class="p">,</span> <span class="nx">usersGrpc</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewComponentTestApplication</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="nx">app</span><span class="p">.</span><span class="nx">Application</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">newApplication</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">TrainerServiceMock</span><span class="p">{},</span> <span class="nx">UserServiceMock</span><span class="p">{})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0e3e9d80eb14639bc42935795f7ca3b73da36304%2Finternal%2Ftrainings%2Fservice%2Fservice.go%23L35" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/service/service.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0e3e9d80eb14639bc42935795f7ca3b73da36304%2Finternal%2Ftrainings%2Fservice%2Fservice.go%23L35" target="_blank">Full source</a> </div> <p>We can now simply run the service in a separate goroutine.</p> <p>We want to run just a single service instance for all tests, so we use the <code>TestMain</code> function. It&rsquo;s a simple way to insert a setup before running tests.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">startService</span><span class="p">()</span> <span class="kt">bool</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">app</span> <span class="o">:=</span> <span class="nf">NewComponentTestApplication</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">trainingsHTTPAddr</span> <span class="o">:=</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;TRAININGS_HTTP_ADDR&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">go</span> <span class="nx">server</span><span class="p">.</span><span class="nf">RunHTTPServerOnAddr</span><span class="p">(</span><span class="nx">trainingsHTTPAddr</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">router</span> <span class="nx">chi</span><span class="p">.</span><span class="nx">Router</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ports</span><span class="p">.</span><span class="nf">HandlerFromMux</span><span class="p">(</span><span class="nx">ports</span><span class="p">.</span><span class="nf">NewHttpServer</span><span class="p">(</span><span class="nx">app</span><span class="p">),</span> <span class="nx">router</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">tests</span><span class="p">.</span><span class="nf">WaitForPort</span><span class="p">(</span><span class="nx">trainingsHTTPAddr</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;Timed out waiting for trainings HTTP to come up&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ok</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestMain</span><span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">M</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nf">startService</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">os</span><span class="p">.</span><span class="nf">Exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">os</span><span class="p">.</span><span class="nf">Exit</span><span class="p">(</span><span class="nx">m</span><span class="p">.</span><span class="nf">Run</span><span class="p">())</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0e3e9d80eb14639bc42935795f7ca3b73da36304%2Finternal%2Ftrainings%2Fservice%2Fcomponent_test.go%23L58" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/service/component_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0e3e9d80eb14639bc42935795f7ca3b73da36304%2Finternal%2Ftrainings%2Fservice%2Fcomponent_test.go%23L58" target="_blank">Full source</a> </div> <p>I created the <code>WaitForPort</code> helper function that waits until the specified port is open or times out. It&rsquo;s crucial, as you need to ensure the service has properly started. <strong>Don&rsquo;t replace it with sleeps.</strong> You will either add too much delay and make the test slow, or make it too short and it will fail randomly.</p> <p>What to test in component tests? <strong>Usually, the happy path should be enough. Don&rsquo;t check for corner cases there. Unit and integration tests should already cover these.</strong> Make sure the correct payloads are processed, the storage works, and responses are correct.</p> <h3 id="calling-the-ports">Calling the ports</h3> <p>I use HTTP clients generated by <code>openapi-codegen</code>. Similar to the server part, this makes writing tests much easier. For example, you don&rsquo;t need to specify the whole REST path and worry about marshaling JSON each time.</p> <p>Even though the generated clients save us a lot of boilerplate, I still added the <code>tests/client.go</code> file with client wrappers for test purposes.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="nx">TrainingsHTTPClient</span><span class="p">)</span> <span class="nf">CreateTraining</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">,</span> <span class="nx">note</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">hour</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">response</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nx">client</span><span class="p">.</span><span class="nf">CreateTrainingWithResponse</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="nx">trainings</span><span class="p">.</span><span class="nx">CreateTrainingJSONRequestBody</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Notes</span><span class="p">:</span> <span class="nx">note</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Time</span><span class="p">:</span> <span class="nx">hour</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusNoContent</span><span class="p">,</span> <span class="nx">response</span><span class="p">.</span><span class="nf">StatusCode</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">contentLocation</span> <span class="o">:=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">HTTPResponse</span><span class="p">.</span><span class="nx">Header</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;content-location&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">lastPathElement</span><span class="p">(</span><span class="nx">contentLocation</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0e3e9d80eb14639bc42935795f7ca3b73da36304%2Finternal%2Fcommon%2Ftests%2Fclients.go%23L88" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/tests/clients.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0e3e9d80eb14639bc42935795f7ca3b73da36304%2Finternal%2Fcommon%2Ftests%2Fclients.go%23L88" target="_blank">Full source</a> </div> <p>The tests become even more readable, and it&rsquo;s easy to understand what&rsquo;s going on. <strong>Making tests pass is easy enough, but it&rsquo;s much harder to understand them in a code review, reading between all asserts and mocks.</strong> It&rsquo;s twice as important when modifying tests.</p> <p>Instead of the snippet above, we can now use a single line that clearly states what&rsquo;s happening.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">trainingUUID</span> <span class="o">:=</span> <span class="nx">client</span><span class="p">.</span><span class="nf">CreateTraining</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="s">&#34;some note&#34;</span><span class="p">,</span> <span class="nx">hour</span><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0e3e9d80eb14639bc42935795f7ca3b73da36304%2Finternal%2Ftrainings%2Fservice%2Fcomponent_test.go%23L25" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/service/component_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0e3e9d80eb14639bc42935795f7ca3b73da36304%2Finternal%2Ftrainings%2Fservice%2Fcomponent_test.go%23L25" target="_blank">Full source</a> </div> <p>Other helper methods are <code>FakeAttendeeJWT</code> and <code>FakeTrainerJWT</code>. They generate valid authorization tokens with the chosen role.</p> <p>As gRPC uses structs generated from protobuf, the clients are already straightforward to use.</p> <h2 id="end-to-end-tests">End-to-end tests</h2> <p>Finally, we come to the most dreaded part of our test suite. We will now leave all mocks behind.</p> <p><strong>End-to-end tests verify your whole system working together. They are slow, error-prone, and hard to maintain.</strong> You still need them, but make sure you build them well.</p> <p>In Wild Workouts, it will be similar to running component tests, except we&rsquo;re going to spin up all services inside the docker-compose. We&rsquo;ll then verify a few critical paths calling just the HTTP endpoints, as this is what we expose to the external world.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If you can&rsquo;t run the entire platform on docker-compose, you need to find a similar approach. It could be a separate Kubernetes cluster or namespace if you&rsquo;re already using it, or some kind of staging environment. </p></div> </div> <img title="" loading="lazy" decoding="async" class="img img-center" width="6556" height="2440" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-test-architecture%2Fend-to-end-tests_hu7a5b5d5cc96d9ada230aeb87ed5b9159_887660_6556x2440_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-test-architecture%5C%2Fend-to-end-tests_hu7a5b5d5cc96d9ada230aeb87ed5b9159_887660_6556x2440_resize_q80_lanczos.jpg"" /> <p>Now comes the part where you&rsquo;re going to have more questions than answers. Where should you keep end-to-end tests? Which team should own them? Where should you run them? How often? Should they be part of the CI/CD pipeline or a separate thing run from a cronjob?</p> <p>I can&rsquo;t give you clear answers, as this heavily depends on your team&rsquo;s structure, organizational culture, and CI/CD setup. As with most challenges, try an approach that seems best and iterate until you&rsquo;re happy.</p> <p>We&rsquo;re lucky to have just three services for the entire application, all using the same database. As the number of services and dependencies grows, your application will become harder to test this way.</p> <p>Try to keep end-to-end tests short. <strong>They should test whether services connect together correctly, not the logic inside them.</strong> These tests work as a double-check. They shouldn&rsquo;t fail most of the time, and if they do, it usually means someone broke the contract.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> I said we&rsquo;ll use just HTTP endpoints, as this is what is exposed publicly. There&rsquo;s one exception: we call the users service via gRPC to add training balance for our test attendee. As you would expect, this endpoint is not accessible to the public. </p></div> </div> <p>The code is very close to what we did in component tests. The main difference is we no longer spin up the service in a separate goroutine. Instead, all services run inside docker-compose using the same binaries we deploy to production.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">user</span> <span class="o">:=</span> <span class="nx">usersHTTPClient</span><span class="p">.</span><span class="nf">GetCurrentUser</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">originalBalance</span> <span class="o">:=</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Balance</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">usersGrpcClient</span><span class="p">.</span><span class="nf">UpdateTrainingBalance</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="o">&amp;</span><span class="nx">users</span><span class="p">.</span><span class="nx">UpdateTrainingBalanceRequest</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserId</span><span class="p">:</span> <span class="nx">userID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">AmountChange</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">user</span> <span class="p">=</span> <span class="nx">usersHTTPClient</span><span class="p">.</span><span class="nf">GetCurrentUser</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">require</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">originalBalance</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Balance</span><span class="p">,</span> <span class="s">&#34;Attendee&#39;s balance should be updated&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">trainingUUID</span> <span class="o">:=</span> <span class="nx">trainingsHTTPClient</span><span class="p">.</span><span class="nf">CreateTraining</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="s">&#34;some note&#34;</span><span class="p">,</span> <span class="nx">hour</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">trainingsResponse</span> <span class="o">:=</span> <span class="nx">trainingsHTTPClient</span><span class="p">.</span><span class="nf">GetTrainings</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">require</span><span class="p">.</span><span class="nf">Len</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">trainingsResponse</span><span class="p">.</span><span class="nx">Trainings</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">require</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">trainingUUID</span><span class="p">,</span> <span class="nx">trainingsResponse</span><span class="p">.</span><span class="nx">Trainings</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">Uuid</span><span class="p">,</span> <span class="s">&#34;Attendee should see the training&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">user</span> <span class="p">=</span> <span class="nx">usersHTTPClient</span><span class="p">.</span><span class="nf">GetCurrentUser</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">require</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">originalBalance</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Balance</span><span class="p">,</span> <span class="s">&#34;Attendee&#39;s balance should be updated after a training is scheduled&#34;</span><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0e3e9d80eb14639bc42935795f7ca3b73da36304%2Finternal%2Fcommon%2Ftests%2Fe2e_test.go%23L49" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/tests/e2e_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0e3e9d80eb14639bc42935795f7ca3b73da36304%2Finternal%2Fcommon%2Ftests%2Fe2e_test.go%23L49" target="_blank">Full source</a> </div> <h3 id="acceptance-tests">Acceptance tests</h3> <p>You can often find acceptance tests defined as the next level after unit and integration tests. We see them as orthogonal to the technical aspects of tests. An acceptance test focuses on a complete business feature instead of implementation details. As Martin Fowler puts it:</p> <blockquote> <p>Here&rsquo;s the thing: At one point you should make sure to test that your software works correctly from a user&rsquo;s perspective, not just from a technical perspective. What you call these tests is really not that important. Having these tests, however, is. Pick a term, stick to it, and write those tests.</p> <footer> <strong></strong> <cite> <a href="proxy.php?url=https%3A%2F%2Fmartinfowler.com%2Farticles%2Fpractical-test-pyramid.html" title="https://martinfowler.com/articles/practical-test-pyramid.html" target="_blank">Acceptance Tests — Do Your Features Work Correctly?</a> </cite> </footer> </blockquote> <p>In our case, component tests and end-to-end tests can both be considered acceptance tests.</p> <p>If you like, you can use the <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FBehavior-driven_development" target="_blank">BDD</a> style for some of them: it makes them easier to read but adds some boilerplate.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="2202" height="1816" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-test-architecture%2Facceptance_hu7a5b5d5cc96d9ada230aeb87ed5b9159_295270_2202x1816_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-test-architecture%5C%2Facceptance_hu7a5b5d5cc96d9ada230aeb87ed5b9159_295270_2202x1816_resize_q80_lanczos.jpg"" /> <h2 id="can-we-sleep-well-now">Can we sleep well now?</h2> <blockquote> <p>A service is not really tested until we break it in production.</p> <footer> <strong></strong> <cite> <a href="proxy.php?url=https%3A%2F%2Fitrevolution.com%2Fthe-devops-handbook%2F" title="https://itrevolution.com/the-devops-handbook/" target="_blank">Jesse Robbins, according to The DevOps Handbook</a> </cite> </footer> </blockquote> <p>A reliable test suite will catch most of your bugs so you can deliver consistently. But you want to be ready for outages anyway. We now enter the topic of monitoring, observability, and chaos engineering. However, this is out of scope for today.</p> <p>Just keep in mind that no test suite will give you total confidence. It&rsquo;s also crucial to have a straightforward process in place that allows quick rollbacks, reverts, and undoing migrations.</p> <h2 id="moving-on">Moving on</h2> <p>If you look through the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F0e3e9d80eb14639bc42935795f7ca3b73da36304" target="_blank">full commit</a>, you might notice how we inject dependencies now is not very elegant. We&rsquo;re going to iterate on this in the future.</p> <p>We have the test suite in place, but we&rsquo;re still missing a critical part: it&rsquo;s not running in our Continuous Integration pipeline.</p> <p>Also, coming up with proper docker-compose and environment variables for tests wasn&rsquo;t trivial. The current solution works but is not straightforward to understand if you don&rsquo;t know what&rsquo;s going on. And running the tests in an automated build will only make it more complicated.</p> <p>I will cover these topics in another post in a few weeks, focusing more on the infrastructure and CI/CD in general. We plan to write on more advanced topics in 2021, and Robert will start with a post about secure repositories.</p> <p>Stay safe, and see you soon!</p>Combining DDD, CQRS, and Clean Architecture in Gohttps://threedots.tech/post/ddd-cqrs-clean-architecture-combined/Thu, 05 Nov 2020 00:00:00 +0100https://threedots.tech/post/ddd-cqrs-clean-architecture-combined/In the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F">previous articles</a>, we introduced techniques like DDD Lite, CQRS, and Clean (Hexagonal) Architecture. Even when used alone, they are beneficial. But they work best together. Like Power Rangers. Unfortunately, using them together in a real project is not easy. <strong>In this article, I will show you how to connect DDD Lite, CQRS, and Clean Architecture in the most pragmatic and efficient way.</strong> <h3 id="why-should-i-care">Why should I care?</h3> <p>Working on a programming project is similar to planning and building a residential district. If you know the district will expand in the near future, you need to keep space for future improvements. Even if at the beginning it may look like a waste of space. You should reserve room for future facilities like residential blocks, hospitals, and temples. <strong>Without that, you will be forced to destroy buildings and streets to make space for new ones.</strong> It&rsquo;s much better to think about that earlier.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="2000" height="1083" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fddd-cqrs-clean-architecture-combined%2Fdistrict-empty_hu220d4ff6c72c1d6020e531f239fbe9c2_271317_2000x1083_resize_q80_h2_lanczos_3.webp" alt="Empty district" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fddd-cqrs-clean-architecture-combined%5C%2Fdistrict-empty_hu220d4ff6c72c1d6020e531f239fbe9c2_271317_2000x1083_resize_lanczos_3.png"" /> <p>The situation is the same with code. If you know the project will be developed for longer than one month, you should keep the long term in mind from the beginning. <strong>You need to write your code in a way that won&rsquo;t block your future work. Even if at the beginning it may look like over-engineering and a lot of extra boilerplate, you need to keep the long term in mind.</strong></p> <p>This doesn&rsquo;t mean you need to plan every feature you will implement in the future &ndash; it&rsquo;s actually the opposite. This approach helps you adapt to new requirements or a changing understanding of your domain. Big upfront design is not needed here. This is critical in current times, when the world is changing fast and those who can&rsquo;t adapt can simply go out of business.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="2000" height="1083" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fddd-cqrs-clean-architecture-combined%2Fdistrict-full_hu4591c93297b951538442105f8fe9960b_269516_2000x1083_resize_q80_h2_lanczos_3.webp" alt="Full district" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fddd-cqrs-clean-architecture-combined%5C%2Fdistrict-full_hu4591c93297b951538442105f8fe9960b_269516_2000x1083_resize_lanczos_3.png"" /> <p><strong>This is exactly what these patterns give you when combined: the ability to maintain constant development speed. Without destroying or touching existing code too much.</strong></p> <p>Does it require more thinking and planning? Is it a more challenging way? Do you need to have extra knowledge to do that? Sure! <strong>But the long term result is worth that!</strong> Fortunately, you are in the right place to learn that. &#x1f609;</p> <p>But let&rsquo;s leave the theory behind us. Let&rsquo;s get to the code. In this article, we will skip the reasoning for our design choices. We described these already in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F">the previous articles</a>. If you haven&rsquo;t read them yet, I recommend doing so &ndash; you will understand this article better.</p> <p>As in previous articles, we will base our code on refactoring a real open-source project. This should make the examples more realistic and applicable to your own projects.</p> <p>Are you ready?</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="this-is-not-just-another-article-with-random-code-snippets">This is not just another article with random code snippets.</h4> <p>This post is part of a bigger series where we show how to build <strong>Go applications that are easy to develop, maintain, and fun to work with in the long term.</strong> We are doing it by sharing proven techniques based on many experiments we did with teams we lead and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%3Futm_source%3Dabout-wild-workouts%23thats-great-but-do-you-have-any-evidence-it-works">scientific research</a>.</p> <p>You can learn these patterns by building with us a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%3Futm_source%3Dabout-wild-workouts%23what-wild-workouts-can-do">fully functional</a> example Go web application &ndash; <strong>Wild Workouts</strong>.</p> <p>We did one thing differently &ndash; <strong>we included some subtle issues to the initial Wild Workouts implementation</strong>. Have we lost our minds to do that? Not yet. 😉 These issues are common for many Go projects. <strong>In the long term, these small issues become critical and stop adding new features.</strong></p> <p><strong>It&rsquo;s one of the essential skills of a senior or lead developer; you always need to keep long-term implications in mind.</strong></p> <p>We will fix them by <strong>refactoring</strong> Wild Workouts. In that way, you will quickly understand the techniques we share.</p> <p>Do you know that feeling after reading an article about some technique and trying implement it only to be blocked by some issues skipped in the guide? Cutting these details makes articles shorter and increases page views, but this is not our goal. Our goal is to create content that provides enough know-how to apply presented techniques. If you did not read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">previous articles from the series</a> yet, we highly recommend doing that.</p> <p>We believe that in some areas, there are no shortcuts. If you want to build complex applications in a fast and efficient way, you need to spend some time learning that. If it was simple, we wouldn&rsquo;t have large amounts of scary legacy code.</p> <p>Here&rsquo;s <strong><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">the full list of 14 articles</a></strong> released so far.</p> <p><strong>The full source code</strong> of Wild Workouts is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%3Futm_source%3Dabout-wild-workouts" target="_blank">GitHub</a>. Don&rsquo;t forget to leave a star for our project! ⭐</p> </p></div> </div> <h3 id="lets-refactor">Let&rsquo;s refactor</h3> <p>Let&rsquo;s start our refactoring with the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%23domain-first-approach">Domain-First</a> approach. We will begin by introducing a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F%23the-dependency-inversion-principle">domain layer</a>. This ensures that implementation details do not affect our domain code. <strong>We can also put all our efforts into understanding the business problem, not writing boring database queries and API endpoints.</strong></p> <p>The Domain-First approach works well for both rescue (refactoring &#x1f609;) and greenfield projects.</p> <p>To start building my domain layer, I needed to identify what the application is actually doing. This article will focus on refactoring of <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Ftree%2Fv2.5%2Finternal%2Ftrainings" target="_blank"><code>trainings</code></a> Wild Workouts microservice. I started with identifying use cases handled by the application. After previous <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F">refactoring to Clean Architecture</a>, we can find it in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fv2.4%2Finternal%2Ftrainings%2Fapp%2Ftraining_service.go%23L32" target="_blank"><code>TrainingService</code></a>. When I work with a messy application, I look at RPC and HTTP endpoints to find supported use cases.</p> <p>One of the functionalities I identified is the <strong>approval of training reschedule</strong>. In Wild Workouts, a training reschedule approval is required if it was requested less than 24 hours before its date. If an attendee requests a reschedule, the trainer needs to approve it. If a trainer requests a reschedule, the attendee needs to accept it.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">- func (c TrainingService) ApproveTrainingReschedule(ctx context.Context, user auth.User, trainingUUID string) error { </span></span></span><span class="line"><span class="cl"><span class="gd">- return c.repo.ApproveTrainingReschedule(ctx, trainingUUID, func(training Training) (Training, error) { </span></span></span><span class="line"><span class="cl"><span class="gd">- if training.ProposedTime == nil { </span></span></span><span class="line"><span class="cl"><span class="gd">- return Training{}, errors.New(&#34;training has no proposed time&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- if training.MoveProposedBy == nil { </span></span></span><span class="line"><span class="cl"><span class="gd">- return Training{}, errors.New(&#34;training has no MoveProposedBy&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- if *training.MoveProposedBy == &#34;trainer&#34; &amp;&amp; training.UserUUID != user.UUID { </span></span></span><span class="line"><span class="cl"><span class="gd">- return Training{}, errors.Errorf(&#34;user &#39;%s&#39; cannot approve reschedule of user &#39;%s&#39;&#34;, user.UUID, training.UserUUID) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- if *training.MoveProposedBy == user.Role { </span></span></span><span class="line"><span class="cl"><span class="gd">- return Training{}, errors.New(&#34;reschedule cannot be accepted by requesting person&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- training.Time = *training.ProposedTime </span></span></span><span class="line"><span class="cl"><span class="gd">- training.ProposedTime = nil </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- return training, nil </span></span></span><span class="line"><span class="cl"><span class="gd">- }) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F8d9274811559399461aa9f6bf3829316b8ddfb63" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/commit/8d9274811559399461aa9f6bf3829316b8ddfb63</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F8d9274811559399461aa9f6bf3829316b8ddfb63" target="_blank">Full source</a> </div> <h3 id="start-with-the-domain">Start with the domain</h3> <p>Even if this doesn&rsquo;t look like the worst code you&rsquo;ve ever seen, functions like <code>ApproveTrainingReschedule</code> tend to get more complex over time. More complex functions mean more potential bugs during future development.</p> <p>This is even more likely if you are new to the project and lack the <em>&ldquo;shaman knowledge&rdquo;</em> about it. <strong>You should always consider all the people who will work on the project after you, and make it resistant to being accidentally broken by them. That will help your project avoid becoming the legacy code that everybody is afraid to touch.</strong> You probably know that feeling when you&rsquo;re new to a project and afraid to touch anything for fear of breaking the system.</p> <p>It&rsquo;s not uncommon for people to change jobs more often than every two years. This makes it even more critical for long-term project development.</p> <p>If you don&rsquo;t believe this code may become complex, I recommend checking the Git history of the worst place in the project you work on. In most cases, that worst code started with <em>&ldquo;just a couple of simple ifs&rdquo;</em>. &#x1f609; The more complex the code becomes, the more difficult it will be to simplify later. <strong>We should be sensitive to emerging complexity and try to simplify it as soon as we can.</strong></p> <h4 id="training-domain-entity"><code>Training</code> domain entity</h4> <p>While analyzing the current use cases handled by the <code>trainings</code> microservice, I found that they are all related to a <em>training</em>. It&rsquo;s natural to create a <code>Training</code> type to handle these operations.</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="833" height="288" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fddd-cqrs-clean-architecture-combined%2Ftraining-svc-methods_hu23e5b80bfce1cbb76338ba1eb2a6298a_91843_833x288_resize_q80_h2_lanczos.webp" alt="Service methods" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fddd-cqrs-clean-architecture-combined%5C%2Ftraining-svc-methods_hu23e5b80bfce1cbb76338ba1eb2a6298a_91843_833x288_resize_q80_lanczos.jpg"" /> <div class="code-ref"> Methods of refactored <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fv2.4%2Finternal%2Ftrainings%2Fapp%2Ftraining_service.go%23L32" target='_blank'>TrainingService</a> </div></p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="noun--entity">noun == entity</h4> <p>Is this a valid approach for discovering entities? Well, not really.</p> <p>DDD provides tools that help us model complex domains without guessing (<em>Strategic DDD Patterns</em>, <em>Aggregates</em>). We don&rsquo;t want to guess what our aggregates look like &ndash; we want tools to discover them. The Event Storming technique is extremely useful here&hellip; but that&rsquo;s a topic for an entire separate article.</p> <p>The topic is complex enough to deserve a couple of articles. And that&rsquo;s what we will do shortly. &#x1f609;</p> <p>Does this mean you shouldn&rsquo;t use these techniques without Strategic DDD Patterns? Of course not! The current approach can be good enough for simpler projects. Unfortunately (or fortunately &#x1f609;), not all projects are simple.</p> </p></div> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">training</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Training</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">uuid</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">userUUID</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">userName</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">time</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"> <span class="nx">notes</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">proposedNewTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"> <span class="nx">moveProposedBy</span> <span class="nx">UserType</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">canceled</span> <span class="kt">bool</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Ftraining.go%23L10" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/domain/training/training.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Ftraining.go%23L10" target="_blank">Full source</a> </div> <p>All fields are private to provide encapsulation. This is critical to meet the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%23the-second-rule-always-keep-a-valid-state-in-the-memory">&ldquo;always keep a valid state in the memory&rdquo;</a> rule from the article about DDD Lite.</p> <p>Thanks to validation in the constructor and encapsulated fields, we are sure that <code>Training</code> is always valid. <strong>Now, someone who has no knowledge about the project cannot use it in the wrong way.</strong></p> <p>The same rule applies to any methods provided by <code>Training</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">training</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewTraining</span><span class="p">(</span><span class="nx">uuid</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">userUUID</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">userName</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">trainingTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Training</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">uuid</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;empty training uuid&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">userUUID</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;empty userUUID&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">userName</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;empty userName&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">trainingTime</span><span class="p">.</span><span class="nf">IsZero</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;zero training time&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">Training</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">uuid</span><span class="p">:</span> <span class="nx">uuid</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">userUUID</span><span class="p">:</span> <span class="nx">userUUID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">userName</span><span class="p">:</span> <span class="nx">userName</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">time</span><span class="p">:</span> <span class="nx">trainingTime</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Ftraining.go%23L25" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/domain/training/training.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Ftraining.go%23L25" target="_blank">Full source</a> </div> <h4 id="approve-reschedule-in-the-domain-layer">Approve reschedule in the domain layer</h4> <p>As described in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%23the-first-rule---reflect-your-business-logic-literally">DDD Lite introduction</a>, we build our domain with methods oriented on behaviours. Not on data. Let&rsquo;s model <code>ApproveReschedule</code> on our domain entity.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">training</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ...s </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">Training</span><span class="p">)</span> <span class="nf">IsRescheduleProposed</span><span class="p">()</span> <span class="kt">bool</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">!</span><span class="nx">t</span><span class="p">.</span><span class="nx">moveProposedBy</span><span class="p">.</span><span class="nf">IsZero</span><span class="p">()</span> <span class="o">&amp;&amp;</span> <span class="p">!</span><span class="nx">t</span><span class="p">.</span><span class="nx">proposedNewTime</span><span class="p">.</span><span class="nf">IsZero</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">ErrNoRescheduleRequested</span> <span class="p">=</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;no training reschedule was requested yet&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">Training</span><span class="p">)</span> <span class="nf">ApproveReschedule</span><span class="p">(</span><span class="nx">userType</span> <span class="nx">UserType</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">t</span><span class="p">.</span><span class="nf">IsRescheduleProposed</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">WithStack</span><span class="p">(</span><span class="nx">ErrNoRescheduleRequested</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">t</span><span class="p">.</span><span class="nx">moveProposedBy</span> <span class="o">==</span> <span class="nx">userType</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;trying to approve reschedule by the same user type which proposed reschedule (%s)&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">userType</span><span class="p">.</span><span class="nf">String</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nx">time</span> <span class="p">=</span> <span class="nx">t</span><span class="p">.</span><span class="nx">proposedNewTime</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nx">proposedNewTime</span> <span class="p">=</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nx">moveProposedBy</span> <span class="p">=</span> <span class="nx">UserType</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Freschedule.go%23L47" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/domain/training/reschedule.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Freschedule.go%23L47" target="_blank">Full source</a> </div> <p>If you haven&rsquo;t had the chance to read:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F">DDD Lite introduction</a>,</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F">Introducing Clean Architecture</a>,</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbasic-cqrs-in-go%2F">Introducing basic CQRS</a>,</li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F">Repository pattern</a>.</li> </ul> <p>I highly recommend checking them out. They will help you understand this article better. They explain the decisions and techniques we combine in this article.</p> <h3 id="orchestrate-with-command">Orchestrate with command</h3> <p>Now the application layer can be responsible only for the orchestration of the flow. There is no domain logic there. <strong>We hide the entire business complexity in the domain layer. This was exactly our goal.</strong></p> <p>For getting and saving a training, we use the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F">Repository pattern</a>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">command</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">ApproveTrainingRescheduleHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">ApproveTrainingReschedule</span><span class="p">)</span> <span class="p">(</span><span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">logs</span><span class="p">.</span><span class="nf">LogCommandExecution</span><span class="p">(</span><span class="s">&#34;ApproveTrainingReschedule&#34;</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">.</span><span class="nx">repo</span><span class="p">.</span><span class="nf">UpdateTraining</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">TrainingUUID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">tr</span> <span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">originalTrainingTime</span> <span class="o">:=</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">Time</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">ApproveReschedule</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nf">Type</span><span class="p">());</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">trainerService</span><span class="p">.</span><span class="nf">MoveTraining</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">Time</span><span class="p">(),</span> <span class="nx">originalTrainingTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">tr</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Fapprove_training_reschedule.go%23L39" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/command/approve_training_reschedule.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Fapprove_training_reschedule.go%23L39" target="_blank">Full source</a> </div> <h3 id="refactoring-of-training-cancelation">Refactoring of training cancelation</h3> <p>Let&rsquo;s now take a look at <code>CancelTraining</code> from <code>TrainingService</code>.</p> <p>The domain logic is simple: you can cancel a training up to 24 hours before its date. If it&rsquo;s less than 24 hours before the training and you want to cancel it anyway:</p> <ul> <li>if you are the trainer, the attendee will get their training &ldquo;back&rdquo; plus one extra session (nobody likes to change plans on the same day!)</li> <li>if you are the attendee, you will lose this training</li> </ul> <p>This is how the current implementation looks like:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">- func (c TrainingService) CancelTraining(ctx context.Context, user auth.User, trainingUUID string) error { </span></span></span><span class="line"><span class="cl"><span class="gd">- return c.repo.CancelTraining(ctx, trainingUUID, func(training Training) error { </span></span></span><span class="line"><span class="cl"><span class="gd">- if user.Role != &#34;trainer&#34; &amp;&amp; training.UserUUID != user.UUID { </span></span></span><span class="line"><span class="cl"><span class="gd">- return errors.Errorf(&#34;user &#39;%s&#39; is trying to cancel training of user &#39;%s&#39;&#34;, user.UUID, training.UserUUID) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- var trainingBalanceDelta int </span></span></span><span class="line"><span class="cl"><span class="gd">- if training.CanBeCancelled() { </span></span></span><span class="line"><span class="cl"><span class="gd">- // just give training back </span></span></span><span class="line"><span class="cl"><span class="gd">- trainingBalanceDelta = 1 </span></span></span><span class="line"><span class="cl"><span class="gd">- } else { </span></span></span><span class="line"><span class="cl"><span class="gd">- if user.Role == &#34;trainer&#34; { </span></span></span><span class="line"><span class="cl"><span class="gd">- // 1 for cancelled training +1 fine for cancelling by trainer less than 24h before training </span></span></span><span class="line"><span class="cl"><span class="gd">- trainingBalanceDelta = 2 </span></span></span><span class="line"><span class="cl"><span class="gd">- } else { </span></span></span><span class="line"><span class="cl"><span class="gd">- // fine for cancelling less than 24h before training </span></span></span><span class="line"><span class="cl"><span class="gd">- trainingBalanceDelta = 0 </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- if trainingBalanceDelta != 0 { </span></span></span><span class="line"><span class="cl"><span class="gd">- err := c.userService.UpdateTrainingBalance(ctx, training.UserUUID, trainingBalanceDelta) </span></span></span><span class="line"><span class="cl"><span class="gd">- if err != nil { </span></span></span><span class="line"><span class="cl"><span class="gd">- return errors.Wrap(err, &#34;unable to change trainings balance&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- err := c.trainerService.CancelTraining(ctx, training.Time) </span></span></span><span class="line"><span class="cl"><span class="gd">- if err != nil { </span></span></span><span class="line"><span class="cl"><span class="gd">- return errors.Wrap(err, &#34;unable to cancel training&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- return nil </span></span></span><span class="line"><span class="cl"><span class="gd">- }) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span></code></pre></div><p>You can see some kind of &ldquo;algorithm&rdquo; for calculating the training balance delta during cancellation. That&rsquo;s not a good sign in the application layer.</p> <p>Logic like this one should live in our domain layer. <strong>If you start to see some <code>if</code>&rsquo;s related to logic in your application layer, you should think about how to move it to the domain layer.</strong> It will be easier to test and re-use in other places.</p> <p>It may depend on the project, but often <strong>domain logic is fairly stable after initial development and can live unchanged for a long time</strong>. It can survive moving between services, framework changes, library changes, and API changes. Thanks to this separation, we can make all these changes in a much safer and faster way.</p> <p>Let&rsquo;s decompose the <code>CancelTraining</code> method to multiple, separated pieces. That will allow us to test and change them independently.</p> <p>First, we need to handle cancellation logic and marking <code>Training</code> as canceled.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">training</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="nx">Training</span><span class="p">)</span> <span class="nf">CanBeCanceledForFree</span><span class="p">()</span> <span class="kt">bool</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">t</span><span class="p">.</span><span class="nx">time</span><span class="p">.</span><span class="nf">Sub</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">())</span> <span class="o">&gt;=</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Hour</span><span class="o">*</span><span class="mi">24</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">ErrTrainingAlreadyCanceled</span> <span class="p">=</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;training is already canceled&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">Training</span><span class="p">)</span> <span class="nf">Cancel</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">t</span><span class="p">.</span><span class="nf">IsCanceled</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ErrTrainingAlreadyCanceled</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nx">canceled</span> <span class="p">=</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Fcancel.go%23L14" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/domain/training/cancel.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Fcancel.go%23L14" target="_blank">Full source</a> </div> <p>Nothing really complicated here. That&rsquo;s good!</p> <p>The second part that needs to move is the &ldquo;algorithm&rdquo; for calculating the trainings balance after cancellation. In theory, we could put it in the <code>Cancel()</code> method, but in my opinion it would break the <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FSingle-responsibility_principle" target="_blank">Single Responsibility Principle</a> and <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FCommand%25E2%2580%2593query_separation" target="_blank">CQS</a>. And I like small functions.</p> <p>But where should we put it? Some object? A domain service? In some languages, like the one that starts with <em>J</em> and ends with <em>ava</em>, that would make sense. But in Go, it&rsquo;s good enough to just create a simple function.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">training</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// CancelBalanceDelta return trainings balance delta that should be adjusted after training cancelation. </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="nf">CancelBalanceDelta</span><span class="p">(</span><span class="nx">tr</span> <span class="nx">Training</span><span class="p">,</span> <span class="nx">cancelingUserType</span> <span class="nx">UserType</span><span class="p">)</span> <span class="kt">int</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">CanBeCanceledForFree</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// just give training back </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="mi">1</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">switch</span> <span class="nx">cancelingUserType</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">Trainer</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 1 for cancelled training +1 &#34;fine&#34; for cancelling by trainer less than 24h before training </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="mi">2</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">Attendee</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="c1">// &#34;fine&#34; for cancelling less than 24h before training </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="mi">0</span> </span></span><span class="line"><span class="cl"> <span class="k">default</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;not supported user type %s&#34;</span><span class="p">,</span> <span class="nx">cancelingUserType</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Fcancel_balance.go%23L6" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/domain/training/cancel_balance.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Fcancel_balance.go%23L6" target="_blank">Full source</a> </div> <p>The code is now straightforward. <strong>I can imagine that I could sit with any non-technical person and go through this code to explain how it works.</strong></p> <p>What about tests? It may be a bit controversial, but in my opinion tests are redundant here. Test code would replicate the implementation of the function. Any change in the calculation algorithm would require copying the logic to the tests. I wouldn&rsquo;t write a test here, but if you&rsquo;ll sleep better at night &ndash; why not!</p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/ddd-cqrs-clean-architecture-combined/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;ddd;domain-driven design;clean-architecture"> <input type="hidden" name="fields[blog_series]" value="Modern Business Software in Go"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h4 id="moving-canceltraining-to-command">Moving CancelTraining to command</h4> <p>Our domain is ready, so let&rsquo;s now use it. We will do it in the same way as previously:</p> <ol> <li>getting the entity from the repository,</li> <li>orchestration of domain stuff,</li> <li>calling external <code>trainer</code> service to cancel the training (this service is the point of truth of &ldquo;trainer&rsquo;s calendar&rdquo;),</li> <li>returning entity to be saved in the database.</li> </ol> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">command</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">CancelTrainingHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">CancelTraining</span><span class="p">)</span> <span class="p">(</span><span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">logs</span><span class="p">.</span><span class="nf">LogCommandExecution</span><span class="p">(</span><span class="s">&#34;CancelTrainingHandler&#34;</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">.</span><span class="nx">repo</span><span class="p">.</span><span class="nf">UpdateTraining</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">TrainingUUID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">tr</span> <span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">Cancel</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">balanceDelta</span> <span class="o">:=</span> <span class="nx">training</span><span class="p">.</span><span class="nf">CancelBalanceDelta</span><span class="p">(</span><span class="o">*</span><span class="nx">tr</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nf">Type</span><span class="p">());</span> <span class="nx">balanceDelta</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">userService</span><span class="p">.</span><span class="nf">UpdateTrainingBalance</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">UserUUID</span><span class="p">(),</span> <span class="nx">balanceDelta</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;unable to change trainings balance&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">trainerService</span><span class="p">.</span><span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">Time</span><span class="p">());</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;unable to cancel training&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">tr</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Fcancel_training.go%23L36" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/command/cancel_training.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Fcancel_training.go%23L36" target="_blank">Full source</a> </div> <h3 id="repository-refactoring">Repository refactoring</h3> <p>The initial implementation of the repository was pretty tricky because of the custom method for every use case.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">- type trainingRepository interface { </span></span></span><span class="line"><span class="cl"><span class="gd">- FindTrainingsForUser(ctx context.Context, user auth.User) ([]Training, error) </span></span></span><span class="line"><span class="cl"><span class="gd">- AllTrainings(ctx context.Context) ([]Training, error) </span></span></span><span class="line"><span class="cl"><span class="gd">- CreateTraining(ctx context.Context, training Training, createFn func() error) error </span></span></span><span class="line"><span class="cl"><span class="gd">- CancelTraining(ctx context.Context, trainingUUID string, deleteFn func(Training) error) error </span></span></span><span class="line"><span class="cl"><span class="gd">- RescheduleTraining(ctx context.Context, trainingUUID string, newTime time.Time, updateFn func(Training) (Training, error)) error </span></span></span><span class="line"><span class="cl"><span class="gd">- ApproveTrainingReschedule(ctx context.Context, trainingUUID string, updateFn func(Training) (Training, error)) error </span></span></span><span class="line"><span class="cl"><span class="gd">- RejectTrainingReschedule(ctx context.Context, trainingUUID string, updateFn func(Training) (Training, error)) error </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span></code></pre></div><p>Thanks to introducing the <code>training.Training</code> entity, we can have a much simpler version: one method for adding a new training and one for updating.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">training</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Repository</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">AddTraining</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">tr</span> <span class="o">*</span><span class="nx">Training</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nf">GetTraining</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">trainingUUID</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">user</span> <span class="nx">User</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Training</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nf">UpdateTraining</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingUUID</span> <span class="kt">string</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">user</span> <span class="nx">User</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">tr</span> <span class="o">*</span><span class="nx">Training</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Training</span><span class="p">,</span> <span class="kt">error</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Frepository.go%23L16" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/domain/training/repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fdomain%2Ftraining%2Frepository.go%23L16" target="_blank">Full source</a> </div> <p>As in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F">the previous article</a>, we implemented our repository using Firestore. We will also use Firestore in the current implementation. Keep in mind that this is an implementation detail &ndash; you can use any database you want. In the previous article, we showed example implementations using different databases.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">adapters</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="nx">TrainingsFirestoreRepository</span><span class="p">)</span> <span class="nf">UpdateTraining</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingUUID</span> <span class="kt">string</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">user</span> <span class="nx">training</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">tr</span> <span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">,</span> <span class="kt">error</span><span class="p">),</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingsCollection</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">trainingsCollection</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">r</span><span class="p">.</span><span class="nx">firestoreClient</span><span class="p">.</span><span class="nf">RunTransaction</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">tx</span> <span class="o">*</span><span class="nx">firestore</span><span class="p">.</span><span class="nx">Transaction</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">documentRef</span> <span class="o">:=</span> <span class="nx">trainingsCollection</span><span class="p">.</span><span class="nf">Doc</span><span class="p">(</span><span class="nx">trainingUUID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">firestoreTraining</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="nx">documentRef</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;unable to get actual docs&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">tr</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">unmarshalTraining</span><span class="p">(</span><span class="nx">firestoreTraining</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">training</span><span class="p">.</span><span class="nf">CanUserSeeTraining</span><span class="p">(</span><span class="nx">user</span><span class="p">,</span> <span class="o">*</span><span class="nx">tr</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">updatedTraining</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">updateFn</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">tr</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Set</span><span class="p">(</span><span class="nx">documentRef</span><span class="p">,</span> <span class="nx">r</span><span class="p">.</span><span class="nf">marshalTraining</span><span class="p">(</span><span class="nx">updatedTraining</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L83" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/adapters/trainings_firestore_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L83" target="_blank">Full source</a> </div> <h3 id="connecting-everything">Connecting everything</h3> <p>How do we use our code now? What about our ports layer? Thanks to the refactoring that Miłosz did in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F">refactoring to Clean Architecture</a> article, our ports layer is decoupled from other layers. That&rsquo;s why, after this refactoring, it requires almost no significant changes. We just call the application command instead of the application service.</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="2000" height="2000" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fintroducing-clean-architecture%2Fclean-arch-2_hua2fbaee330357286a5d6d11d974a364a_220043_2000x2000_resize_q80_h2_lanczos.webp" alt="Clean Architecture" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fintroducing-clean-architecture%5C%2Fclean-arch-2_hua2fbaee330357286a5d6d11d974a364a_220043_2000x2000_resize_q80_lanczos.jpg"" /> <div class="code-ref"> Clean/Hexagonal Architecture layers. </div></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">ports</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">HttpServer</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">app</span> <span class="nx">app</span><span class="p">.</span><span class="nx">Application</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">HttpServer</span><span class="p">)</span> <span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingUUID</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">().</span><span class="nf">Value</span><span class="p">(</span><span class="s">&#34;trainingUUID&#34;</span><span class="p">).(</span><span class="kt">string</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">newDomainUserFromAuthUser</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">RespondWithSlugError</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">app</span><span class="p">.</span><span class="nx">Commands</span><span class="p">.</span><span class="nx">CancelTraining</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">(),</span> <span class="nx">command</span><span class="p">.</span><span class="nx">CancelTraining</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">TrainingUUID</span><span class="p">:</span> <span class="nx">trainingUUID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">User</span><span class="p">:</span> <span class="nx">user</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">RespondWithSlugError</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fports%2Fhttp.go%23L87" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/ports/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fports%2Fhttp.go%23L87" target="_blank">Full source</a> </div> <h3 id="how-to-approach-such-refactoring-in-a-real-project">How to approach such refactoring in a real project?</h3> <p>It may not be obvious how to do such a refactoring in a real project. It&rsquo;s hard to do a code review and agree as a team on the refactoring direction.</p> <p>In my experience, the best approach is <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FPair_programming" target="_blank">Pair</a> or <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FMob_programming" target="_blank">Mob</a> programming. Even if at the beginning you may feel it&rsquo;s a waste of time, the knowledge sharing and instant review will save a lot of time in the future. Thanks to great knowledge sharing, you can work much faster after the initial project or refactoring phase.</p> <p>In this case, don&rsquo;t consider the time &ldquo;lost&rdquo; for Mob/Pair programming. Consider the time you may lose by not doing it. It will also help you finish the refactoring much faster because you won&rsquo;t need to wait for decisions. You can agree on them immediately.</p> <p>Mob and pair programming also work perfectly when implementing complex, greenfield projects. Knowledge sharing is an especially important investment in that case. I&rsquo;ve seen multiple times how this approach allowed teams to move very fast on the project in the long term.</p> <p>When you are doing refactoring, it&rsquo;s also critical to agree on reasonable timeboxes. <strong>And keep them.</strong> You can quickly lose your stakeholders&rsquo; trust when you spend an entire month on refactoring and the improvement is not visible. It&rsquo;s also critical to integrate and deploy your refactoring as fast as you can. Ideally, on a daily basis (if you can do it for non-refactoring work, I&rsquo;m sure you can do it for refactoring as well!). If your changes stay unmerged and undeployed for a longer time, you increase the chance of breaking functionality. It will also block any work in the refactored service or make changes harder to merge (it&rsquo;s not always possible to stop all other development around).</p> <p>But how do you know if the project is complex enough to use mob programming? Unfortunately, there is no magic formula for that. But there are questions you should ask yourself:</p> <ul> <li>do we understand the domain?</li> <li>do we know how to implement that?</li> <li>will it end up with a monstrous pull request that nobody will be able to review?</li> <li>can we risk worse knowledge sharing while not doing mob/pair programming?</li> </ul> <h3 id="summary">Summary</h3> <p>And we come to an end. &#x1f604;</p> <p>The entire diff for the refactoring is available on our <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F8d9274811559399461aa9f6bf3829316b8ddfb63" target="_blank">Wild Workouts GitHub</a> (watch out, it&rsquo;s huge!).</p> If you haven&rsquo;t had the chance to read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F">previous articles</a> yet, you know what to do! Even if some of the approaches used are simplified, you should already be able to use them in your project and see value from them. <p>I hope that after this article, you also see how all the introduced patterns work nicely together. If not yet, don&rsquo;t worry. <strong>It took me three years to connect all the dots.</strong> But it was worth the time spent. After I understood how everything is connected, I started to look at new projects in a totally different way. It allowed me and my teams to work more efficiently in the long term.</p> <p>It&rsquo;s also important to mention that, like all techniques, this combination is not a silver bullet. If you are creating a project that is not complex and won&rsquo;t be touched any time soon after one month of development, it&rsquo;s probably enough to put everything in one <code>main</code> package. &#x1f609; Just keep in mind when that one month of development becomes one year!</p> <p>We will also continue these topics in the next articles. We will shortly drift to <em>Strategic DDD Patterns</em>, which should also help you gain a higher-level perspective on your projects.</p> <p>Did this article help you understand how to connect DDD, Clean Architecture, and CQRS? Is something still unclear? Please let us know in the comments! We&rsquo;re happy to discuss all your questions!</p>How to use basic CQRS in Gohttps://threedots.tech/post/basic-cqrs-in-go/Mon, 28 Sep 2020 00:00:00 +0200https://threedots.tech/post/basic-cqrs-in-go/<p>You probably know at least one service that:</p> <ul> <li>has one big, unmaintainable model that is hard to understand and change,</li> <li>limits parallel work on new features,</li> <li>or can&rsquo;t scale optimally.</li> </ul> <p>But bad things often come in threes. It&rsquo;s not uncommon to see services with all these problems.</p> <p>What idea comes to mind first for solving these issues? Let&rsquo;s split it into more microservices!</p> <p>Unfortunately, without proper research and planning, the situation after blindly refactoring may actually be worse than before:</p> <ul> <li><strong>Business logic and flow may become even harder to understand</strong>: complex logic is often easier to understand if it&rsquo;s in one place.</li> <li><strong>Distributed transactions</strong>: things are sometimes together for a reason. A big transaction in one database is much faster and less complex than a distributed transaction across multiple services.</li> <li><strong>Adding new changes may require extra coordination</strong> if one of the services is owned by another team.</li> </ul> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="800" height="562" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fintroducing-cqrs%2Fmore-microservices_hu28cedd6a052bf106df026426fe6274fc_62154_800x562_resize_q80_h2_lanczos.webp" alt="MAKE MORE MICROSERVICES!" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fintroducing-cqrs%5C%2Fmore-microservices_hu28cedd6a052bf106df026426fe6274fc_62154_800x562_resize_q80_lanczos.jpg"" /> <div class="code-ref"> Microservices are useful, but they will not solve all your issues... </div></p> <p>To be clear: I&rsquo;m not an enemy of microservices. <strong>I&rsquo;m just against blindly applying microservices in a way that introduces unnecessary complexity and mess instead of making our lives easier.</strong></p> <p>Another approach is using CQRS (Command Query Responsibility Segregation) with previously described <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F">Clean Architecture</a> and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F">DDD Lite</a>. <strong>It can solve the mentioned problems in a much simpler way.</strong></p> <h3 id="isnt-cqrs-a-complex-technique">Isn&rsquo;t CQRS a complex technique?</h3> <p>Isn&rsquo;t CQRS one of those C#/Java/über enterprise patterns that are hard to implement and make a big mess in the code? Many books, presentations, and articles describe CQRS as a very complicated pattern. But that&rsquo;s not the case.</p> <p><strong>In practice, CQRS is a simple pattern that doesn&rsquo;t require a lot of investment.</strong> <strong>It can be easily extended with more complex techniques like event-driven architecture, event-sourcing, or polyglot persistence.</strong> But these aren&rsquo;t always needed. Even without any extra patterns, CQRS can offer better decoupling and code structure that is easier to understand.</p> <p>When to not use CQRS in Go? How to get all benefits from CQRS? You can learn all that in today&rsquo;s article. &#x1f609;</p> <p>As always, I will do this by refactoring the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">Wild Workouts</a> application.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="this-is-not-just-another-article-with-random-code-snippets">This is not just another article with random code snippets.</h4> <p>This post is part of a bigger series where we show how to build <strong>Go applications that are easy to develop, maintain, and fun to work with in the long term.</strong> We are doing it by sharing proven techniques based on many experiments we did with teams we lead and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%3Futm_source%3Dabout-wild-workouts%23thats-great-but-do-you-have-any-evidence-it-works">scientific research</a>.</p> <p>You can learn these patterns by building with us a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%3Futm_source%3Dabout-wild-workouts%23what-wild-workouts-can-do">fully functional</a> example Go web application &ndash; <strong>Wild Workouts</strong>.</p> <p>We did one thing differently &ndash; <strong>we included some subtle issues to the initial Wild Workouts implementation</strong>. Have we lost our minds to do that? Not yet. 😉 These issues are common for many Go projects. <strong>In the long term, these small issues become critical and stop adding new features.</strong></p> <p><strong>It&rsquo;s one of the essential skills of a senior or lead developer; you always need to keep long-term implications in mind.</strong></p> <p>We will fix them by <strong>refactoring</strong> Wild Workouts. In that way, you will quickly understand the techniques we share.</p> <p>Do you know that feeling after reading an article about some technique and trying implement it only to be blocked by some issues skipped in the guide? Cutting these details makes articles shorter and increases page views, but this is not our goal. Our goal is to create content that provides enough know-how to apply presented techniques. If you did not read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">previous articles from the series</a> yet, we highly recommend doing that.</p> <p>We believe that in some areas, there are no shortcuts. If you want to build complex applications in a fast and efficient way, you need to spend some time learning that. If it was simple, we wouldn&rsquo;t have large amounts of scary legacy code.</p> <p>Here&rsquo;s <strong><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">the full list of 14 articles</a></strong> released so far.</p> <p><strong>The full source code</strong> of Wild Workouts is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%3Futm_source%3Dabout-wild-workouts" target="_blank">GitHub</a>. Don&rsquo;t forget to leave a star for our project! ⭐</p> </p></div> </div> <h3 id="how-to-implement-basic-cqrs-in-go">How to implement basic CQRS in Go</h3> <p>CQRS (Command Query Responsibility Segregation) was initially <a href="proxy.php?url=https%3A%2F%2Fcqrs.files.wordpress.com%2F2010%2F11%2Fcqrs_documents.pdf" target="_blank">described by Greg Young</a>. <strong>It has one simple assumption: instead of having one big model for reads and writes, you should have two separate models. One for writes and one for reads.</strong> It also introduces concepts of <em>command</em> and <em>query</em>, and leads to splitting application services into two separate types: command and query handlers.</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1779" height="679" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fintroducing-cqrs%2Fnon-cqrs-architecture_hub3393e4d2884102f3f287e9a63bc722c_96987_1779x679_resize_q80_h2_lanczos.webp" alt="Non-CQRS architecture" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fintroducing-cqrs%5C%2Fnon-cqrs-architecture_hub3393e4d2884102f3f287e9a63bc722c_96987_1779x679_resize_q80_lanczos.jpg"" /> <div class="code-ref"> Standard, non-CQRS architecture </div></p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1779" height="679" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fintroducing-cqrs%2Fcqrs-architecture_hu79ed5d69a816456afea28c33a0d86dac_112338_1779x679_resize_q80_h2_lanczos.webp" alt="CQRS architecture" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fintroducing-cqrs%5C%2Fcqrs-architecture_hu79ed5d69a816456afea28c33a0d86dac_112338_1779x679_resize_q80_lanczos.jpg"" /> <div class="code-ref"> CQRS architecture </div></p> <h4 id="command-vs-query">Command vs Query</h4> <p>In the simplest terms: <strong>a Query should not modify anything, just return data. A command is the opposite: it should make changes in the system but not return any data.</strong> This lets us cache queries more efficiently and reduces the complexity of commands.</p> <p>This may sound like a serious constraint, but in practice, it&rsquo;s not. Most operations are either reads or writes. Very rarely both.</p> <p>Of course, for a query, we don&rsquo;t consider side effects like logs or metrics as modifying anything. For commands, returning an error is perfectly normal.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> As with most rules, it&rsquo;s okay to break them&hellip; as long as you <strong>understand why they were introduced and what tradeoffs</strong> you&rsquo;re making. In practice, you rarely need to break these rules. I&rsquo;ll share examples at the end of the article. </p></div> </div> <p>What does the most basic implementation look like in practice? In the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F">previous article</a>, Miłosz introduced an application service that executes application use cases. Let&rsquo;s start by cutting this service into separate command and query handlers.</p> <h4 id="approvetrainingreschedule-command">ApproveTrainingReschedule command</h4> <p>Previously, the training reschedule was approved from the application service <code>TrainingService</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">- func (c TrainingService) ApproveTrainingReschedule(ctx context.Context, user auth.User, trainingUUID string) error { </span></span></span><span class="line"><span class="cl"><span class="gd">- return c.repo.ApproveTrainingReschedule(ctx, trainingUUID, func(training Training) (Training, error) { </span></span></span><span class="line"><span class="cl"><span class="gd">- if training.ProposedTime == nil { </span></span></span><span class="line"><span class="cl"><span class="gd">- return Training{}, errors.New(&#34;training has no proposed time&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- if training.MoveProposedBy == nil { </span></span></span><span class="line"><span class="cl"><span class="gd">- return Training{}, errors.New(&#34;training has no MoveProposedBy&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- if *training.MoveProposedBy == &#34;trainer&#34; &amp;&amp; training.UserUUID != user.UUID { </span></span></span><span class="line"><span class="cl"><span class="gd">- return Training{}, errors.Errorf(&#34;user &#39;%s&#39; cannot approve reschedule of user &#39;%s&#39;&#34;, user.UUID, training.UserUUID) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- if *training.MoveProposedBy == user.Role { </span></span></span><span class="line"><span class="cl"><span class="gd">- return Training{}, errors.New(&#34;reschedule cannot be accepted by requesting person&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- training.Time = *training.ProposedTime </span></span></span><span class="line"><span class="cl"><span class="gd">- training.ProposedTime = nil </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- return training, nil </span></span></span><span class="line"><span class="cl"><span class="gd">- }) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F8d9274811559399461aa9f6bf3829316b8ddfb63%23diff-ddf06fa26668dd91e829c7bfbd68feaeL127" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/commit/8d9274811559399461aa9f6bf3829316b8ddfb63#diff-ddf06fa26668dd91e829c7bfbd68feaeL127</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F8d9274811559399461aa9f6bf3829316b8ddfb63%23diff-ddf06fa26668dd91e829c7bfbd68feaeL127" target="_blank">Full source</a> </div> <p>There were some magic validations there. These are now done in the domain layer. I also found that we forgot to call the external <code>trainer</code> service to move the training. Oops. &#x1f609; Let&rsquo;s refactor it to the CQRS approach.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Because CQRS works best with applications following Domain-Driven Design, I also refactored existing models to DDD Lite during the CQRS refactoring. DDD Lite is described in more detail in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F">the previous article</a>. </p></div> </div> <p>We start implementing a <em>command</em> with the command structure definition. This structure provides all data needed to execute the command. If a command has only one field, you can skip the structure and just pass it as a parameter.</p> <p>It&rsquo;s a good idea to use domain-defined types in the command, like <code>training.User</code> in this case. We don&rsquo;t need to do any casting later, and we have type safety assured. <strong>It can save us a lot of issues with string parameters passed in wrong order.</strong></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">command</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">ApproveTrainingReschedule</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">TrainingUUID</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">User</span> <span class="nx">training</span><span class="p">.</span><span class="nx">User</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Fapprove_training_reschedule.go%23L10" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/command/approve_training_reschedule.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Fapprove_training_reschedule.go%23L10" target="_blank">Full source</a> </div> <p>The second part is a <em>command handler</em> that executes the command.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">command</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">ApproveTrainingRescheduleHandler</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">repo</span> <span class="nx">training</span><span class="p">.</span><span class="nx">Repository</span> </span></span><span class="line"><span class="cl"> <span class="nx">userService</span> <span class="nx">UserService</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainerService</span> <span class="nx">TrainerService</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">ApproveTrainingRescheduleHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">ApproveTrainingReschedule</span><span class="p">)</span> <span class="p">(</span><span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">logs</span><span class="p">.</span><span class="nf">LogCommandExecution</span><span class="p">(</span><span class="s">&#34;ApproveTrainingReschedule&#34;</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">.</span><span class="nx">repo</span><span class="p">.</span><span class="nf">UpdateTraining</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">TrainingUUID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">tr</span> <span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">originalTrainingTime</span> <span class="o">:=</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">Time</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">ApproveReschedule</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nf">Type</span><span class="p">());</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">trainerService</span><span class="p">.</span><span class="nf">MoveTraining</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">Time</span><span class="p">(),</span> <span class="nx">originalTrainingTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">tr</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Fapprove_training_reschedule.go%23L39" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/command/approve_training_reschedule.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Fapprove_training_reschedule.go%23L39" target="_blank">Full source</a> </div> <p>The flow is now much easier to understand. You can clearly see that we approve a reschedule of a persisted <code>*training.Training</code>, and if it succeeds, we call the external <code>trainer</code> service. Thanks to techniques described in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F">the DDD Lite article</a>, the command handler doesn&rsquo;t need to know when it can perform this operation. The domain layer handles all of that.</p> <p>This clear flow is even more visible in complex commands. Fortunately, the current implementation is straightforward. That&rsquo;s good. <strong>Our goal is to create simple software, not complicated software.</strong></p> <p>If CQRS is the standard way of building applications in your team, it also speeds up onboarding teammates unfamiliar with a service. You just need a list of available commands and queries and a quick look at how their execution works. No more jumping through random places in code.</p> <p>This is how it looks in one of my team&rsquo;s most complex services:</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="693" height="917" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fintroducing-cqrs%2Fcomplex-command_hufb9f2c486775b128479d3e2dbfb7f355_79352_693x917_resize_q80_h2_lanczos.webp" alt="Example commands list of a complex application" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fintroducing-cqrs%5C%2Fcomplex-command_hufb9f2c486775b128479d3e2dbfb7f355_79352_693x917_resize_q80_lanczos.jpg"" /> <p>You may ask: shouldn&rsquo;t it be split into multiple services? <strong>In practice, that would be a terrible idea.</strong> Many operations here need to be transactionally consistent. Splitting into separate services would involve several distributed transactions (<em>Sagas</em>). This would make the flow much more complex, harder to maintain, and harder to debug. It&rsquo;s not a good tradeoff.</p> <p>It&rsquo;s also worth mentioning that none of these operations are very complex. <strong>Complexity scales horizontally here.</strong> We will cover the important topic of splitting microservices in-depth soon. Did I mention that we messed it up in Wild Workouts on purpose? &#x1f609;</p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/basic-cqrs-in-go/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;cqrs;ddd;domain-driven design"> <input type="hidden" name="fields[blog_series]" value="Modern Business Software in Go"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <p>But let&rsquo;s go back to our command. It&rsquo;s time to use it in our HTTP port. It&rsquo;s available in <code>HttpServer</code> via the injected <code>Application</code> structure, which contains all of our command and query handlers.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">app</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/command&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/query&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Application</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Commands</span> <span class="nx">Commands</span> </span></span><span class="line"><span class="cl"> <span class="nx">Queries</span> <span class="nx">Queries</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Commands</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ApproveTrainingReschedule</span> <span class="nx">command</span><span class="p">.</span><span class="nx">ApproveTrainingRescheduleHandler</span> </span></span><span class="line"><span class="cl"> <span class="nx">CancelTraining</span> <span class="nx">command</span><span class="p">.</span><span class="nx">CancelTrainingHandler</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fapp.go%23L8" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/app.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fapp.go%23L8" target="_blank">Full source</a> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">HttpServer</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">app</span> <span class="nx">app</span><span class="p">.</span><span class="nx">Application</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">HttpServer</span><span class="p">)</span> <span class="nf">ApproveRescheduleTraining</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingUUID</span> <span class="o">:=</span> <span class="nx">chi</span><span class="p">.</span><span class="nf">URLParam</span><span class="p">(</span><span class="nx">r</span><span class="p">,</span> <span class="s">&#34;trainingUUID&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">newDomainUserFromAuthUser</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">RespondWithSlugError</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">app</span><span class="p">.</span><span class="nx">Commands</span><span class="p">.</span><span class="nx">ApproveTrainingReschedule</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">(),</span> <span class="nx">command</span><span class="p">.</span><span class="nx">ApproveTrainingReschedule</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">User</span><span class="p">:</span> <span class="nx">user</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">TrainingUUID</span><span class="p">:</span> <span class="nx">trainingUUID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">RespondWithSlugError</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fports%2Fhttp.go%23L160" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/ports/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fports%2Fhttp.go%23L160" target="_blank">Full source</a> </div> <p>The command handler can be called this way from any port: HTTP, gRPC, or CLI. It&rsquo;s also useful for executing migrations and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainer%2Ffixtures.go%23L62" target="_blank">loading fixtures</a> (we already do this in Wild Workouts).</p> <h4 id="requesttrainingreschedule-command">RequestTrainingReschedule command</h4> <p>Some command handlers can be very simple.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">RequestTrainingRescheduleHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="nx">RequestTrainingReschedule</span><span class="p">)</span> <span class="p">(</span><span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">logs</span><span class="p">.</span><span class="nf">LogCommandExecution</span><span class="p">(</span><span class="s">&#34;RequestTrainingReschedule&#34;</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">.</span><span class="nx">repo</span><span class="p">.</span><span class="nf">UpdateTraining</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">TrainingUUID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">tr</span> <span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">tr</span><span class="p">.</span><span class="nf">UpdateNotes</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">NewNotes</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">tr</span><span class="p">.</span><span class="nf">ProposeReschedule</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">NewTime</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">User</span><span class="p">.</span><span class="nf">Type</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">tr</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Frequest_training_reschedule.go%23L32" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/command/request_training_reschedule.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fcommand%2Frequest_training_reschedule.go%23L32" target="_blank">Full source</a> </div> <p>It may be tempting to skip this layer for such simple cases to save some boilerplate. That&rsquo;s true, but remember that <strong>writing code is always much cheaper than maintenance. Adding this simple type takes 3 minutes. People who read and extend this code later will appreciate that effort.</strong></p> <h4 id="availablehourshandler-query">AvailableHoursHandler query</h4> <p>Queries in the application layer are usually boring. In the most common case, we need to write a <em>read model interface</em> (<code>AvailableHoursReadModel</code>) that defines how we query the data.</p> <p>Commands and queries are also a great place for <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FCross-cutting_concern" target="_blank">cross-cutting concerns</a>, like logging and instrumentation. By putting them here, we ensure that performance is measured consistently whether called from the HTTP or gRPC port.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">query</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">AvailableHoursHandler</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">readModel</span> <span class="nx">AvailableHoursReadModel</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">AvailableHoursReadModel</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">AvailableHours</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">from</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> <span class="nx">to</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="p">([]</span><span class="nx">Date</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">AvailableHours</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">From</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"> <span class="nx">To</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">AvailableHoursHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">query</span> <span class="nx">AvailableHours</span><span class="p">)</span> <span class="p">(</span><span class="nx">d</span> <span class="p">[]</span><span class="nx">Date</span><span class="p">,</span> <span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">start</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">logrus</span><span class="p">.</span> </span></span><span class="line"><span class="cl"> <span class="nf">WithError</span><span class="p">(</span><span class="nx">err</span><span class="p">).</span> </span></span><span class="line"><span class="cl"> <span class="nf">WithField</span><span class="p">(</span><span class="s">&#34;duration&#34;</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Since</span><span class="p">(</span><span class="nx">start</span><span class="p">)).</span> </span></span><span class="line"><span class="cl"> <span class="nf">Debug</span><span class="p">(</span><span class="s">&#34;AvailableHoursHandler executed&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">query</span><span class="p">.</span><span class="nx">From</span><span class="p">.</span><span class="nf">After</span><span class="p">(</span><span class="nx">query</span><span class="p">.</span><span class="nx">To</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">NewIncorrectInputError</span><span class="p">(</span><span class="s">&#34;date-from-after-date-to&#34;</span><span class="p">,</span> <span class="s">&#34;Date from after date to&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">.</span><span class="nx">readModel</span><span class="p">.</span><span class="nf">AvailableHours</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">query</span><span class="p">.</span><span class="nx">From</span><span class="p">,</span> <span class="nx">query</span><span class="p">.</span><span class="nx">To</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainer%2Fapp%2Fquery%2Favailable_hours.go%23L11" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/query/available_hours.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainer%2Fapp%2Fquery%2Favailable_hours.go%23L11" target="_blank">Full source</a> </div> <p>We also need to define data types returned by the query. In our case, it&rsquo;s <code>query.Date</code>.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> To understand why we don&rsquo;t use structures generated from OpenAPI, you should check our articles on <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthings-to-know-about-dry%2F">DRY</a> and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F">Clean Architecture</a>. </p></div> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">query</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;time&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Date</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Date</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"> <span class="nx">HasFreeHours</span> <span class="kt">bool</span> </span></span><span class="line"><span class="cl"> <span class="nx">Hours</span> <span class="p">[]</span><span class="nx">Hour</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Hour</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Available</span> <span class="kt">bool</span> </span></span><span class="line"><span class="cl"> <span class="nx">HasTrainingScheduled</span> <span class="kt">bool</span> </span></span><span class="line"><span class="cl"> <span class="nx">Hour</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainer%2Fapp%2Fquery%2Ftypes.go" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/query/types.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainer%2Fapp%2Fquery%2Ftypes.go" target="_blank">Full source</a> </div> <p>Our query model is more complex than the domain <code>hour.Hour</code> type. This is a common scenario. Often, it&rsquo;s driven by the website&rsquo;s UI, and it&rsquo;s more efficient to generate optimal responses on the backend side.</p> <p>As the application grows, differences between domain and query models may increase. <strong>Thanks to this separation and decoupling, we can make changes to each independently.</strong> This is critical for maintaining fast development in the long term.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">hour</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Hour</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">hour</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">availability</span> <span class="nx">Availability</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Fhour.go%23L11" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour/hour.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Fhour.go%23L11" target="_blank">Full source</a> </div> <p>But where does <code>AvailableHoursReadModel</code> get the data? For the application layer, this is fully transparent and not relevant. This allows us to add performance optimizations in the future while touching just one part of the application.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If you are not familiar with the concept of _ports and adapters_, I highly recommend reading our <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture%2F">article about Clean Architecture in Go</a>. </p></div> </div> <p>In practice, the current implementation gets data <strong>from our write models database</strong>. You can find the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fapp%2Fquery%2Fall_trainings.go%23L19" target="_blank"><code>AllTrainings</code></a> read model <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L170" target="_blank">implementation</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository_test.go%23L152" target="_blank">tests</a> for <code>DatesFirestoreRepository</code> in the adapters layer.</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1779" height="679" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fintroducing-cqrs%2Fcqrs-architecture_hu79ed5d69a816456afea28c33a0d86dac_112338_1779x679_resize_q80_h2_lanczos.webp" alt="CQRS architecture" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fintroducing-cqrs%5C%2Fcqrs-architecture_hu79ed5d69a816456afea28c33a0d86dac_112338_1779x679_resize_q80_lanczos.jpg"" /> <div class="code-ref"> Data for our queries is currently queried from the same database where write models are stored. </div></p> <p>If you&rsquo;ve read about CQRS before, you may have seen recommendations to use a separate database built from events for queries. This can be a good idea, but only in very specific cases. I&rsquo;ll describe this in the <em>Future optimizations</em> section. In our case, getting data from the write models database is sufficient.</p> <h4 id="houravailabilityhandler-query">HourAvailabilityHandler query</h4> <p>We don&rsquo;t need to add a <em>read model</em> interface for every query. It&rsquo;s also fine to use the domain repository and pick the data we need.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;context&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;time&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">HourAvailabilityHandler</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourRepo</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">Repository</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">HourAvailabilityHandler</span><span class="p">)</span> <span class="nf">Handle</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">time</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">hour</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">hourRepo</span><span class="p">.</span><span class="nf">GetHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">time</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">false</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">hour</span><span class="p">.</span><span class="nf">IsAvailable</span><span class="p">(),</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainer%2Fapp%2Fquery%2Fhour_availability.go%23L22" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/query/hour_availability.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainer%2Fapp%2Fquery%2Fhour_availability.go%23L22" target="_blank">Full source</a> </div> <h3 id="naming">Naming</h3> <p>Naming is one of the most challenging and essential parts of software development. In <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F">Introduction to DDD Lite article</a>, I described a rule that says you should stick to language that is as close as possible to how non-technical people (often called &ldquo;business&rdquo;) talk. This also applies to command and query names.</p> <p>You should avoid names like &ldquo;Create training&rdquo; or &ldquo;Delete training&rdquo;. <strong>This is not how business and users understand your domain. You should instead use &ldquo;Schedule training&rdquo; and &ldquo;Cancel training&rdquo;.</strong></p> <p><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Ftree%2F8d9274811559399461aa9f6bf3829316b8ddfb63%2Finternal%2Ftrainings%2Fapp" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="550" height="443" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fintroducing-cqrs%2Ftrainer-cqrs_hu18eecb2eb5a873ce0bafcd12ba51273f_33793_550x443_resize_q80_h2_lanczos_3.webp" alt="Commands" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fintroducing-cqrs%5C%2Ftrainer-cqrs_hu18eecb2eb5a873ce0bafcd12ba51273f_33793_550x443_resize_lanczos_3.png"" /> <div class="code-ref"> All commands and queries of the trainings service </div></a></p> <p>We will cover this topic more deeply in an article about Ubiquitous Language. Until then, talk to your business people and listen to how they describe operations. Think twice about whether any of your command names really need to start with &ldquo;Create/Delete/Update&rdquo;.</p> <h3 id="future-optimizations">Future optimizations</h3> <p>Basic CQRS gives advantages like <strong>better code organization, decoupling, and simpler models</strong>. There is also one even more important advantage: <strong>the ability to extend CQRS with more powerful and complex patterns</strong>.</p> <h4 id="async-commands">Async commands</h4> <p>Some commands are slow by nature. They may be making external calls or doing heavy computation. In these cases, we can introduce an <em>Asynchronous Command Bus</em>, which executes the command in the background.</p> <p>Using asynchronous commands has additional infrastructure requirements, like having a queue or pub/sub. Fortunately, the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill" target="_blank">Watermill</a> library can help you handle this in Go. You can find more details in <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fcqrs%2F%3Futm_source%3Dintroducing-cqrs-art" target="_blank">the Watermill CQRS documentation</a>. (By the way, we are the authors of Watermill as well. &#x1f609; Feel free to contact us if something&rsquo;s not clear there!)</p> <h4 id="a-separate-database-for-queries">A separate database for queries</h4> <p>Our current implementation uses the same database for reads (queries) and writes (commands). If we needed to provide more complex queries or have really fast reads, we could use the <em>polyglot persistence</em> technique. The idea is to duplicate queried data in a more optimal format in another database. For example, we could use Elasticsearch to index data that can be searched and filtered more easily.</p> <p>In this case, data synchronization can be done via <em>events</em>. One of the most important implications of this approach is eventual consistency. You should ask yourself if it&rsquo;s an acceptable tradeoff in your system. If you&rsquo;re not sure, you can start without polyglot persistence and migrate later. It&rsquo;s good to defer key decisions like this one.</p> <p>An example implementation is described in <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fcqrs%2F%3Futm_source%3Dcqrs-art%23building-a-read-model-with-the-event-handler" target="_blank">the Watermill CQRS documentation</a> as well. Maybe with time, we will introduce it also in Wild Workouts, who knows?</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1281" height="825" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fintroducing-cqrs%2Fcqrs-polyglot-persistence_hua5ca2f918768592753bbfb365749cec2_108076_1281x825_resize_q80_h2_lanczos.webp" alt="Commands" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fintroducing-cqrs%5C%2Fcqrs-polyglot-persistence_hua5ca2f918768592753bbfb365749cec2_108076_1281x825_resize_q80_lanczos.jpg"" /> <div class="code-ref"> CQRS with polyglot persistence </div></p> <h4 id="event-sourcing">Event-Sourcing</h4> <p>If you work in a domain with strict audit requirements, you should definitely check out the <em>event sourcing</em> technique. For example, I&rsquo;m currently working in the financial domain, and <em>event sourcing</em> is our default persistence choice. It provides out-of-the-box auditing and helps with reverting the effects of bugs.</p> <p>CQRS is often described together with <em>event sourcing</em>. The reason is that by design, event-sourced systems don&rsquo;t store the model in a format ready for reads (queries), but just a list of events used by writes (commands). In other words, it&rsquo;s harder to provide API responses.</p> <p>Thanks to the separation of <em>command</em> and <em>query</em> models, this isn&rsquo;t a big problem. Our read models for queries live independently by design.</p> <p>There are many more advantages of event sourcing that are visible in financial systems. But let&rsquo;s leave that for another article. &#x1f609; Until then, you can check out the ebook by Greg Young: <a href="proxy.php?url=https%3A%2F%2Fleanpub.com%2Fesversioning" target="_blank">Versioning in an Event Sourced System</a>. The same Greg Young who described CQRS.</p> <h3 id="when-to-not-use-cqrs">When to not use CQRS?</h3> <p>CQRS is not a silver bullet that fits everywhere. A good example is authentication. You provide a login and password, and in return, you get confirmation of success and maybe a token.</p> <p>If your application is a simple CRUD that receives and returns the same data, it&rsquo;s also not the best fit for CQRS. That&rsquo;s why the <code>users</code> microservice in Wild Workouts doesn&rsquo;t use Clean Architecture and CQRS. In simple, data-oriented services, these patterns usually don&rsquo;t make sense. On the other hand, you should keep an eye on such services. If you notice the logic growing and development becoming painful, maybe it&rsquo;s time for some refactoring?</p> <h4 id="returning-created-entity-via-api-with-cqrs">Returning created entity via API with CQRS</h4> <p>I know some people struggle with using CQRS for REST APIs that return the created entity as the response to a POST request. Isn&rsquo;t that against CQRS? Not really! You can solve it in two ways:</p> <ol> <li>Call the command in the HTTP port and, after it succeeds, call the query to get the data to return.</li> <li>Instead of returning the created entity, return a <code>204</code> HTTP code with the <code>content-location</code> header set to the created resource URL.</li> </ol> <p>The second approach is better in my opinion because it doesn&rsquo;t require always querying for the created entity (even if the client doesn&rsquo;t need the data). With the second approach, the client only follows the link if needed. That call can also be cached.</p> <p>The only question is: how do you get the created entity&rsquo;s ID? A common practice is to provide the UUID of the entity to be created in the command.</p> <p>This approach has the advantage of still working as expected if the command handler is asynchronous. If you don&rsquo;t want to work with UUIDs, as a last resort you can return the ID from the handler. It won&rsquo;t be the end of the world. &#x1f609;</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">cmd</span> <span class="o">:=</span> <span class="nx">command</span><span class="p">.</span><span class="nx">ScheduleTraining</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">TrainingUUID</span><span class="p">:</span> <span class="nx">uuid</span><span class="p">.</span><span class="nf">New</span><span class="p">().</span><span class="nf">String</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserUUID</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">UUID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserName</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">DisplayName</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">TrainingTime</span><span class="p">:</span> <span class="nx">postTraining</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Notes</span><span class="p">:</span> <span class="nx">postTraining</span><span class="p">.</span><span class="nx">Notes</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="nx">err</span> <span class="p">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">app</span><span class="p">.</span><span class="nx">Commands</span><span class="p">.</span><span class="nx">ScheduleTraining</span><span class="p">.</span><span class="nf">Handle</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">(),</span> <span class="nx">cmd</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">RespondWithSlugError</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;content-location&#34;</span><span class="p">,</span> <span class="s">&#34;/trainings/&#34;</span> <span class="o">+</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">TrainingUUID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusNoContent</span><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fports%2Fhttp.go%23L70" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/ports/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F22c0a25b67c4669d612a2fa4a434ffae8e35e65a%2Finternal%2Ftrainings%2Fports%2Fhttp.go%23L70" target="_blank">Full source</a> </div> <h3 id="you-can-now-put-cqrs-in-your-resume">You can now put CQRS in your resume!</h3> <p>We did it: we have a basic CQRS implementation in Wild Workouts. You should also have an idea of how to extend the application in the future.</p> <p>While preparing the code for this article, I also refactored the <code>trainer</code> service towards DDD. I will cover this in the next article. The entire diff of that refactoring is already available on our <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F8d9274811559399461aa9f6bf3829316b8ddfb63" target="_blank">GitHub repository</a>.</p> <p>Having every command handler as a separate type also helps with testing because it&rsquo;s easier to build dependencies for them. This part is covered by Miłosz in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-test-architecture%2F">Microservices Test Architecture</a>.</p> Are you using CQRS with any extensions? Do you have any project where you don&rsquo;t know how to apply these patterns? Feel free to share and ask in the comments!How to implement Clean Architecture in Go (Golang)https://threedots.tech/post/introducing-clean-architecture/Tue, 01 Sep 2020 00:00:00 +0200https://threedots.tech/post/introducing-clean-architecture/<p>The authors of <a href="proxy.php?url=https%3A%2F%2Fitrevolution.com%2Fbook%2Faccelerate%2F" target="_blank">Accelerate</a> dedicate an entire chapter to software architecture and how it affects development performance. One recurring theme is designing applications to be &ldquo;loosely coupled&rdquo;.</p> <blockquote> <p>The goal is for your architecture to support the ability of teams to get their work done—from design through to deployment—without requiring high-bandwidth communication between teams.</p> <footer> <strong></strong> <cite> <a href="proxy.php?url=https%3A%2F%2Fitrevolution.com%2Fbook%2Faccelerate%2F" title="https://itrevolution.com/book/accelerate/" target="_blank">Accelerate</a> </cite> </footer> </blockquote> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If you haven&rsquo;t read <a href="proxy.php?url=https%3A%2F%2Fitrevolution.com%2Fbook%2Faccelerate%2F" target="_blank">Accelerate</a> yet, I highly recommend it. The book presents scientific evidence on methods leading to high performance in development teams. The approach I describe is not only based on our experiences but also mentioned throughout the book. </p></div> </div> <p>While coupling seems mostly related to microservices across multiple teams, we find loosely coupled architecture just as useful for work within a single team. Maintaining architecture standards enables parallel work and helps onboard new team members.</p> <p>You&rsquo;ve probably heard of the <em>&ldquo;low coupling, high cohesion&rdquo;</em> concept, but it&rsquo;s rarely obvious how to achieve it. The good news: it&rsquo;s the main benefit of Clean Architecture.</p> <p>The pattern is not only an excellent way to start a project but also helpful when refactoring a poorly designed application. I focus on the latter in this post. I show the refactoring of a real application, so it should be clear how to apply similar changes in your projects.</p> <p>We&rsquo;ve also noticed other benefits of this approach:</p> <ul> <li>a standard structure, so it&rsquo;s easy to find your way in the project,</li> <li>faster development in the long term,</li> <li>mocking dependencies becomes trivial in unit tests,</li> <li>easy switching from prototypes to proper solutions (e.g., changing in-memory storage to an SQL database).</li> </ul> <h2 id="clean-architecture">Clean Architecture</h2> <p>I had a hard time coming up with this post&rsquo;s title because the pattern comes in many flavors. There&rsquo;s <a href="proxy.php?url=https%3A%2F%2Fblog.cleancoder.com%2Funcle-bob%2F2012%2F08%2F13%2Fthe-clean-architecture.html" target="_blank">Clean Architecture</a>, <a href="proxy.php?url=https%3A%2F%2Fjeffreypalermo.com%2F2008%2F07%2Fthe-onion-architecture-part-1%2F" target="_blank">Onion Architecture</a>, <a href="proxy.php?url=https%3A%2F%2Fweb.archive.org%2Fweb%2F20180822100852%2Fhttp%3A%2F%2Falistair.cockburn.us%2FHexagonal%26amp%3B%2343%3Barchitecture" target="_blank">Hexagonal Architecture</a>, and Ports and Adapters.</p> <p>Over the past few years, we&rsquo;ve tried to use these patterns in Go in an idiomatic way. This involved trying out approaches, failing, changing them, and trying again.</p> <p>We arrived at a mix of the ideas above, sometimes not strictly following the original patterns, but we found it works well in Go. I&rsquo;ll demonstrate our approach by refactoring <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">Wild Workouts</a>, our example application.</p> <p>I want to point out that the idea is not new at all. A big part of it is <strong>abstracting away implementation details</strong>, a standard practice in technology, especially software.</p> <p>Another name for it is <strong>separation of concerns</strong>. The concept is so old that it exists on several levels: structures, namespaces, modules, packages, and even (micro)services. All are meant to keep related things within a boundary. Sometimes, it feels like common sense:</p> <ul> <li>When you optimize an SQL query, you don&rsquo;t want to risk changing the display format.</li> <li>When you change an HTTP response format, you don&rsquo;t want to alter the database schema.</li> </ul> <p><strong>Our approach to Clean Architecture combines two ideas:</strong> separating Ports and Adapters, and limiting how code structures refer to each other.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="this-is-not-just-another-article-with-random-code-snippets">This is not just another article with random code snippets.</h4> <p>This post is part of a bigger series where we show how to build <strong>Go applications that are easy to develop, maintain, and fun to work with in the long term.</strong> We are doing it by sharing proven techniques based on many experiments we did with teams we lead and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%3Futm_source%3Dabout-wild-workouts%23thats-great-but-do-you-have-any-evidence-it-works">scientific research</a>.</p> <p>You can learn these patterns by building with us a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%3Futm_source%3Dabout-wild-workouts%23what-wild-workouts-can-do">fully functional</a> example Go web application &ndash; <strong>Wild Workouts</strong>.</p> <p>We did one thing differently &ndash; <strong>we included some subtle issues to the initial Wild Workouts implementation</strong>. Have we lost our minds to do that? Not yet. 😉 These issues are common for many Go projects. <strong>In the long term, these small issues become critical and stop adding new features.</strong></p> <p><strong>It&rsquo;s one of the essential skills of a senior or lead developer; you always need to keep long-term implications in mind.</strong></p> <p>We will fix them by <strong>refactoring</strong> Wild Workouts. In that way, you will quickly understand the techniques we share.</p> <p>Do you know that feeling after reading an article about some technique and trying implement it only to be blocked by some issues skipped in the guide? Cutting these details makes articles shorter and increases page views, but this is not our goal. Our goal is to create content that provides enough know-how to apply presented techniques. If you did not read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">previous articles from the series</a> yet, we highly recommend doing that.</p> <p>We believe that in some areas, there are no shortcuts. If you want to build complex applications in a fast and efficient way, you need to spend some time learning that. If it was simple, we wouldn&rsquo;t have large amounts of scary legacy code.</p> <p>Here&rsquo;s <strong><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">the full list of 14 articles</a></strong> released so far.</p> <p><strong>The full source code</strong> of Wild Workouts is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%3Futm_source%3Dabout-wild-workouts" target="_blank">GitHub</a>. Don&rsquo;t forget to leave a star for our project! ⭐</p> </p></div> </div> <h3 id="before-we-start">Before We Start</h3> <p>Before introducing Clean Architecture in Wild Workouts, I refactored the project a bit. The changes come from patterns we shared in previous posts.</p> <p>The first one is using <strong>separate models for database entities and HTTP responses</strong>. I&rsquo;ve introduced changes in the <code>users</code> service in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthings-to-know-about-dry%2F">my post on the DRY principle</a>. I applied the same pattern now in <code>trainer</code> and <code>trainings</code> as well. See the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2Fe48fbc3d2324da887415bd91918d5e46ebfd5baf" target="_blank">full commit on GitHub</a>.</p> <p>The second change follows <strong>the Repository Pattern</strong> that Robert introduced in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F">the previous article</a>. <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2Ff89da08cc3c1c7ed8e7767415b04e87d3a5ef9cf" target="_blank">My refactoring</a> moved database-related code in <code>trainings</code> to a separate structure.</p> <h3 id="separating-ports-and-adapters">Separating Ports and Adapters</h3> <p>Ports and Adapters can be called different names, like interfaces and infrastructure. The idea is to explicitly separate these two categories from the rest of your application code.</p> <p>We take the code in these groups and place it in different packages. We refer to them as &ldquo;layers&rdquo;. <strong>The layers we usually use are adapters, ports, application, and domain.</strong></p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h3 id="read-this-if-you-know-hexagonal-architecture">Read this if you know Hexagonal Architecture</h3> <p>You might be confused about the ports and adapters. We accidentally picked the same names for different things (even if they fit well).</p> <ul> <li>Our <strong>ports</strong> are Hexagonal Architecture&rsquo;s <strong>Primary Adapters</strong>.</li> <li>Our <strong>adapters</strong> are Hexagonal Architecture&rsquo;s <strong>Secondary Adapters</strong>.</li> </ul> <p>The idea stays the same. We find the original primary/secondary naming hard to grasp, so feel free to use what works for you. You could use gateways, entry points, interfaces, infrastructure, and so on. Just make sure it&rsquo;s consistent and your team knows what goes where.</p> <p>What about the original ports? Thanks to Go&rsquo;s implicit interfaces, we see no value in keeping a dedicated layer for them. We keep interfaces close to where they&rsquo;re used (see below).</p> </p></div> </div> <ul> <li><strong>An adapter is how your application talks to the external world.</strong> You have to <strong>adapt</strong> your internal structures to what the external API expects. Think SQL queries, HTTP or gRPC clients, file readers and writers, Pub/Sub message publishers.</li> <li><strong>A port is an input to your application</strong>, and the only way the external world can reach it. It could be an HTTP or gRPC server, a CLI command, or a Pub/Sub message subscriber.</li> <li><strong>The application logic</strong> is a thin layer that &ldquo;glues together&rdquo; other layers. It&rsquo;s also known as &ldquo;use cases&rdquo;. If you read this code and can&rsquo;t tell what database it uses or what URL it calls, it&rsquo;s a good sign. Sometimes it&rsquo;s very short, and that&rsquo;s fine. Think about it as an orchestrator.</li> <li>If you also follow <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F">Domain-Driven Design</a>, you can introduce <strong>a domain layer that holds just the business logic</strong>.</li> </ul> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>If the idea of separating layers is still unclear, look at your smartphone. It uses similar concepts.</p> <p>You can control your smartphone using physical buttons, the touchscreen, or a voice assistant. Whether you press the &ldquo;volume up&rdquo; button, swipe the volume bar up, or say &ldquo;Siri, volume up,&rdquo; the effect is the same. There are several entry points (<strong>ports</strong>) to the &ldquo;change volume&rdquo; <strong>logic</strong>.</p> <p>When you play music, you hear it from the speaker. If you plug in headphones, the audio automatically switches to them. Your music app doesn&rsquo;t care. It&rsquo;s not talking to the hardware directly but using one of the <strong>adapters</strong> the OS provides.</p> <p>Can you imagine creating a mobile app that has to know which headphones model is connected to the smartphone? Including SQL queries directly inside application logic is similar: it exposes implementation details.</p> </p></div> </div> <img title="" loading="lazy" decoding="async" class="img img-center" width="2000" height="2000" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fintroducing-clean-architecture%2Fclean-arch_hua2fbaee330357286a5d6d11d974a364a_212674_2000x2000_resize_q80_h2_lanczos.webp" alt="Clean Architecture layers" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fintroducing-clean-architecture%5C%2Fclean-arch_hua2fbaee330357286a5d6d11d974a364a_212674_2000x2000_resize_q80_lanczos.jpeg"" /> <p>Let&rsquo;s start refactoring by introducing the layers in the <code>trainings</code> service. The project looks like this so far:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">trainings/ </span></span><span class="line"><span class="cl">├── firestore.go </span></span><span class="line"><span class="cl">├── go.mod </span></span><span class="line"><span class="cl">├── go.sum </span></span><span class="line"><span class="cl">├── http.go </span></span><span class="line"><span class="cl">├── main.go </span></span><span class="line"><span class="cl">├── openapi_api.gen.go </span></span><span class="line"><span class="cl">└── openapi_types.gen.go </span></span></code></pre></div><p>This part of refactoring is simple:</p> <ol> <li>Create <code>ports</code>, <code>adapters</code>, and <code>app</code> directories.</li> <li>Move each file to the proper directory.</li> </ol> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">trainings/ </span></span><span class="line"><span class="cl">├── adapters </span></span><span class="line"><span class="cl">│   └── firestore.go </span></span><span class="line"><span class="cl">├── app </span></span><span class="line"><span class="cl">├── go.mod </span></span><span class="line"><span class="cl">├── go.sum </span></span><span class="line"><span class="cl">├── main.go </span></span><span class="line"><span class="cl">└── ports </span></span><span class="line"><span class="cl"> ├── http.go </span></span><span class="line"><span class="cl"> ├── openapi_api.gen.go </span></span><span class="line"><span class="cl"> └── openapi_types.gen.go </span></span></code></pre></div><p>I introduced similar packages in the <code>trainer</code> service. We won&rsquo;t make any changes to the <code>users</code> service this time. There&rsquo;s no application logic there, and overall it&rsquo;s tiny. As with every technique, apply Clean Architecture where it makes sense.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If the project grows in size, you may find it helpful to add another level of subdirectories. For example, <code>adapters/hour/mysql_repository.go</code> or <code>ports/http/hour_handler.go</code>. </p></div> </div> <p>You probably noticed there are no files in the <code>app</code> package. We now have to extract the application logic from HTTP handlers.</p> <h3 id="the-application-layer">The Application Layer</h3> <p>Let&rsquo;s see where our application logic lives. Take a look at the <code>CancelTraining</code> method in the <code>trainings</code> service.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">HttpServer</span><span class="p">)</span> <span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingUUID</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">().</span><span class="nf">Value</span><span class="p">(</span><span class="s">&#34;trainingUUID&#34;</span><span class="p">).(</span><span class="kt">string</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">auth</span><span class="p">.</span><span class="nf">UserFromCtx</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">Unauthorised</span><span class="p">(</span><span class="s">&#34;no-user-found&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">(),</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">trainingUUID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">InternalError</span><span class="p">(</span><span class="s">&#34;cannot-update-training&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Ff89da08cc3c1c7ed8e7767415b04e87d3a5ef9cf%2Finternal%2Ftrainings%2Fhttp.go%23L95" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Ff89da08cc3c1c7ed8e7767415b04e87d3a5ef9cf%2Finternal%2Ftrainings%2Fhttp.go%23L95" target="_blank">Full source</a> </div> <p>This method is the entry point to the application. There&rsquo;s not much logic there, so let&rsquo;s go deeper into the <code>db.CancelTraining</code> method.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="2028" height="1971" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fintroducing-clean-architecture%2Flayers_hu4310ab217e383291f47108a5c7c76e0c_459425_2028x1971_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fintroducing-clean-architecture%5C%2Flayers_hu4310ab217e383291f47108a5c7c76e0c_459425_2028x1971_resize_q80_lanczos.jpg"" /> <p>Inside the Firestore transaction, there&rsquo;s a lot of code that doesn&rsquo;t belong in database handling.</p> <p>What&rsquo;s worse, the application logic inside this method uses the database model (<code>TrainingModel</code>) for decision-making:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">training</span><span class="p">.</span><span class="nf">canBeCancelled</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Ff89da08cc3c1c7ed8e7767415b04e87d3a5ef9cf%2Finternal%2Ftrainings%2Ffirestore.go%23L133" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/firestore.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Ff89da08cc3c1c7ed8e7767415b04e87d3a5ef9cf%2Finternal%2Ftrainings%2Ffirestore.go%23L133" target="_blank">Full source</a> </div> <p>Mixing business rules (like when a training can be canceled) with the database model slows down development because the code becomes hard to understand and reason about. It&rsquo;s also difficult to test such logic.</p> <p>To fix this, we add an intermediate <code>Training</code> type in the <code>app</code> layer:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Training</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">UUID</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserUUID</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">User</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">Time</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"> <span class="nx">Notes</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">ProposedTime</span> <span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"> <span class="nx">MoveProposedBy</span> <span class="o">*</span><span class="kt">string</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="nx">Training</span><span class="p">)</span> <span class="nf">CanBeCancelled</span><span class="p">()</span> <span class="kt">bool</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">t</span><span class="p">.</span><span class="nx">Time</span><span class="p">.</span><span class="nf">Sub</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">())</span> <span class="p">&gt;</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Hour</span><span class="o">*</span><span class="mi">24</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="nx">Training</span><span class="p">)</span> <span class="nf">MoveRequiresAccept</span><span class="p">()</span> <span class="kt">bool</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">!</span><span class="nx">t</span><span class="p">.</span><span class="nf">CanBeCancelled</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining.go%23L5" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/training.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining.go%23L5" target="_blank">Full source</a> </div> <p>It should now be clear on first read when a training can be canceled. We can&rsquo;t tell how the training is stored in the database or what JSON format the HTTP API uses. That&rsquo;s a good sign.</p> <p>We can now update the database layer methods to return this generic application type instead of the database-specific structure (<code>TrainingModel</code>). The mapping is trivial because the structs have the same fields (but from now on, they can evolve independently from each other).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">t</span> <span class="o">:=</span> <span class="nx">TrainingModel</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">doc</span><span class="p">.</span><span class="nf">DataTo</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">t</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">trainings</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">trainings</span><span class="p">,</span> <span class="nx">app</span><span class="p">.</span><span class="nf">Training</span><span class="p">(</span><span class="nx">t</span><span class="p">))</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L77" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/adapters/trainings_firestore_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fadapters%2Ftrainings_firestore_repository.go%23L77" target="_blank">Full source</a> </div> <h3 id="the-application-service">The Application Service</h3> <p>We then create a <code>TrainingsService</code> struct in the <code>app</code> package that will serve as the entry point to the trainings application logic.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">TrainingService</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="nx">TrainingService</span><span class="p">)</span> <span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> <span class="nx">trainingUUID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>So how do we call the database now? Let&rsquo;s try to replicate what was used so far in the HTTP handler.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">TrainingService</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span> <span class="nx">adapters</span><span class="p">.</span><span class="nx">DB</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="nx">TrainingService</span><span class="p">)</span> <span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> <span class="nx">trainingUUID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">c</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">trainingUUID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This code won&rsquo;t compile, though.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nx">cycle</span> <span class="nx">not</span> <span class="nx">allowed</span> </span></span><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">github</span><span class="p">.</span><span class="nx">com</span><span class="o">/</span><span class="nx">ThreeDotsLabs</span><span class="o">/</span><span class="nx">wild</span><span class="o">-</span><span class="nx">workouts</span><span class="o">-</span><span class="k">go</span><span class="o">-</span><span class="nx">ddd</span><span class="o">-</span><span class="nx">example</span><span class="o">/</span><span class="nx">internal</span><span class="o">/</span><span class="nx">trainings</span> </span></span><span class="line"><span class="cl"> <span class="nx">imports</span> <span class="nx">github</span><span class="p">.</span><span class="nx">com</span><span class="o">/</span><span class="nx">ThreeDotsLabs</span><span class="o">/</span><span class="nx">wild</span><span class="o">-</span><span class="nx">workouts</span><span class="o">-</span><span class="k">go</span><span class="o">-</span><span class="nx">ddd</span><span class="o">-</span><span class="nx">example</span><span class="o">/</span><span class="nx">internal</span><span class="o">/</span><span class="nx">trainings</span><span class="o">/</span><span class="nx">adapters</span> </span></span><span class="line"><span class="cl"> <span class="nx">imports</span> <span class="nx">github</span><span class="p">.</span><span class="nx">com</span><span class="o">/</span><span class="nx">ThreeDotsLabs</span><span class="o">/</span><span class="nx">wild</span><span class="o">-</span><span class="nx">workouts</span><span class="o">-</span><span class="k">go</span><span class="o">-</span><span class="nx">ddd</span><span class="o">-</span><span class="nx">example</span><span class="o">/</span><span class="nx">internal</span><span class="o">/</span><span class="nx">trainings</span><span class="o">/</span><span class="nx">app</span> </span></span><span class="line"><span class="cl"> <span class="nx">imports</span> <span class="nx">github</span><span class="p">.</span><span class="nx">com</span><span class="o">/</span><span class="nx">ThreeDotsLabs</span><span class="o">/</span><span class="nx">wild</span><span class="o">-</span><span class="nx">workouts</span><span class="o">-</span><span class="k">go</span><span class="o">-</span><span class="nx">ddd</span><span class="o">-</span><span class="nx">example</span><span class="o">/</span><span class="nx">internal</span><span class="o">/</span><span class="nx">trainings</span><span class="o">/</span><span class="nx">adapters</span> </span></span></code></pre></div><p>We need to <strong>decide how the layers should refer to each other.</strong></p> <h3 id="the-dependency-inversion-principle">The Dependency Inversion Principle</h3> <p>A clear separation between ports, adapters, and application logic is useful by itself. Clean Architecture takes it further with Dependency Inversion.</p> <p>The rule states that <strong>outer layers (implementation details) can refer to inner layers (abstractions), but not the other way around</strong>. The inner layers should instead depend on interfaces.</p> <ul> <li>The <strong>Domain</strong> knows nothing about other layers whatsoever. It contains pure business logic.</li> <li>The <strong>Application</strong> can import domain but knows nothing about outer layers. <strong>It has no idea whether it&rsquo;s being called by an HTTP request, a Pub/Sub handler, or a CLI command</strong>.</li> <li><strong>Ports</strong> can import inner layers. Ports are the entry points to the application, so they often execute application services or commands. However, they can&rsquo;t directly access <strong>Adapters</strong>.</li> <li><strong>Adapters</strong> can import inner layers. Usually, they will operate on types found in <strong>Application</strong> and <strong>Domain</strong>, for example, retrieving them from the database.</li> </ul> <img title="" loading="lazy" decoding="async" class="img img-center" width="2000" height="2000" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fintroducing-clean-architecture%2Fclean-arch-2_hua2fbaee330357286a5d6d11d974a364a_220043_2000x2000_resize_q80_h2_lanczos.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fintroducing-clean-architecture%5C%2Fclean-arch-2_hua2fbaee330357286a5d6d11d974a364a_220043_2000x2000_resize_q80_lanczos.jpg"" /> <p>Again, this is not a new idea. <strong>The Dependency Inversion Principle is the &ldquo;D&rdquo; in <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FSOLID" target="_blank">SOLID</a></strong>. Do you think it applies only to OOP? It turns out <strong><a href="proxy.php?url=https%3A%2F%2Fdave.cheney.net%2F2016%2F08%2F20%2Fsolid-go-design" target="_blank">Go interfaces are a perfect match</a></strong>.</p> <p>The principle addresses how packages should refer to each other. The best approach is rarely obvious, especially in Go, where import cycles are forbidden. Perhaps that&rsquo;s why some developers claim it&rsquo;s best to avoid &ldquo;nesting&rdquo; and keep all code in one package. <strong>But packages exist for a reason: separation of concerns.</strong></p> <p>Going back to our example, how should we refer to the database layer?</p> <p>Because Go interfaces don&rsquo;t need to be explicitly implemented, we can <strong>define them next to the code that needs them</strong>.</p> <p>So the application service says: <em>&ldquo;I need a way to cancel a training with a given UUID. I don&rsquo;t care how you do it, but I trust you to do it right if you implement this interface.&rdquo;</em></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">trainingRepository</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> <span class="nx">trainingUUID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">TrainingService</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingRepository</span> <span class="nx">trainingRepository</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="nx">TrainingService</span><span class="p">)</span> <span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> <span class="nx">trainingUUID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">c</span><span class="p">.</span><span class="nx">trainingRepository</span><span class="p">.</span><span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">trainingUUID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service.go%23L13" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/training_service.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service.go%23L13" target="_blank">Full source</a> </div> <p>The database method calls gRPC clients of the <code>trainer</code> and <code>users</code> services. That&rsquo;s not the proper place, so we introduce two new interfaces for the service to use.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">userService</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">UpdateTrainingBalance</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">userID</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">amountChange</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">trainerService</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">ScheduleTraining</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">trainingTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"> <span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">trainingTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service.go%23L23" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/training_service.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service.go%23L23" target="_blank">Full source</a> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Note that &ldquo;user&rdquo; and &ldquo;trainer&rdquo; in this context are not microservices, but application (business) concepts. It just happens that in this project, they live in the scope of microservices with the same names. </p></div> </div> <p>We move implementations of these interfaces to <code>adapters</code> as <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fadapters%2Fusers_grpc.go%23L9" target="_blank"><code>UsersGrpc</code></a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fadapters%2Ftrainer_grpc.go%23L13" target="_blank"><code>TrainerGrpc</code></a>. As a bonus, the timestamp conversion now happens there as well, invisible to the application service.</p> <h3 id="extracting-the-application-logic">Extracting the Application Logic</h3> <p>The code compiles, but our application service doesn&rsquo;t do much yet. Now it&rsquo;s time to extract the logic and put it in the proper place.</p> <p>Finally, we can use the update function pattern from <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F">the Repositories post</a> to extract the application logic out of the repository.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">c</span> <span class="nx">TrainingService</span><span class="p">)</span> <span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">user</span> <span class="nx">auth</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> <span class="nx">trainingUUID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">c</span><span class="p">.</span><span class="nx">repo</span><span class="p">.</span><span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">trainingUUID</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">training</span> <span class="nx">Training</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Role</span> <span class="o">!=</span> <span class="s">&#34;trainer&#34;</span> <span class="o">&amp;&amp;</span> <span class="nx">training</span><span class="p">.</span><span class="nx">UserUUID</span> <span class="o">!=</span> <span class="nx">user</span><span class="p">.</span><span class="nx">UUID</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;user &#39;%s&#39; is trying to cancel training of user &#39;%s&#39;&#34;</span><span class="p">,</span> <span class="nx">user</span><span class="p">.</span><span class="nx">UUID</span><span class="p">,</span> <span class="nx">training</span><span class="p">.</span><span class="nx">UserUUID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">trainingBalanceDelta</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">training</span><span class="p">.</span><span class="nf">CanBeCancelled</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// just give training back </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">trainingBalanceDelta</span> <span class="p">=</span> <span class="mi">1</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Role</span> <span class="o">==</span> <span class="s">&#34;trainer&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// 1 for cancelled training +1 fine for cancelling by trainer less than 24h before training </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">trainingBalanceDelta</span> <span class="p">=</span> <span class="mi">2</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// fine for cancelling less than 24h before training </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">trainingBalanceDelta</span> <span class="p">=</span> <span class="mi">0</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">trainingBalanceDelta</span> <span class="o">!=</span> <span class="mi">0</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nx">userService</span><span class="p">.</span><span class="nf">UpdateTrainingBalance</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">training</span><span class="p">.</span><span class="nx">UserUUID</span><span class="p">,</span> <span class="nx">trainingBalanceDelta</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;unable to change trainings balance&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nx">trainerService</span><span class="p">.</span><span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">training</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;unable to cancel training&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service.go%23L164" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/training_service.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service.go%23L164" target="_blank">Full source</a> </div> <p>The amount of logic suggests we might want to introduce a domain layer in the future. For now, let&rsquo;s keep it as is.</p> <p>I described the process for just a single <code>CancelTraining</code> method. Refer to the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2Fe98630507809492b16496f4370dd26b1d26220d3" target="_blank">full diff</a> to see how I refactored all other methods.</p> <h3 id="dependency-injection">Dependency Injection</h3> <p>How do we tell the service which adapter to use? First, we define a simple constructor for the service.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewTrainingsService</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">repo</span> <span class="nx">trainingRepository</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainerService</span> <span class="nx">trainerService</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">userService</span> <span class="nx">userService</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="nx">TrainingService</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">repo</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="s">&#34;missing trainingRepository&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">trainerService</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="s">&#34;missing trainerService&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">userService</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="s">&#34;missing userService&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">TrainingService</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">repo</span><span class="p">:</span> <span class="nx">repo</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainerService</span><span class="p">:</span> <span class="nx">trainerService</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">userService</span><span class="p">:</span> <span class="nx">userService</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service.go%23L38" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/training_service.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service.go%23L38" target="_blank">Full source</a> </div> <p>Then, in <code>main.go</code> we inject the adapter.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">trainingsRepository</span> <span class="o">:=</span> <span class="nx">adapters</span><span class="p">.</span><span class="nf">NewTrainingsFirestoreRepository</span><span class="p">(</span><span class="nx">client</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">trainerGrpc</span> <span class="o">:=</span> <span class="nx">adapters</span><span class="p">.</span><span class="nf">NewTrainerGrpc</span><span class="p">(</span><span class="nx">trainerClient</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nx">usersGrpc</span> <span class="o">:=</span> <span class="nx">adapters</span><span class="p">.</span><span class="nf">NewUsersGrpc</span><span class="p">(</span><span class="nx">usersClient</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">trainingsService</span> <span class="o">:=</span> <span class="nx">app</span><span class="p">.</span><span class="nf">NewTrainingsService</span><span class="p">(</span><span class="nx">trainingsRepository</span><span class="p">,</span> <span class="nx">trainerGrpc</span><span class="p">,</span> <span class="nx">usersGrpc</span><span class="p">)</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fmain.go%23L39" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fmain.go%23L39" target="_blank">Full source</a> </div> <p>Using the <code>main</code> function is the simplest way to inject dependencies. We&rsquo;ll explore the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgoogle%2Fwire" target="_blank">wire library</a> as the project becomes more complex in future posts.</p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/introducing-clean-architecture/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;clean-architecture;ddd;domain-driven design"> <input type="hidden" name="fields[blog_series]" value="Modern Business Software in Go"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h3 id="adding-tests">Adding tests</h3> <p>Initially, the project had all layers mixed, and it wasn&rsquo;t possible to mock dependencies. The only way to test it was with integration tests, requiring a proper database and all services running.</p> <p>While it&rsquo;s fine to cover some scenarios with such tests, they tend to be slower and not as enjoyable to work with as unit tests. After introducing these changes, I was able to <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service_test.go" target="_blank">cover <code>CancelTraining</code> with a unit test suite</a>.</p> <p>I used the standard Go approach of table-driven tests to make all cases easy to read and understand.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;return_training_balance_when_trainer_cancels&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserRole</span><span class="p">:</span> <span class="s">&#34;trainer&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Training</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">Training</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserUUID</span><span class="p">:</span> <span class="s">&#34;trainer-id&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Time</span><span class="p">:</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">().</span><span class="nf">Add</span><span class="p">(</span><span class="mi">48</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Hour</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">ShouldUpdateBalance</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">ExpectedBalanceChange</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">},</span> </span></span><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;extra_training_balance_when_trainer_cancels_before_24h&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserRole</span><span class="p">:</span> <span class="s">&#34;trainer&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Training</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">Training</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserUUID</span><span class="p">:</span> <span class="s">&#34;trainer-id&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Time</span><span class="p">:</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">().</span><span class="nf">Add</span><span class="p">(</span><span class="mi">12</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Hour</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">ShouldUpdateBalance</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">ExpectedBalanceChange</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">},</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service_test.go%23L38" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/training_service_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service_test.go%23L38" target="_blank">Full source</a> </div> <p>I didn&rsquo;t use any libraries for mocking. You can use them if you like, but your interfaces should usually be small enough to write dedicated mocks by hand.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">trainerServiceMock</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingsCancelled</span> <span class="p">[]</span><span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">trainerServiceMock</span><span class="p">)</span> <span class="nf">CancelTraining</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">trainingTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nx">trainingsCancelled</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">t</span><span class="p">.</span><span class="nx">trainingsCancelled</span><span class="p">,</span> <span class="nx">trainingTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service_test.go%23L169" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/app/training_service_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainings%2Fapp%2Ftraining_service_test.go%23L169" target="_blank">Full source</a> </div> <p>Did you notice the unusually high number of unimplemented methods in <code>repositoryMock</code>? That&rsquo;s because we use a single training service for all methods, so we need to implement the full interface even when testing just one method.</p> We&rsquo;ll improve it in our next <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fbasic-cqrs-in-go">post on CQRS</a>. <h3 id="what-about-the-boilerplate">What about the boilerplate?</h3> <p>You might be wondering if we introduced too much boilerplate. The project did grow in lines of code, but that alone doesn&rsquo;t do any harm. <strong>It&rsquo;s an <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthings-to-know-about-dry%2F">investment in loose coupling</a> that will pay off as the project grows.</strong></p> <p>Keeping everything in one package may seem easier at first, but having boundaries helps when working in a team. If all your projects have a similar structure, onboarding new team members is straightforward. Consider how much harder it would be with all layers mixed (<a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmattermost%2Fmattermost-server%2Ftree%2Fmaster%2Fapp" target="_blank">Mattermost&rsquo;s app package</a> is an example of this approach).</p> <h3 id="handling-application-errors">Handling application errors</h3> <p>One extra thing I added is <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Fcommon%2Ferrors%2Ferrors.go" target="_blank">port-agnostic errors with slugs</a>. They allow the application layer to return generic errors that both HTTP and gRPC handlers can handle.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">from</span><span class="p">.</span><span class="nf">After</span><span class="p">(</span><span class="nx">to</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">NewIncorrectInputError</span><span class="p">(</span><span class="s">&#34;date-from-after-date-to&#34;</span><span class="p">,</span> <span class="s">&#34;Date from after date to&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainer%2Fapp%2Fhour_service.go%23L29" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/hour_service.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fe98630507809492b16496f4370dd26b1d26220d3%2Finternal%2Ftrainer%2Fapp%2Fhour_service.go%23L29" target="_blank">Full source</a> </div> <p>The error above translates to a <code>400 Bad Request</code> HTTP response in ports. It includes a slug that can be translated on the frontend and shown to the user. It&rsquo;s yet another pattern to avoid leaking implementation details into application logic.</p> <h3 id="what-else">What else?</h3> <p>I encourage you to read through the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2Fe98630507809492b16496f4370dd26b1d26220d3" target="_blank">full commit</a> to see how I refactored other parts of Wild Workouts.</p> <p>You might be wondering how to enforce correct usage of layers. Is it yet another thing to remember in code reviews?</p> <p>Luckily, you can check the rules with static analysis. Use Robert&rsquo;s <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fgo-cleanarch" target="_blank">go-cleanarch</a> linter locally or include it in your CI pipeline.</p> <p>With layers separated, we&rsquo;re ready to introduce more advanced patterns.</p> <p>Next time, <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Froblaszczak" target="_blank">Robert</a> will show how to improve the project by applying CQRS. </p> <p>If you&rsquo;d like to read more on Clean Architecture, see <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-or-monolith-its-detail%2F">Why using Microservices or Monolith can be just a detail?</a>.</p>4 practical principles of high-quality database integration tests in Gohttps://threedots.tech/post/database-integration-testing/Thu, 30 Jul 2020 00:00:00 +0200https://threedots.tech/post/database-integration-testing/<p>Did you ever hear about a project where changes were tested on customers you don&rsquo;t like or countries that aren&rsquo;t profitable? Or even worse: did you work on such a project?</p> <p>It&rsquo;s not enough to say that it&rsquo;s unfair and unprofessional. It&rsquo;s also hard to develop anything new because you&rsquo;re afraid to make any change in your codebase.</p> <p>In the <a href="proxy.php?url=https%3A%2F%2Fresearch.hackerrank.com%2Fdeveloper-skills%2F2019%23jobsearch3" target="_blank">2019 HackerRank Developer Skills Report</a>, <em>Professional growth &amp; learning</em> was marked as the most important factor when looking for a new job. Do you think you can learn anything and grow when you test your application this way?</p> <p><strong>It all leads to frustration and burnout.</strong></p> <p>To develop your application easily and with confidence, you need a set of tests on multiple levels. <strong>In this article, I will cover practical examples of implementing high-quality database integration tests. I will also cover basic Go testing techniques, like test tables, assert functions, parallel execution, and black-box testing.</strong></p> <p>What does it actually mean for test quality to be high?</p> <h2 id="4-principles-of-high-quality-tests">4 principles of high-quality tests</h2> <p>I prepared 4 rules that we need to pass to say that our integration test quality is high.</p> <h4 id="1-fast">1. Fast</h4> <p>Good tests <strong>need to be fast. There is no compromise here.</strong></p> <p>Everybody hates long-running tests. Think about your teammates&rsquo; time and mental health when they&rsquo;re waiting for test results, both in CI and locally. It&rsquo;s terrifying.</p> <p>When you wait for a long time, you&rsquo;ll likely start doing something else in the meantime. After the CI passes (hopefully), you&rsquo;ll need to switch back to this task. Context switching is one of the biggest productivity killers. It&rsquo;s exhausting for our brains. We are not robots.</p> <p>I know that some companies still have tests that run for 24 hours. We don&rsquo;t want to follow this approach. &#x1f609; You should be able to run your tests locally in less than 1 minute, ideally in <strong>less than 10s</strong>. I know that sometimes this requires a time investment. It&rsquo;s an investment with an excellent ROI <em>(Return on Investment)</em>! You can quickly check your changes, and deployment times are much shorter too.</p> <p>From my experience, it&rsquo;s always worth finding quick wins that reduce test execution time the most. The <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FPareto_principle" target="_blank">Pareto principle (80/20 rule)</a> works here perfectly!</p> <h4 id="2-testing-enough-scenarios-on-all-levels">2. Testing enough scenarios on all levels</h4> <p>I hope you already know that 100% test coverage is not the best idea (unless it&rsquo;s a simple/critical library).</p> <p>It&rsquo;s always a good idea to ask yourself the question <em>&ldquo;how easily can it break?&rdquo;</em>. It&rsquo;s even more worth asking this question if the test you&rsquo;re implementing starts to look exactly like the function you&rsquo;re testing. At the end of the day, we&rsquo;re not writing tests because tests are nice: they should save our ass!</p> <p>From my experience, <strong>coverage like 70-80% is a pretty good result in Go</strong>.</p> <p>It&rsquo;s also not the best idea to cover everything with <em>component</em> or <em>end-to-end tests</em>. First, you won&rsquo;t be able to do that because of the inability to simulate some error scenarios, like rollbacks on the repository. Second, it will break the first rule: these tests will be slow.</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Ftesting-repository%2Ftesting-app.svg" alt="Testing Application" onerror="this.onerror='null';this.src=''" /> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Ftesting-repository%2Ftesting-e2e.svg" alt="E2E Testing" onerror="this.onerror='null';this.src=''" /> </p> <p>Tests on several layers should also overlap, so we will know that integration is done correctly.</p> <p>You may think that solution for that is simple: the test pyramid! And that&rsquo;s true&hellip;sometimes. Especially in applications that handle a lot of operations based on writes.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Ftesting-repository%2Ftesting-pyramid.svg" alt="Testing Pyramid" onerror="this.onerror='null';this.src=''" /> <p>But what about applications that aggregate data from multiple services and expose it via API? They have no complex logic for saving the data. Most of the code is probably related to database operations. In this case, we should use a <strong>reversed test pyramid</strong> (it actually looks more like a Christmas tree). When a big part of our application is connected to infrastructure (for example, a database), it&rsquo;s hard to cover a lot of functionality with unit tests.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Ftesting-repository%2Freversed-testing-pyramid.svg" alt="Christmas Tree" onerror="this.onerror='null';this.src=''" /> <h4 id="3-tests-need-to-be-robust-and-deterministic">3. Tests need to be robust and deterministic</h4> <p>Do you know that feeling when you&rsquo;re doing some urgent fix, tests are passing locally, you push changes to the repository and&hellip; after 20 minutes they fail in the CI? It&rsquo;s incredibly frustrating. It also discourages us from adding new tests and decreases our trust in them.</p> <p>You should fix that issue as fast as you can. The <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FBroken_windows_theory" target="_blank">Broken windows theory</a> is really valid here.</p> <h4 id="4-you-should-be-able-to-execute-most-of-the-tests-locally">4. You should be able to execute most of the tests locally</h4> <p>Tests that you run locally should give you enough confidence that the feature you developed or refactored is still working. <strong>E2E tests should just double-check if everything is integrated correctly.</strong></p> <p>You&rsquo;ll also have much more confidence when contracts between services are <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frobust-grpc-google-cloud-run%2F">robust because of using gRPC</a>, protobuf, or OpenAPI.</p> <p>This is a good reason to cover as much as we can at lower levels (starting with the lowest): unit, integration, and component tests. Only then E2E.</p> <h2 id="implementation">Implementation</h2> <p>We have some common theoretical ground. But nobody pays us for being masters of programming theory. Let&rsquo;s go to some practical examples that you can implement in your project.</p> <p>Let&rsquo;s start with the repository pattern that I described in the previous article. You don&rsquo;t need to read the rest of the articles in the series, but it&rsquo;s a good idea to check at least the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F">previous one</a>. It will make it much clearer how our repository implementation works.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="this-is-not-just-another-article-with-random-code-snippets">This is not just another article with random code snippets.</h4> <p>This post is part of a bigger series where we show how to build <strong>Go applications that are easy to develop, maintain, and fun to work with in the long term.</strong> We are doing it by sharing proven techniques based on many experiments we did with teams we lead and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%3Futm_source%3Dabout-wild-workouts%23thats-great-but-do-you-have-any-evidence-it-works">scientific research</a>.</p> <p>You can learn these patterns by building with us a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%3Futm_source%3Dabout-wild-workouts%23what-wild-workouts-can-do">fully functional</a> example Go web application &ndash; <strong>Wild Workouts</strong>.</p> <p>We did one thing differently &ndash; <strong>we included some subtle issues to the initial Wild Workouts implementation</strong>. Have we lost our minds to do that? Not yet. 😉 These issues are common for many Go projects. <strong>In the long term, these small issues become critical and stop adding new features.</strong></p> <p><strong>It&rsquo;s one of the essential skills of a senior or lead developer; you always need to keep long-term implications in mind.</strong></p> <p>We will fix them by <strong>refactoring</strong> Wild Workouts. In that way, you will quickly understand the techniques we share.</p> <p>Do you know that feeling after reading an article about some technique and trying implement it only to be blocked by some issues skipped in the guide? Cutting these details makes articles shorter and increases page views, but this is not our goal. Our goal is to create content that provides enough know-how to apply presented techniques. If you did not read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">previous articles from the series</a> yet, we highly recommend doing that.</p> <p>We believe that in some areas, there are no shortcuts. If you want to build complex applications in a fast and efficient way, you need to spend some time learning that. If it was simple, we wouldn&rsquo;t have large amounts of scary legacy code.</p> <p>Here&rsquo;s <strong><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">the full list of 14 articles</a></strong> released so far.</p> <p><strong>The full source code</strong> of Wild Workouts is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%3Futm_source%3Dabout-wild-workouts" target="_blank">GitHub</a>. Don&rsquo;t forget to leave a star for our project! ⭐</p> </p></div> </div> <p>The way we interact with our database is defined by the <code>hour.Repository</code> interface. It assumes that our repository implementation is simple. All complex logic is handled by the domain part of our application. <strong>It should just save the data without any validations, etc. One of the significant advantages of this approach is the simplification of the repository and test implementation.</strong></p> <p>In the previous article, I prepared three different database implementations: MySQL, Firebase, and in-memory. We will test all of them. They are fully compatible, so we can have just one test suite.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">hour</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Repository</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">GetOrCreateHour</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">UpdateHour</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Frepository.go%23L8" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour/repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Frepository.go%23L8" target="_blank">Full source</a> </div> <p>Because of multiple repository implementations, in our tests we iterate through a list of them.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>It&rsquo;s actually a pretty similar pattern to how we implemented <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fpubsub%2Ftests%2Ftest_pubsub.go" target="_blank">tests in Watermill</a>.</p> <p>All Pub/Sub implementations are passing the same test suite.</p> </p></div> </div> <p>All tests that we write will be black-box tests. In other words, we will only cover public functions with tests. To ensure that, all our test packages have the <code>_test</code> suffix. This forces us to use only the public interface of the package. <strong>It will pay back in the future with much more stable tests that are not affected by any internal changes.</strong> If you can&rsquo;t write good black-box tests, you should consider if your public APIs are well designed.</p> <p>All our repository tests are executed in parallel. Thanks to that, they take less than <code>200ms</code>. After adding multiple test cases, this time should not increase significantly.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">main_test</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestRepository</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">rand</span><span class="p">.</span><span class="nf">Seed</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">().</span><span class="nf">UTC</span><span class="p">().</span><span class="nf">UnixNano</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">repositories</span> <span class="o">:=</span> <span class="nf">createRepositories</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">repositories</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// When you are looping over the slice and later using iterated value in goroutine (here because of t.Parallel()), </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// you need to always create variable scoped in loop body! </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// More info here: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">r</span> <span class="o">:=</span> <span class="nx">repositories</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// It&#39;s always a good idea to build all non-unit tests to be able to work in parallel. </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// Thanks to that, your tests will be always fast and you will not be afraid to add more tests because of slowdown. </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="s">&#34;testUpdateHour&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nf">testUpdateHour</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Repository</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="s">&#34;testUpdateHour_parallel&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nf">testUpdateHour_parallel</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Repository</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="s">&#34;testHourRepository_update_existing&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nf">testHourRepository_update_existing</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Repository</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="s">&#34;testUpdateHour_rollback&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Parallel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nf">testUpdateHour_rollback</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">r</span><span class="p">.</span><span class="nx">Repository</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fhour_repository_test.go%23L19" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_repository_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fhour_repository_test.go%23L19" target="_blank">Full source</a> </div> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/database-integration-testing/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;repository;mysql;firestore;testing"> <input type="hidden" name="fields[blog_series]" value="Modern Business Software in Go"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <p>When we have multiple tests where we pass the same input and check the same output, it&rsquo;s a good idea to use a technique known as <em>test table</em>. The idea is simple: define a slice of inputs and expected outputs, then iterate over it to execute tests.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testUpdateHour</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">,</span> <span class="nx">repository</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">Repository</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Helper</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">testCases</span> <span class="o">:=</span> <span class="p">[]</span><span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">CreateHour</span> <span class="kd">func</span><span class="p">(</span><span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span> </span></span><span class="line"><span class="cl"> <span class="p">}{</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;available_hour&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">CreateHour</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">newValidAvailableHour</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;not_available_hour&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">CreateHour</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">h</span> <span class="o">:=</span> <span class="nf">newValidAvailableHour</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">h</span><span class="p">.</span><span class="nf">MakeNotAvailable</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;hour_with_training&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">CreateHour</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">h</span> <span class="o">:=</span> <span class="nf">newValidAvailableHour</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">h</span><span class="p">.</span><span class="nf">ScheduleTraining</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">tc</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">testCases</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="nx">tc</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">newHour</span> <span class="o">:=</span> <span class="nx">tc</span><span class="p">.</span><span class="nf">CreateHour</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repository</span><span class="p">.</span><span class="nf">UpdateHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">newHour</span><span class="p">.</span><span class="nf">Time</span><span class="p">(),</span> <span class="kd">func</span><span class="p">(</span><span class="nx">_</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// UpdateHour provides us existing/new *hour.Hour, </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// but we are ignoring this hour and persisting result of `CreateHour` </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// we can assert this hour later in assertHourInRepository </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="nx">newHour</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nf">assertHourInRepository</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">t</span><span class="p">,</span> <span class="nx">repository</span><span class="p">,</span> <span class="nx">newHour</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fhour_repository_test.go%23L77" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_repository_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fhour_repository_test.go%23L77" target="_blank">Full source</a> </div> <p>You can see that we used the popular <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fstretchr%2Ftestify" target="_blank"><code>github.com/stretchr/testify</code></a> library. It significantly reduces boilerplate in tests by providing multiple helpers for <a href="proxy.php?url=https%3A%2F%2Fgodoc.org%2Fgithub.com%2Fstretchr%2Ftestify%2Fassert" target="_blank">asserts</a>.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="requirenoerror"><code>require.NoError()</code></h4> <p>When <code>assert.NoError</code> assert fails, test execution is not interrupted.</p> <p>It&rsquo;s worth mentioning that asserts from the <code>require</code> package stop execution of the test when they fail. Because of that, it&rsquo;s often a good idea to use <code>require</code> for checking errors. In many cases, if some operation fails, it doesn&rsquo;t make sense to check anything later.</p> <p>When we assert multiple values, <code>assert</code> is a better choice because you will receive more context.</p> </p></div> </div> <p>If we have more specific data to assert, it&rsquo;s always a good idea to add some helpers. It removes a lot of duplication and improves test readability!</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">assertHourInRepository</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">,</span> <span class="nx">repo</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">Repository</span><span class="p">,</span> <span class="nx">hour</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">NotNil</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">hour</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">hourFromRepo</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repo</span><span class="p">.</span><span class="nf">GetOrCreateHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">hour</span><span class="p">.</span><span class="nf">Time</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">assert</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">hour</span><span class="p">,</span> <span class="nx">hourFromRepo</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fhour_repository_test.go%23L327" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_repository_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fhour_repository_test.go%23L327" target="_blank">Full source</a> </div> <h3 id="testing-transactions">Testing transactions</h3> <p>Mistakes taught me that I should not trust myself when implementing complex code. Sometimes we don&rsquo;t understand the documentation or just introduce some stupid mistake. You can gain confidence in two ways:</p> <ol> <li>TDD - let&rsquo;s start with a test that will check if the transaction is working properly.</li> <li>Let&rsquo;s start with the implementation and add tests later.</li> </ol> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testUpdateHour_rollback</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">,</span> <span class="nx">repository</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">Repository</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">.</span><span class="nf">Helper</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span> <span class="o">:=</span> <span class="nf">newValidHourTime</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repository</span><span class="p">.</span><span class="nf">UpdateHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">hourTime</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">h</span><span class="p">.</span><span class="nf">MakeAvailable</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">repository</span><span class="p">.</span><span class="nf">UpdateHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">hourTime</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">assert</span><span class="p">.</span><span class="nf">True</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">h</span><span class="p">.</span><span class="nf">IsAvailable</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">h</span><span class="p">.</span><span class="nf">MakeNotAvailable</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;something went wrong&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">persistedHour</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repository</span><span class="p">.</span><span class="nf">GetOrCreateHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">hourTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">assert</span><span class="p">.</span><span class="nf">True</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">persistedHour</span><span class="p">.</span><span class="nf">IsAvailable</span><span class="p">(),</span> <span class="s">&#34;availability change was persisted, not rolled back&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fhour_repository_test.go%23L197" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_repository_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fhour_repository_test.go%23L197" target="_blank">Full source</a> </div> <p>When I&rsquo;m not using TDD, I try to be paranoid about whether the test implementation is valid.</p> <p>To be more confident, I use a technique that I call <strong>test sabotage</strong>.</p> <p><strong>The method is simple: break the implementation that we&rsquo;re testing and see if anything fails.</strong></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> func (m MySQLHourRepository) finishTransaction(err error, tx *sqlx.Tx) error { </span></span><span class="line"><span class="cl"><span class="gd">- if err != nil { </span></span></span><span class="line"><span class="cl"><span class="gd">- if rollbackErr := tx.Rollback(); rollbackErr != nil { </span></span></span><span class="line"><span class="cl"><span class="gd">- return multierr.Combine(err, rollbackErr) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- return err </span></span></span><span class="line"><span class="cl"><span class="gd">- } else { </span></span></span><span class="line"><span class="cl"><span class="gd">- if commitErr := tx.Commit(); commitErr != nil { </span></span></span><span class="line"><span class="cl"><span class="gd">- return errors.Wrap(err, &#34;failed to commit tx&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- return nil </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+ if commitErr := tx.Commit(); commitErr != nil { </span></span></span><span class="line"><span class="cl"><span class="gi">+ return errors.Wrap(err, &#34;failed to commit tx&#34;) </span></span></span><span class="line"><span class="cl"><span class="gi"></span> } </span></span><span class="line"><span class="cl"><span class="gi">+ </span></span></span><span class="line"><span class="cl"><span class="gi">+ return nil </span></span></span><span class="line"><span class="cl"><span class="gi"></span> } </span></span></code></pre></div><p>If your tests are passing after a change like that, I have bad news&hellip;</p> <h3 id="testing-database-race-conditions">Testing database race conditions</h3> <p>Our applications don&rsquo;t work in a void. It can always happen that multiple clients try to do the same operation, and only one can win!</p> <p>In our case, the typical scenario is when two clients try to schedule a training at the same time. <strong>We can have only one training scheduled in one hour.</strong></p> <p>This constraint is achieved by optimistic locking (described in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F%23updating-the-data">the previous article</a>) and domain constraints (described <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%23the-third-rule---domain-needs-to-be-database-agnostic">two articles ago</a>).</p> <p>Let&rsquo;s verify if it&rsquo;s possible to schedule one hour more than once. The idea is simple: <strong>create 20 goroutines, release them all at once, and try to schedule training.</strong> We expect exactly one worker to succeed.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testUpdateHour_parallel</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">,</span> <span class="nx">repository</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">Repository</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="nx">workersCount</span> <span class="o">:=</span> <span class="mi">20</span> </span></span><span class="line"><span class="cl"> <span class="nx">workersDone</span> <span class="o">:=</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="nx">workersDone</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="nx">workersCount</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// closing startWorkers will unblock all workers at once, </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// thanks to that it will be more likely to have race condition </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">startWorkers</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kd">struct</span><span class="p">{})</span> </span></span><span class="line"><span class="cl"> <span class="c1">// if training was successfully scheduled, number of the worker is sent to this channel </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">trainingsScheduled</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">chan</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">workersCount</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// we are trying to do race condition, in practice only one worker should be able to finish transaction </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">for</span> <span class="nx">worker</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">worker</span> <span class="p">&lt;</span> <span class="nx">workersCount</span><span class="p">;</span> <span class="nx">worker</span><span class="o">++</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">workerNum</span> <span class="o">:=</span> <span class="nx">worker</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="nx">workersDone</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="o">&lt;-</span><span class="nx">startWorkers</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">schedulingTraining</span> <span class="o">:=</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">repository</span><span class="p">.</span><span class="nf">UpdateHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">hourTime</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// training is already scheduled, nothing to do there </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">if</span> <span class="nx">h</span><span class="p">.</span><span class="nf">HasTrainingScheduled</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="c1">// training is not scheduled yet, so let&#39;s try to do that </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nf">ScheduleTraining</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">schedulingTraining</span> <span class="p">=</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">schedulingTraining</span> <span class="o">&amp;&amp;</span> <span class="nx">err</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// training is only scheduled if UpdateHour didn&#39;t return an error </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">trainingsScheduled</span> <span class="o">&lt;-</span> <span class="nx">workerNum</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nb">close</span><span class="p">(</span><span class="nx">startWorkers</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// we are waiting, when all workers did the job </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">workersDone</span><span class="p">.</span><span class="nf">Wait</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nb">close</span><span class="p">(</span><span class="nx">trainingsScheduled</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">workersScheduledTraining</span> <span class="p">[]</span><span class="kt">int</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">workerNum</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">trainingsScheduled</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">workersScheduledTraining</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">workersScheduledTraining</span><span class="p">,</span> <span class="nx">workerNum</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">assert</span><span class="p">.</span><span class="nf">Len</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">workersScheduledTraining</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="s">&#34;only one worker should schedule training&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fhour_repository_test.go%23L128" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_repository_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fhour_repository_test.go%23L128" target="_blank">Full source</a> </div> <p><strong>This is also a good example of use cases that are easier to test at the integration test level, not at acceptance or E2E level.</strong> Tests like this as E2E would be really heavy, and you would need more workers to ensure they execute transactions simultaneously.</p> <h3 id="making-tests-fast">Making tests fast</h3> <p><strong>If your tests can&rsquo;t be executed in parallel, they will be slow.</strong> Even on the best machine.</p> <p>Is putting <code>t.Parallel()</code> enough? Well, we need to ensure that our tests are independent. In our case, <strong>if two tests tried to edit the same hour, they could fail randomly.</strong> This is a highly undesirable situation.</p> <p>To achieve that, I created the <code>newValidHourTime()</code> function that provides a random hour unique to the current test run. In most applications, generating a unique UUID for your entities may be enough.</p> <p>In some situations, it may be less obvious but still not impossible. I encourage you to spend some time finding the solution. Please treat it as an investment in your and your teammates&rsquo; mental health &#x1f609;.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// usedHours is storing hours used during the test, </span></span></span><span class="line"><span class="cl"><span class="c1">// to ensure that within one test run we are not using the same hour </span></span></span><span class="line"><span class="cl"><span class="c1">// (it should be not a problem between test runs) </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">var</span> <span class="nx">usedHours</span> <span class="p">=</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">Map</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">newValidHourTime</span><span class="p">()</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">minTime</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">().</span><span class="nf">AddDate</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">minTimestamp</span> <span class="o">:=</span> <span class="nx">minTime</span><span class="p">.</span><span class="nf">Unix</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nx">maxTimestamp</span> <span class="o">:=</span> <span class="nx">minTime</span><span class="p">.</span><span class="nf">AddDate</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">testHourFactory</span><span class="p">.</span><span class="nf">Config</span><span class="p">().</span><span class="nx">MaxWeeksInTheFutureToSet</span><span class="o">*</span><span class="mi">7</span><span class="p">).</span><span class="nf">Unix</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">t</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Unix</span><span class="p">(</span><span class="nx">rand</span><span class="p">.</span><span class="nf">Int63n</span><span class="p">(</span><span class="nx">maxTimestamp</span><span class="o">-</span><span class="nx">minTimestamp</span><span class="p">)</span><span class="o">+</span><span class="nx">minTimestamp</span><span class="p">,</span> <span class="mi">0</span><span class="p">).</span><span class="nf">Truncate</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Hour</span><span class="p">).</span><span class="nf">Local</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">_</span><span class="p">,</span> <span class="nx">alreadyUsed</span> <span class="o">:=</span> <span class="nx">usedHours</span><span class="p">.</span><span class="nf">LoadOrStore</span><span class="p">(</span><span class="nx">t</span><span class="p">.</span><span class="nf">Unix</span><span class="p">(),</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">alreadyUsed</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">t</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fhour_repository_test.go%23L306" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_repository_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2Finternal%2Ftrainer%2Fhour_repository_test.go%23L306" target="_blank">Full source</a> </div> <p>Another good thing about making our tests independent is that there&rsquo;s no need for data cleanup. In my experience, doing data cleanup is always messy because:</p> <ul> <li>when it doesn&rsquo;t work correctly, it creates hard-to-debug issues in tests,</li> <li>it makes tests slower,</li> <li>it adds overhead to development (you need to remember to update the cleanup function),</li> <li>it may make running tests in parallel harder.</li> </ul> <p>It may also happen that we&rsquo;re not able to run tests in parallel. Two common examples are:</p> <ul> <li>pagination: if you iterate over pages, other tests can insert something in-between and move &ldquo;items&rdquo; in the pages.</li> <li>global counters: like with pagination, other tests may affect the counter in an unexpected way.</li> </ul> <p>In that case, it&rsquo;s worth keeping these tests as short as we can.</p> <h4 id="please-dont-use-sleep-in-tests">Please, don&rsquo;t use sleep in tests!</h4> <p>The last tip about what makes tests flaky and slow: putting the sleep function in them. Please, <strong>don&rsquo;t</strong>! It&rsquo;s much better to synchronize your tests with channels or <code>sync.WaitGroup{}</code>. They are faster and more stable that way.</p> <p>If you really need to wait for something, it&rsquo;s better to use <code>assert.Eventually</code> instead of a sleep.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p><code>Eventually</code> asserts that given <code>condition</code> will be met in <code>waitFor</code> time, periodically checking target function each <code>tick</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">assert</span><span class="p">.</span><span class="nf">Eventually</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">()</span> <span class="kt">bool</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">true</span> <span class="p">},</span> <span class="c1">// condition </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">,</span> <span class="c1">// waitFor </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="mi">10</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span><span class="p">,</span> <span class="c1">// tick </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">)</span> </span></span></code></pre></div><p>Source: <a href="proxy.php?url=https%3A%2F%2Fgodoc.org%2Fgithub.com%2Fstretchr%2Ftestify%2Fassert%23Eventually" target="_blank">godoc.org/github.com/stretchr/testify/assert</a></p> </p></div> </div> <h2 id="running">Running</h2> <p>Now that our tests are implemented, it&rsquo;s time to run them!</p> <p>Before that, we need to start our container with Firebase and MySQL using <code>docker-compose up</code>.</p> <p>I prepared a <code>make test</code> command that runs tests in a consistent way (for example, with the <code>-race</code> flag). It can also be used in CI.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ make <span class="nb">test</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">? github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/auth <span class="o">[]</span> </span></span><span class="line"><span class="cl">? github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/client <span class="o">[</span>no <span class="nb">test</span> files<span class="o">]</span> </span></span><span class="line"><span class="cl">? github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/trainer <span class="o">[</span>no <span class="nb">test</span> files<span class="o">]</span> </span></span><span class="line"><span class="cl">? github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/users <span class="o">[</span>no <span class="nb">test</span> files<span class="o">]</span> </span></span><span class="line"><span class="cl">? github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/logs <span class="o">[</span>no <span class="nb">test</span> files<span class="o">]</span> </span></span><span class="line"><span class="cl">? github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server <span class="o">[</span>no <span class="nb">test</span> files<span class="o">]</span> </span></span><span class="line"><span class="cl">? github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server/httperr <span class="o">[</span>no <span class="nb">test</span> files<span class="o">]</span> </span></span><span class="line"><span class="cl">ok github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer 0.172s </span></span><span class="line"><span class="cl">ok github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour 0.031s </span></span><span class="line"><span class="cl">? github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings <span class="o">[</span>no <span class="nb">test</span> files<span class="o">]</span> </span></span><span class="line"><span class="cl">? github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/users <span class="o">[</span>no <span class="nb">test</span> files<span class="o">]</span> </span></span></code></pre></div><h3 id="running-one-test-and-passing-custom-params">Running one test and passing custom params</h3> <p>If you would like to pass some extra params, to have a verbose output (<code>-v</code>) or execute exact test (<code>-run</code>), you can pass it after <code>make test --</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ make <span class="nb">test</span> -- -v -run ^TestRepository/memory/testUpdateHour$ </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">--- PASS: TestRepository <span class="o">(</span>0.00s<span class="o">)</span> </span></span><span class="line"><span class="cl"> --- PASS: TestRepository/memory <span class="o">(</span>0.00s<span class="o">)</span> </span></span><span class="line"><span class="cl"> --- PASS: TestRepository/memory/testUpdateHour <span class="o">(</span>0.00s<span class="o">)</span> </span></span><span class="line"><span class="cl"> --- PASS: TestRepository/memory/testUpdateHour/available_hour <span class="o">(</span>0.00s<span class="o">)</span> </span></span><span class="line"><span class="cl"> --- PASS: TestRepository/memory/testUpdateHour/not_available_hour <span class="o">(</span>0.00s<span class="o">)</span> </span></span><span class="line"><span class="cl"> --- PASS: TestRepository/memory/testUpdateHour/hour_with_training <span class="o">(</span>0.00s<span class="o">)</span> </span></span><span class="line"><span class="cl">PASS </span></span></code></pre></div><p>If you are interested in how it is implemented, I&rsquo;d recommend you check my <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F521fdb5d6aa4f1f7ff18ec33f50ce6710906d73b%2FMakefile%23L60" target="_blank">Makefile magic :man_mage:</a>.</p> <h2 id="debugging">Debugging</h2> <p>Sometimes our tests fail in an unclear way. In that case, it&rsquo;s useful to be able to easily check what data is in our database.</p> <p>For SQL databases, my first choice for that is <a href="proxy.php?url=https%3A%2F%2Fwww.mycli.net%2Finstall" target="_blank">mycli for MySQL</a> and <a href="proxy.php?url=https%3A%2F%2Fwww.pgcli.com%2F" target="_blank">pgcli for PostgreSQL</a>. I&rsquo;ve added a <code>make mycli</code> command to the Makefile, so you don&rsquo;t need to pass credentials all the time.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="err">$</span><span class="w"> </span><span class="n">make</span><span class="w"> </span><span class="n">mycli</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">mysql</span><span class="w"> </span><span class="k">user</span><span class="o">@</span><span class="n">localhost</span><span class="p">:</span><span class="n">db</span><span class="o">&gt;</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="o">`</span><span class="n">hours</span><span class="o">`</span><span class="p">;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">+</span><span class="c1">---------------------+--------------------+ </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w"> </span><span class="n">hour</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">availability</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="c1">---------------------+--------------------| </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="o">|</span><span class="w"> </span><span class="mi">2020</span><span class="o">-</span><span class="mi">08</span><span class="o">-</span><span class="mi">31</span><span class="w"> </span><span class="mi">15</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">available</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2020</span><span class="o">-</span><span class="mi">09</span><span class="o">-</span><span class="mi">13</span><span class="w"> </span><span class="mi">19</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">training_scheduled</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2022</span><span class="o">-</span><span class="mi">07</span><span class="o">-</span><span class="mi">19</span><span class="w"> </span><span class="mi">19</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">training_scheduled</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2023</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">19</span><span class="w"> </span><span class="mi">14</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">available</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2023</span><span class="o">-</span><span class="mi">08</span><span class="o">-</span><span class="mi">05</span><span class="w"> </span><span class="mi">03</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">training_scheduled</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2024</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">17</span><span class="w"> </span><span class="mi">07</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">not_available</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2024</span><span class="o">-</span><span class="mi">02</span><span class="o">-</span><span class="mi">07</span><span class="w"> </span><span class="mi">15</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">available</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2024</span><span class="o">-</span><span class="mi">05</span><span class="o">-</span><span class="mi">07</span><span class="w"> </span><span class="mi">18</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">training_scheduled</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2024</span><span class="o">-</span><span class="mi">05</span><span class="o">-</span><span class="mi">22</span><span class="w"> </span><span class="mi">09</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">available</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2025</span><span class="o">-</span><span class="mi">03</span><span class="o">-</span><span class="mi">04</span><span class="w"> </span><span class="mi">15</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">available</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2025</span><span class="o">-</span><span class="mi">04</span><span class="o">-</span><span class="mi">15</span><span class="w"> </span><span class="mi">08</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">training_scheduled</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2026</span><span class="o">-</span><span class="mi">05</span><span class="o">-</span><span class="mi">22</span><span class="w"> </span><span class="mi">09</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">training_scheduled</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2028</span><span class="o">-</span><span class="mi">01</span><span class="o">-</span><span class="mi">24</span><span class="w"> </span><span class="mi">18</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">not_available</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2028</span><span class="o">-</span><span class="mi">07</span><span class="o">-</span><span class="mi">09</span><span class="w"> </span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">not_available</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">|</span><span class="w"> </span><span class="mi">2029</span><span class="o">-</span><span class="mi">09</span><span class="o">-</span><span class="mi">23</span><span class="w"> </span><span class="mi">15</span><span class="p">:</span><span class="mi">00</span><span class="p">:</span><span class="mi">00</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">training_scheduled</span><span class="w"> </span><span class="o">|</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="o">+</span><span class="c1">---------------------+--------------------+ </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="mi">15</span><span class="w"> </span><span class="k">rows</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="k">set</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="n">Time</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">.</span><span class="mi">025</span><span class="n">s</span><span class="w"> </span></span></span></code></pre></div><p>For Firestore, the emulator is exposing the UI at <a href="proxy.php?url=http%3A%2F%2Flocalhost%3A4000%2Ffirestore%2F" target="_blank">localhost:4000/firestore</a>.</p> <p><a href="proxy.php?url=http%3A%2F%2Flocalhost%3A4000%2Ffirestore%2F" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="982" height="633" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Ffirestore-console_hu073cf84a622e1edd694184861852ee5f_74134_982x633_resize_q80_h2_lanczos_3.webp" alt="Firestore Console" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Ffirestore-console_hu073cf84a622e1edd694184861852ee5f_74134_982x633_resize_lanczos_3.png"" /> </a></p> <h3 id="first-step-for-having-a-well-tested-application">First step for having a well-tested application</h3> <p>The biggest gap we currently have is a lack of tests at the component and E2E level. Also, a big part of the application is not tested at all. We will fix that in the next articles. We will also cover some topics that we skipped this time.</p> <p>But before that, we have one topic that we need to cover first: Clean/Hexagonal architecture! This approach will help us organize our application and make future refactoring and features easier to implement.</p> <p>Just to remind, <strong>the entire source code of Wild Workouts is <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2F" target="_blank">available on GitHub</a></strong>. <strong>You can run it locally and deploy to Google Cloud with one command.</strong></p> <p>Did you like this article and haven&rsquo;t had a chance to read the previous ones? There are <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dtesting-repository-outro">14 more articles to check</a>!</p> <p>And that&rsquo;s all for today. See you soon! &#x1f642;</p>The Repository pattern in Go: a painless way to simplify your service logichttps://threedots.tech/post/repository-pattern-in-go/Thu, 16 Jul 2020 00:00:00 +0200https://threedots.tech/post/repository-pattern-in-go/<p>I&rsquo;ve seen a lot of complicated code in my life. Pretty often, the reason for that complexity was application logic coupled with database logic. <strong>Keeping the logic of your application together with your database logic makes your application much more complex, harder to test, and harder to maintain</strong>.</p> <p>There is already a proven and simple pattern that solves these issues. This pattern allows you to <strong>separate your application logic from database logic</strong>. It allows you to <strong>make your code simpler and easier to extend with new functionalities</strong>. As a bonus, you can <strong>defer the important decision</strong> of choosing a database solution and schema. Another good side effect of this approach is out-of-the-box <strong>immunity to database vendor lock-in</strong>. The pattern that I have in mind is the <em>repository</em>.</p> <p>When I think back to the applications I worked with, I remember that it was tough to understand how they worked. <strong>I was always afraid to make any changes: you never knew what unexpected, bad side effects it could have.</strong> It&rsquo;s hard to understand the application logic when it&rsquo;s mixed with the database implementation. It&rsquo;s also a source of duplication.</p> <p>One rescue here may be building <a href="proxy.php?url=https%3A%2F%2Fmartinfowler.com%2Farticles%2Fmicroservice-testing%2F%23testing-end-to-end-introduction" target="_blank">end-to-end tests</a>. But that hides the problem instead of solving it. Having a lot of E2E tests is slow, flaky, and hard to maintain. Sometimes they even prevent us from creating new functionality rather than help.</p> <p>In this article, I will teach you how to apply this pattern in <strong>Go</strong> in a pragmatic, elegant, and straightforward way. I will also deeply cover a topic that is often skipped - <strong>clean transactions handling</strong>.</p> <p>To prove that I prepared 3 implementations: <strong>Firestore, MySQL, and simple in-memory</strong>.</p> <p>Without a long introduction, let&rsquo;s jump to the practical examples!</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="this-is-not-just-another-article-with-random-code-snippets">This is not just another article with random code snippets.</h4> <p>This post is part of a bigger series where we show how to build <strong>Go applications that are easy to develop, maintain, and fun to work with in the long term.</strong> We are doing it by sharing proven techniques based on many experiments we did with teams we lead and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%3Futm_source%3Dabout-wild-workouts%23thats-great-but-do-you-have-any-evidence-it-works">scientific research</a>.</p> <p>You can learn these patterns by building with us a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%3Futm_source%3Dabout-wild-workouts%23what-wild-workouts-can-do">fully functional</a> example Go web application &ndash; <strong>Wild Workouts</strong>.</p> <p>We did one thing differently &ndash; <strong>we included some subtle issues to the initial Wild Workouts implementation</strong>. Have we lost our minds to do that? Not yet. 😉 These issues are common for many Go projects. <strong>In the long term, these small issues become critical and stop adding new features.</strong></p> <p><strong>It&rsquo;s one of the essential skills of a senior or lead developer; you always need to keep long-term implications in mind.</strong></p> <p>We will fix them by <strong>refactoring</strong> Wild Workouts. In that way, you will quickly understand the techniques we share.</p> <p>Do you know that feeling after reading an article about some technique and trying implement it only to be blocked by some issues skipped in the guide? Cutting these details makes articles shorter and increases page views, but this is not our goal. Our goal is to create content that provides enough know-how to apply presented techniques. If you did not read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">previous articles from the series</a> yet, we highly recommend doing that.</p> <p>We believe that in some areas, there are no shortcuts. If you want to build complex applications in a fast and efficient way, you need to spend some time learning that. If it was simple, we wouldn&rsquo;t have large amounts of scary legacy code.</p> <p>Here&rsquo;s <strong><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">the full list of 14 articles</a></strong> released so far.</p> <p><strong>The full source code</strong> of Wild Workouts is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%3Futm_source%3Dabout-wild-workouts" target="_blank">GitHub</a>. Don&rsquo;t forget to leave a star for our project! ⭐</p> </p></div> </div> <h2 id="repository-interface">Repository interface</h2> <p>The idea of using the repository pattern is:</p> <p><strong>Let&rsquo;s abstract our database implementation by defining the interaction with it through an interface.</strong> <strong>You need to be able to use this interface for any database implementation: that means it should be free of any implementation details of any specific database.</strong></p> <p>Let&rsquo;s start with refactoring the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Ftree%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer" target="_blank">trainer</a> service. Currently, the service allows us to get information about hour availability <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhttp.go%23L17" target="_blank">via HTTP API</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fgrpc.go%23L75" target="_blank">via gRPC</a>. We can also change the hour&rsquo;s availability <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhttp.go%23L12" target="_blank">via HTTP API</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fgrpc.go%23L16" target="_blank">gRPC</a>.</p> <p>In the previous <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%23refactoring-of-trainer-service">article</a>, we refactored <code>Hour</code> to use the DDD Lite approach. Thanks to that, we don&rsquo;t need to think about enforcing rules for when <code>Hour</code> can be updated. Our domain layer ensures that we can&rsquo;t do anything &ldquo;stupid&rdquo;. We also don&rsquo;t need to think about validation. We can just use the type and execute the necessary operations.</p> <p>We need to be able to get the current state of <code>Hour</code> from the database and save it. Also, if two people try to schedule a training simultaneously, only one person should be able to schedule training for that hour.</p> <p>Let&rsquo;s reflect our needs in the interface:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">hour</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Repository</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">GetOrCreateHour</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">UpdateHour</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Frepository.go%23L8" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour/repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Frepository.go%23L8" target="_blank">Full source</a> </div> <p>We will use <code>GetOrCreateHour</code> to get the data, and <code>UpdateHour</code> to save the data.</p> <p>We define the interface in the same package as the <code>Hour</code> type. Thanks to that, we can avoid duplication when using this interface in many modules (from my experience, this may often be the case). It&rsquo;s also a similar pattern to <code>io.Writer</code>, where the <code>io</code> package defines the interface and all implementations are decoupled in separate packages.</p> <p>How to implement that interface?</p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/repository-pattern-in-go/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;repository;mysql;firestore;ddd;domain-driven design"> <input type="hidden" name="fields[blog_series]" value="Modern Business Software in Go"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h2 id="reading-the-data">Reading the data</h2> <p>Most database drivers can use <code>ctx context.Context</code> for cancellation, tracing, etc. It&rsquo;s not specific to any database (it&rsquo;s a common Go concept), so you shouldn&rsquo;t be afraid that you&rsquo;ll spoil the domain. &#x1f609;</p> <p>In most cases, we query data by using UUID or ID rather than <code>time.Time</code>. In our case, it&rsquo;s okay: the hour is unique by design. I can imagine a situation where we would want to support multiple trainers. In that case, this assumption would not be valid. Changing to UUID/ID would still be simple. But for now, <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FYou_aren%2527t_gonna_need_it" target="_blank">YAGNI</a>!</p> <p>For clarity, this is how an interface based on UUID might look:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nf">GetOrCreateHour</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">hourUUID</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span></code></pre></div><div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> You can find an example of a repository based on UUID in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-cqrs-clean-architecture-combined%2F%23repository-refactoring">Combining DDD, CQRS, and Clean Architecture article</a>. </p></div> </div> <p>How is the interface used in the application?</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="s">&#34;github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour&#34;</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">GrpcServer</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourRepository</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">Repository</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="nx">GrpcServer</span><span class="p">)</span> <span class="nf">IsHourAvailable</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">request</span> <span class="o">*</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">IsHourAvailableRequest</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">IsHourAvailableResponse</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingTime</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">protoTimestampToTime</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">InvalidArgument</span><span class="p">,</span> <span class="s">&#34;unable to parse time&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">h</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">g</span><span class="p">.</span><span class="nx">hourRepository</span><span class="p">.</span><span class="nf">GetOrCreateHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">trainingTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">Internal</span><span class="p">,</span> <span class="nx">err</span><span class="p">.</span><span class="nf">Error</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">IsHourAvailableResponse</span><span class="p">{</span><span class="nx">IsAvailable</span><span class="p">:</span> <span class="nx">h</span><span class="p">.</span><span class="nf">IsAvailable</span><span class="p">()},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fgrpc.go%23L75" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/grpc.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fgrpc.go%23L75" target="_blank">Full source</a> </div> <p>No rocket science! We get <code>hour.Hour</code> and check if it&rsquo;s available. Can you guess what database we use? No, and that&rsquo;s the point!</p> <p>As I mentioned, we can avoid vendor lock-in and easily swap the database. If you can swap the database, <strong>it&rsquo;s a sign that you implemented the repository pattern correctly</strong>. In practice, changing the database is rare. &#x1f609; When you&rsquo;re using a solution that is not self-hosted (like Firestore), it&rsquo;s more important to mitigate the risk and avoid vendor lock-in.</p> <p>A helpful side effect is that we can defer the decision of which database implementation to use. I call this approach <em>Domain First</em>. I described it in depth in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%23domain-first-approach">the previous article</a>. <strong>Deferring the decision about the database for later can save time at the beginning of the project. With more information and context, we can also make a better decision.</strong></p> <p>When we use the Domain-First approach, the first and simplest repository implementation may be an in-memory implementation.</p> <h3 id="example-in-memory-implementation">Example In-memory implementation</h3> <p>Our memory uses a simple map under the hood. <code>getOrCreateHour</code> has 5 lines (without a comment and one newline)! &#x1f609;</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">MemoryHourRepository</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">hours</span> <span class="kd">map</span><span class="p">[</span><span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">]</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span> </span></span><span class="line"><span class="cl"> <span class="nx">lock</span> <span class="o">*</span><span class="nx">sync</span><span class="p">.</span><span class="nx">RWMutex</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">hourFactory</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">Factory</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewMemoryHourRepository</span><span class="p">(</span><span class="nx">hourFactory</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">Factory</span><span class="p">)</span> <span class="o">*</span><span class="nx">MemoryHourRepository</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">hourFactory</span><span class="p">.</span><span class="nf">IsZero</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="s">&#34;missing hourFactory&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">MemoryHourRepository</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">hours</span><span class="p">:</span> <span class="kd">map</span><span class="p">[</span><span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">]</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">lock</span><span class="p">:</span> <span class="o">&amp;</span><span class="nx">sync</span><span class="p">.</span><span class="nx">RWMutex</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourFactory</span><span class="p">:</span> <span class="nx">hourFactory</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="nx">MemoryHourRepository</span><span class="p">)</span> <span class="nf">GetOrCreateHour</span><span class="p">(</span><span class="nx">_</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">m</span><span class="p">.</span><span class="nx">lock</span><span class="p">.</span><span class="nf">RLock</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="nx">m</span><span class="p">.</span><span class="nx">lock</span><span class="p">.</span><span class="nf">RUnlock</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">m</span><span class="p">.</span><span class="nf">getOrCreateHour</span><span class="p">(</span><span class="nx">hourTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="nx">MemoryHourRepository</span><span class="p">)</span> <span class="nf">getOrCreateHour</span><span class="p">(</span><span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">currentHour</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">hours</span><span class="p">[</span><span class="nx">hourTime</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">m</span><span class="p">.</span><span class="nx">hourFactory</span><span class="p">.</span><span class="nf">NewNotAvailableHour</span><span class="p">(</span><span class="nx">hourTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// we don&#39;t store hours as pointers, but as values </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// thanks to that, we are sure that nobody can modify Hour without using UpdateHour </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">currentHour</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_memory_repository.go%23L11" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_memory_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_memory_repository.go%23L11" target="_blank">Full source</a> </div> <p>Unfortunately, the memory implementation has some downsides. The biggest one is that it doesn&rsquo;t keep the data after a service restart. &#x1f609; It can be enough for a functional pre-alpha version. To make our application production-ready, we need something a bit more persistent.</p> <h3 id="example-mysql-implementation">Example MySQL implementation</h3> <p>We already know what our model <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Fhour.go%23L11" target="_blank">looks like</a> and how <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Favailability.go%23L63" target="_blank">it behaves</a>. Based on that, let&rsquo;s define our SQL schema.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-mysql" data-lang="mysql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="o">`</span><span class="n">hours</span><span class="o">`</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">(</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">hour</span><span class="w"> </span><span class="kt">TIMESTAMP</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="no">NULL</span><span class="p">,</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="n">availability</span><span class="w"> </span><span class="kt">ENUM</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;available&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;not_available&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;training_scheduled&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="no">NULL</span><span class="p">,</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="p">(</span><span class="n">hour</span><span class="p">)</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">);</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Fsql%2Fschema.sql%23L1" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/sql/schema.sql</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Fsql%2Fschema.sql%23L1" target="_blank">Full source</a> </div> <p>When I work with SQL databases, my default choices are:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjmoiron%2Fsqlx" target="_blank">sqlx</a>: for simpler data models, it provides useful functions that help with using structs to unmarshal query results. When the schema is more complex because of relations and multiple models, it&rsquo;s time for&hellip;</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvolatiletech%2Fsqlboiler" target="_blank">SQLBoiler</a>: excellent for more complex models with many fields and relations, it&rsquo;s based on code generation. Thanks to that, it&rsquo;s very fast, and you don&rsquo;t need to be afraid that you passed an invalid <code>interface{}</code> instead of another <code>interface{}</code>. &#x1f609; Generated code is based on the SQL schema, so you can avoid a lot of duplication.</li> </ul> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <strong>Update (2026):</strong> SQLBoiler is now in maintenance mode. It still receives updates, and we continue using it in existing projects, but our default choice for new projects is <a href="proxy.php?url=https%3A%2F%2Fsqlc.dev%2F" target="_blank">sqlc</a>. Like SQLBoiler, sqlc generates type-safe Go code from SQL, but it&rsquo;s under more active development and has a growing ecosystem. Check out our <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Flist-of-recommended-libraries%2F">list of recommended Go libraries</a> for more details. </p></div> </div> <p>We currently have only one table. <code>sqlx</code> will be more than enough. &#x1f609; Let&rsquo;s reflect our DB model with a &ldquo;transport type&rdquo;.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">mysqlHour</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">string</span> <span class="s">`db:&#34;id&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Hour</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> <span class="s">`db:&#34;hour&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Availability</span> <span class="kt">string</span> <span class="s">`db:&#34;availability&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_mysql_repository.go%23L17" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_mysql_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_mysql_repository.go%23L17" target="_blank">Full source</a> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> You may ask why not add the `db` attribute to `hour.Hour`. From my experience, it's better to entirely separate domain types from the database. It's easier to test, we don't duplicate validation, and it doesn't introduce a lot of boilerplate. In case of any change in the schema, we can do it just in our repository implementation, not in half of the project. Miłosz described a similar case in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fthings-to-know-about-dry%2F">Things to know about DRY</a> article. I also described that rule deeper in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%23the-third-rule---domain-needs-to-be-database-agnostic">the previous article about DDD Lite</a>. </p></div> </div> <p>How can we use this struct?</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// sqlContextGetter is an interface provided both by transaction and standard db connection </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">sqlContextGetter</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">GetContext</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">dest</span> <span class="kd">interface</span><span class="p">{},</span> <span class="nx">query</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">args</span> <span class="o">...</span><span class="kd">interface</span><span class="p">{})</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="nx">MySQLHourRepository</span><span class="p">)</span> <span class="nf">GetOrCreateHour</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">time</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">m</span><span class="p">.</span><span class="nf">getOrCreateHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">m</span><span class="p">.</span><span class="nx">db</span><span class="p">,</span> <span class="nx">time</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="nx">MySQLHourRepository</span><span class="p">)</span> <span class="nf">getOrCreateHour</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span> <span class="nx">sqlContextGetter</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">forUpdate</span> <span class="kt">bool</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">dbHour</span> <span class="o">:=</span> <span class="nx">mysqlHour</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">query</span> <span class="o">:=</span> <span class="s">&#34;SELECT * FROM `hours` WHERE `hour` = ?&#34;</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">forUpdate</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">query</span> <span class="o">+=</span> <span class="s">&#34; FOR UPDATE&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">GetContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">dbHour</span><span class="p">,</span> <span class="nx">query</span><span class="p">,</span> <span class="nx">hourTime</span><span class="p">.</span><span class="nf">UTC</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">sql</span><span class="p">.</span><span class="nx">ErrNoRows</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// in reality this date exists, even if it&#39;s not persisted </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="nx">m</span><span class="p">.</span><span class="nx">hourFactory</span><span class="p">.</span><span class="nf">NewNotAvailableHour</span><span class="p">(</span><span class="nx">hourTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;unable to get hour from db&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">availability</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">hour</span><span class="p">.</span><span class="nf">NewAvailabilityFromString</span><span class="p">(</span><span class="nx">dbHour</span><span class="p">.</span><span class="nx">Availability</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">domainHour</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">hourFactory</span><span class="p">.</span><span class="nf">UnmarshalHourFromDatabase</span><span class="p">(</span><span class="nx">dbHour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">.</span><span class="nf">Local</span><span class="p">(),</span> <span class="nx">availability</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">domainHour</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_mysql_repository.go%23L40" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_mysql_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_mysql_repository.go%23L40" target="_blank">Full source</a> </div> <p>With the SQL implementation, it&rsquo;s simple because we don&rsquo;t need to maintain backward compatibility. In previous articles, we used Firestore as our primary database. Let&rsquo;s prepare an implementation based on that, keeping backward compatibility.</p> <h3 id="firestore-implementation">Firestore implementation</h3> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Since writing this article, our recommendation has changed: <strong>just use PostgreSQL</strong>. It&rsquo;s easier to operate and simpler to migrate between cloud providers if needed. See the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%23state-of-this-article-in-2026">State of this article in 2026</a> note in our first post for more context. </p></div> </div> <p>When you want to refactor a legacy application, abstracting the database may be a good starting point.</p> <p>Sometimes, applications are built in a database-centric way. In our case, it&rsquo;s an HTTP Response-centric approach &#x1f609;: our database models are based on Swagger-generated models. In other words, our data models are based on Swagger data models that are returned by the API. Does it stop us from abstracting the database? Of course not! It will just need some extra code to handle unmarshaling.</p> <p><strong>With the Domain-First approach, our database model would be much better, like in the SQL implementation.</strong> But we are where we are. Let&rsquo;s cut this old legacy step by step. I also have a feeling that CQRS will help us with that. &#x1f609;</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>In practice, a migration of the data may be simple, as long as no other services are integrated directly via the database.</p> <p>Unfortunately, it&rsquo;s an optimistic assumption when we work with a legacy response/database-centric or CRUD service&hellip;</p> </p></div> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">f</span> <span class="nx">FirestoreHourRepository</span><span class="p">)</span> <span class="nf">GetOrCreateHour</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">time</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">date</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">f</span><span class="p">.</span><span class="nf">getDateDTO</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="c1">// getDateDTO should be used both for transactional and non transactional query, </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// the best way for that is to use closure </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kd">func</span><span class="p">()</span> <span class="p">(</span><span class="nx">doc</span> <span class="o">*</span><span class="nx">firestore</span><span class="p">.</span><span class="nx">DocumentSnapshot</span><span class="p">,</span> <span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">f</span><span class="p">.</span><span class="nf">documentRef</span><span class="p">(</span><span class="nx">time</span><span class="p">).</span><span class="nf">Get</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">hourFromDb</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">f</span><span class="p">.</span><span class="nf">domainHourFromDateDTO</span><span class="p">(</span><span class="nx">date</span><span class="p">,</span> <span class="nx">time</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">hourFromDb</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_firestore_repository.go%23L31" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_firestore_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_firestore_repository.go%23L31" target="_blank">Full source</a> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// for now we are keeping backward comparability, because of that it&#39;s a bit messy and overcomplicated </span></span></span><span class="line"><span class="cl"><span class="c1">// todo - we will clean it up later with CQRS :-) </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="p">(</span><span class="nx">f</span> <span class="nx">FirestoreHourRepository</span><span class="p">)</span> <span class="nf">domainHourFromDateDTO</span><span class="p">(</span><span class="nx">date</span> <span class="nx">Date</span><span class="p">,</span> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">firebaseHour</span><span class="p">,</span> <span class="nx">found</span> <span class="o">:=</span> <span class="nf">findHourInDateDTO</span><span class="p">(</span><span class="nx">date</span><span class="p">,</span> <span class="nx">hourTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">found</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// in reality this date exists, even if it&#39;s not persisted </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="nx">f</span><span class="p">.</span><span class="nx">hourFactory</span><span class="p">.</span><span class="nf">NewNotAvailableHour</span><span class="p">(</span><span class="nx">hourTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">availability</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">mapAvailabilityFromDTO</span><span class="p">(</span><span class="nx">firebaseHour</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">f</span><span class="p">.</span><span class="nx">hourFactory</span><span class="p">.</span><span class="nf">UnmarshalHourFromDatabase</span><span class="p">(</span><span class="nx">firebaseHour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">.</span><span class="nf">Local</span><span class="p">(),</span> <span class="nx">availability</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_firestore_repository.go%23L120" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_firestore_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_firestore_repository.go%23L120" target="_blank">Full source</a> </div> <p>Unfortunately, the Firebase interfaces for transactional and non-transactional queries are not fully compatible. To avoid duplication, I created <code>getDateDTO</code>, which can handle this difference by passing <code>getDocumentFn</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">f</span> <span class="nx">FirestoreHourRepository</span><span class="p">)</span> <span class="nf">getDateDTO</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">getDocumentFn</span> <span class="kd">func</span><span class="p">()</span> <span class="p">(</span><span class="nx">doc</span> <span class="o">*</span><span class="nx">firestore</span><span class="p">.</span><span class="nx">DocumentSnapshot</span><span class="p">,</span> <span class="nx">err</span> <span class="kt">error</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">dateTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="p">(</span><span class="nx">Date</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">doc</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">getDocumentFn</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Code</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">==</span> <span class="nx">codes</span><span class="p">.</span><span class="nx">NotFound</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// in reality this date exists, even if it&#39;s not persisted </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">return</span> <span class="nf">NewEmptyDateDTO</span><span class="p">(</span><span class="nx">dateTime</span><span class="p">),</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Date</span><span class="p">{},</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">date</span> <span class="o">:=</span> <span class="nx">Date</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">doc</span><span class="p">.</span><span class="nf">DataTo</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">date</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Date</span><span class="p">{},</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;unable to unmarshal Date from Firestore&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">date</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_firestore_repository.go%23L97" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_firestore_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_firestore_repository.go%23L97" target="_blank">Full source</a> </div> <p>Even if some extra code is needed, it&rsquo;s not bad. And at least it&rsquo;s easy to test.</p> <h2 id="updating-the-data">Updating the data</h2> <p>As I mentioned before, it&rsquo;s critical to ensure that <strong>only one person can schedule a training in one hour</strong>. To handle that scenario, we need to use <strong>optimistic locking and transactions</strong>. Even if <em>transactions</em> is a pretty common term, let&rsquo;s ensure we&rsquo;re on the same page with <em>Optimistic Locking</em>.</p> <blockquote> <p><strong>Optimistic concurrency control</strong> assumes that many transactions can frequently complete without interfering with each other. While running, transactions use data resources without acquiring locks on those resources. Before committing, each transaction verifies that no other transaction has modified the data it has read. If the check reveals conflicting modifications, the committing transaction rolls back and can be restarted.</p> <footer> <strong></strong> <cite> <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FOptimistic_concurrency_control" title="https://en.wikipedia.org/wiki/Optimistic_concurrency_control" target="_blank">wikipedia.org</a> </cite> </footer> </blockquote> <p>Technically, transaction handling is not complicated. The biggest challenge I had was a bit different: how to manage transactions in a clean way that does not affect the rest of the application too much, is not dependent on the implementation, and is explicit and fast.</p> <p>I experimented with many ideas, like passing a transaction via <code>context.Context</code>, handling transactions at the HTTP/gRPC/message middleware level, etc. All approaches I tried had many major issues: they were a bit magical, not explicit, and slow in some cases.</p> <p>Currently, my favorite approach is based on a closure passed to the update function.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Repository</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">UpdateHour</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Frepository.go%23L8" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour/repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Frepository.go%23L8" target="_blank">Full source</a> </div> <p>The basic idea is that when we run <code>UpdateHour</code>, we need to provide an <code>updateFn</code> that can update the provided hour.</p> <p>So in practice, in one transaction we:</p> <ul> <li>get and provide all parameters for <code>updateFn</code> (<code>h *Hour</code> in our case) based on provided UUID or any other parameter (in our case <code>hourTime time.Time</code>)</li> <li>execute the closure (<code>updateFn</code> in our case)</li> <li>save return values (<code>*Hour</code> in our case, if needed we can return more)</li> <li>execute a rollback in case of an error returned from the closure</li> </ul> <p>How is it used in practice?</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="nx">GrpcServer</span><span class="p">)</span> <span class="nf">MakeHourAvailable</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">request</span> <span class="o">*</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">UpdateHourRequest</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">EmptyResponse</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingTime</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">protoTimestampToTime</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">InvalidArgument</span><span class="p">,</span> <span class="s">&#34;unable to parse time&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">g</span><span class="p">.</span><span class="nx">hourRepository</span><span class="p">.</span><span class="nf">UpdateHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">trainingTime</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nf">MakeAvailable</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">Internal</span><span class="p">,</span> <span class="nx">err</span><span class="p">.</span><span class="nf">Error</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">EmptyResponse</span><span class="p">{},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fgrpc.go%23L20" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/grpc.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fgrpc.go%23L20" target="_blank">Full source</a> </div> <p>As you can see, we get an <code>Hour</code> instance from some (unknown!) database. After that, we make this hour <code>Available</code>. If everything is fine, we save the hour by returning it. As part of <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F">the previous article</a>, <strong>all validations were moved to the domain level, so here we&rsquo;re sure that we aren&rsquo;t doing anything &ldquo;stupid&rdquo;. It also simplified this code a lot.</strong></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gi">+func (g GrpcServer) MakeHourAvailable(ctx context.Context, request *trainer.UpdateHourRequest) (*trainer.EmptyResponse, error) { </span></span></span><span class="line"><span class="cl"><span class="gi"></span><span class="gu">@ ... </span></span></span><span class="line"><span class="cl"><span class="gu"></span><span class="gd">-func (g GrpcServer) UpdateHour(ctx context.Context, req *trainer.UpdateHourRequest) (*trainer.EmptyResponse, error) { </span></span></span><span class="line"><span class="cl"><span class="gd">- trainingTime, err := grpcTimestampToTime(req.Time) </span></span></span><span class="line"><span class="cl"><span class="gd">- if err != nil { </span></span></span><span class="line"><span class="cl"><span class="gd">- return nil, status.Error(codes.InvalidArgument, &#34;unable to parse time&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- date, err := g.db.DateModel(ctx, trainingTime) </span></span></span><span class="line"><span class="cl"><span class="gd">- if err != nil { </span></span></span><span class="line"><span class="cl"><span class="gd">- return nil, status.Error(codes.Internal, fmt.Sprintf(&#34;unable to get data model: %s&#34;, err)) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- hour, found := date.FindHourInDate(trainingTime) </span></span></span><span class="line"><span class="cl"><span class="gd">- if !found { </span></span></span><span class="line"><span class="cl"><span class="gd">- return nil, status.Error(codes.NotFound, fmt.Sprintf(&#34;%s hour not found in schedule&#34;, trainingTime)) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- if req.HasTrainingScheduled &amp;&amp; !hour.Available { </span></span></span><span class="line"><span class="cl"><span class="gd">- return nil, status.Error(codes.FailedPrecondition, &#34;hour is not available for training&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- if req.Available &amp;&amp; req.HasTrainingScheduled { </span></span></span><span class="line"><span class="cl"><span class="gd">- return nil, status.Error(codes.FailedPrecondition, &#34;cannot set hour as available when it have training scheduled&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- if !req.Available &amp;&amp; !req.HasTrainingScheduled { </span></span></span><span class="line"><span class="cl"><span class="gd">- return nil, status.Error(codes.FailedPrecondition, &#34;cannot set hour as unavailable when it have no training scheduled&#34;) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- hour.Available = req.Available </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- if hour.HasTrainingScheduled &amp;&amp; hour.HasTrainingScheduled == req.HasTrainingScheduled { </span></span></span><span class="line"><span class="cl"><span class="gd">- return nil, status.Error(codes.FailedPrecondition, fmt.Sprintf(&#34;hour HasTrainingScheduled is already %t&#34;, hour.HasTrainingScheduled)) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- hour.HasTrainingScheduled = req.HasTrainingScheduled </span></span></span><span class="line"><span class="cl"><span class="gd">- if err := g.db.SaveModel(ctx, date); err != nil { </span></span></span><span class="line"><span class="cl"><span class="gd">- return nil, status.Error(codes.Internal, fmt.Sprintf(&#34;failed to save date: %s&#34;, err)) </span></span></span><span class="line"><span class="cl"><span class="gd">- } </span></span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- return &amp;trainer.EmptyResponse{}, nil </span></span></span><span class="line"><span class="cl"><span class="gd">-} </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F0249977c58a310d343ca2237c201b9ba016b148e%23diff-5e57cb39050b6e252711befcf6fb0a89L20" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/commit/0249977c58a310d343ca2237c201b9ba016b148e#diff-5e57cb39050b6e252711befcf6fb0a89L20</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F0249977c58a310d343ca2237c201b9ba016b148e%23diff-5e57cb39050b6e252711befcf6fb0a89L20" target="_blank">Full source</a> </div> <p>In our case, from <code>updateFn</code> we return only <code>(*Hour, error)</code>: <strong>you can return more values if needed.</strong> You can return event sourcing events, read models, etc.</p> <p>We could also, in theory, use the same <code>hour.*Hour</code> that we provide to <code>updateFn</code>. I decided not to do that. Using the returned value gives us more flexibility (we can replace it with a different instance of <code>hour.*Hour</code> if we want).</p> <p>It&rsquo;s also fine to have multiple <code>UpdateHour</code>-like functions created with extra data to save. Under the hood, the implementation should reuse the same code without a lot of duplication.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Repository</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">UpdateHour</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nf">UpdateHourWithMagic</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Hour</span><span class="p">,</span> <span class="o">*</span><span class="nx">Magic</span><span class="p">,</span> <span class="kt">error</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>How to implement it now?</p> <h4 id="in-memory-transactions-implementation">In-memory transactions implementation</h4> <p>The memory implementation is again the simplest one. &#x1f609; We need to get the current value, execute the closure, and save the result.</p> <p>What&rsquo;s essential is that in the map, we store a copy instead of a pointer. Thanks to that, we&rsquo;re sure that without the &ldquo;commit&rdquo; (<code>m.hours[hourTime] = *updatedHour</code>) our values are not saved. We&rsquo;ll double-check this in tests.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="o">*</span><span class="nx">MemoryHourRepository</span><span class="p">)</span> <span class="nf">UpdateHour</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">_</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">),</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">m</span><span class="p">.</span><span class="nx">lock</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">defer</span> <span class="nx">m</span><span class="p">.</span><span class="nx">lock</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">currentHour</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">m</span><span class="p">.</span><span class="nf">getOrCreateHour</span><span class="p">(</span><span class="nx">hourTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">updatedHour</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">updateFn</span><span class="p">(</span><span class="nx">currentHour</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">m</span><span class="p">.</span><span class="nx">hours</span><span class="p">[</span><span class="nx">hourTime</span><span class="p">]</span> <span class="p">=</span> <span class="o">*</span><span class="nx">updatedHour</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_memory_repository.go%23L48" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_memory_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_memory_repository.go%23L48" target="_blank">Full source</a> </div> <h4 id="firestore-transactions-implementation">Firestore transactions implementation</h4> <p>The Firestore implementation is a bit more complex, but again, it&rsquo;s related to backward compatibility. Functions <code>getDateDTO</code>, <code>domainHourFromDateDTO</code>, <code>updateHourInDataDTO</code> could probably be skipped if our data model were better. Another reason not to use a Database-centric/Response-centric approach!</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">f</span> <span class="nx">FirestoreHourRepository</span><span class="p">)</span> <span class="nf">UpdateHour</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">),</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">f</span><span class="p">.</span><span class="nx">firestoreClient</span><span class="p">.</span><span class="nf">RunTransaction</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">transaction</span> <span class="o">*</span><span class="nx">firestore</span><span class="p">.</span><span class="nx">Transaction</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">dateDocRef</span> <span class="o">:=</span> <span class="nx">f</span><span class="p">.</span><span class="nf">documentRef</span><span class="p">(</span><span class="nx">hourTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">firebaseDate</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">f</span><span class="p">.</span><span class="nf">getDateDTO</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="c1">// getDateDTO should be used both for transactional and non transactional query, </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// the best way for that is to use closure </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kd">func</span><span class="p">()</span> <span class="p">(</span><span class="nx">doc</span> <span class="o">*</span><span class="nx">firestore</span><span class="p">.</span><span class="nx">DocumentSnapshot</span><span class="p">,</span> <span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">transaction</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="nx">dateDocRef</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">hourFromDB</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">f</span><span class="p">.</span><span class="nf">domainHourFromDateDTO</span><span class="p">(</span><span class="nx">firebaseDate</span><span class="p">,</span> <span class="nx">hourTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">updatedHour</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">updateFn</span><span class="p">(</span><span class="nx">hourFromDB</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;unable to update hour&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nf">updateHourInDataDTO</span><span class="p">(</span><span class="nx">updatedHour</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">firebaseDate</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">transaction</span><span class="p">.</span><span class="nf">Set</span><span class="p">(</span><span class="nx">dateDocRef</span><span class="p">,</span> <span class="nx">firebaseDate</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;firestore transaction failed&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_firestore_repository.go%23L52" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_firestore_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_firestore_repository.go%23L52" target="_blank">Full source</a> </div> <p>As you can see, we get <code>*hour.Hour</code>, call <code>updateFn</code>, and save the results inside <code>RunTransaction</code>.</p> <p><strong>Despite some extra complexity, this implementation is still clear, easy to understand and test.</strong></p> <h4 id="mysql-transactions-implementation">MySQL transactions implementation</h4> <p>Let&rsquo;s compare it with the MySQL implementation, where we designed models in a better way. Even though the implementation approach is similar, the biggest difference is how transactions are handled.</p> <p>In the SQL driver, the transaction is represented by <code>*db.Tx</code>. We use this particular object to call all queries and do rollback and commit. To ensure that we don&rsquo;t forget about closing the transaction, we do rollback and commit in the <code>defer</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="nx">MySQLHourRepository</span><span class="p">)</span> <span class="nf">UpdateHour</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">),</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="p">(</span><span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">tx</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">m</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">Beginx</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;unable to start transaction&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Defer is executed on function just before exit. </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// With defer, we are always sure that we will close our transaction properly. </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="k">defer</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// In `UpdateHour` we are using named return - `(err error)`. </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// Thanks to that that can check if function exits with error. </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// Even if function exits without error, commit still can return error. </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// In that case we can override nil to err `err = m.finish...`. </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">m</span><span class="p">.</span><span class="nf">finishTransaction</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">tx</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">existingHour</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">m</span><span class="p">.</span><span class="nf">getOrCreateHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">tx</span><span class="p">,</span> <span class="nx">hourTime</span><span class="p">,</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">updatedHour</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">updateFn</span><span class="p">(</span><span class="nx">existingHour</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">m</span><span class="p">.</span><span class="nf">upsertHour</span><span class="p">(</span><span class="nx">tx</span><span class="p">,</span> <span class="nx">updatedHour</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_mysql_repository.go%23L82" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_mysql_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_mysql_repository.go%23L82" target="_blank">Full source</a> </div> <p>In this case, we also get the hour by passing <code>forUpdate == true</code> to <code>getOrCreateHour</code>. This flag adds the <code>FOR UPDATE</code> statement to our query. The <code>FOR UPDATE</code> statement is critical because without it, we would allow parallel transactions to change the hour.</p> <blockquote> <p><p><code>SELECT ... FOR UPDATE</code></p> <p>For index records the search encounters, locks the rows and any associated index entries, the same as if you issued an UPDATE statement for those rows. Other transactions are blocked from updating those rows.</p> </p> <footer> <strong></strong> <cite> <a href="proxy.php?url=https%3A%2F%2Fdev.mysql.com%2Fdoc%2Frefman%2F8.0%2Fen%2Finnodb-locking-reads.html" title="https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html" target="_blank">dev.mysql.com</a> </cite> </footer> </blockquote> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="nx">MySQLHourRepository</span><span class="p">)</span> <span class="nf">getOrCreateHour</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span> <span class="nx">sqlContextGetter</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">forUpdate</span> <span class="kt">bool</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">dbHour</span> <span class="o">:=</span> <span class="nx">mysqlHour</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">query</span> <span class="o">:=</span> <span class="s">&#34;SELECT * FROM `hours` WHERE `hour` = ?&#34;</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">forUpdate</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">query</span> <span class="o">+=</span> <span class="s">&#34; FOR UPDATE&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_mysql_repository.go%23L48" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_mysql_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_mysql_repository.go%23L48" target="_blank">Full source</a> </div> <p>I never sleep well when I don&rsquo;t have an automated test for code like that. Let&rsquo;s address it later. &#x1f609;</p> <p><code>finishTransaction</code> is executed when <code>UpdateHour</code> exits. When commit or rollback fails, we can also override the returned error.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// finishTransaction rollbacks transaction if error is provided. </span></span></span><span class="line"><span class="cl"><span class="c1">// If err is nil transaction is committed. </span></span></span><span class="line"><span class="cl"><span class="c1">// </span></span></span><span class="line"><span class="cl"><span class="c1">// If the rollback fails, we are using multierr library to add error about rollback failure. </span></span></span><span class="line"><span class="cl"><span class="c1">// If the commit fails, commit error is returned. </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="nx">MySQLHourRepository</span><span class="p">)</span> <span class="nf">finishTransaction</span><span class="p">(</span><span class="nx">err</span> <span class="kt">error</span><span class="p">,</span> <span class="nx">tx</span> <span class="o">*</span><span class="nx">sqlx</span><span class="p">.</span><span class="nx">Tx</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">rollbackErr</span> <span class="o">:=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">();</span> <span class="nx">rollbackErr</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">multierr</span><span class="p">.</span><span class="nf">Combine</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">rollbackErr</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">commitErr</span> <span class="o">:=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Commit</span><span class="p">();</span> <span class="nx">commitErr</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;failed to commit tx&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_mysql_repository.go%23L149" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_mysql_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_mysql_repository.go%23L149" target="_blank">Full source</a> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// upsertHour updates hour if hour already exists in the database. </span></span></span><span class="line"><span class="cl"><span class="c1">// If your doesn&#39;t exists, it&#39;s inserted. </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="p">(</span><span class="nx">m</span> <span class="nx">MySQLHourRepository</span><span class="p">)</span> <span class="nf">upsertHour</span><span class="p">(</span><span class="nx">tx</span> <span class="o">*</span><span class="nx">sqlx</span><span class="p">.</span><span class="nx">Tx</span><span class="p">,</span> <span class="nx">hourToUpdate</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">updatedDbHour</span> <span class="o">:=</span> <span class="nx">mysqlHour</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Hour</span><span class="p">:</span> <span class="nx">hourToUpdate</span><span class="p">.</span><span class="nf">Time</span><span class="p">().</span><span class="nf">UTC</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="nx">Availability</span><span class="p">:</span> <span class="nx">hourToUpdate</span><span class="p">.</span><span class="nf">Availability</span><span class="p">().</span><span class="nf">String</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">NamedExec</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">`INSERT INTO </span></span></span><span class="line"><span class="cl"><span class="s"> hours (hour, availability) </span></span></span><span class="line"><span class="cl"><span class="s"> VALUES </span></span></span><span class="line"><span class="cl"><span class="s"> (:hour, :availability) </span></span></span><span class="line"><span class="cl"><span class="s"> ON DUPLICATE KEY UPDATE </span></span></span><span class="line"><span class="cl"><span class="s"> availability = :availability`</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">updatedDbHour</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;unable to upsert hour&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_mysql_repository.go%23L122" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/hour_mysql_repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb%2Finternal%2Ftrainer%2Fhour_mysql_repository.go%23L122" target="_blank">Full source</a> </div> <h2 id="summary">Summary</h2> <p>Even if the repository approach adds a bit more code, it&rsquo;s totally worth making that investment. <strong>In practice, you may spend 5 minutes more to do that, and the investment should pay off shortly.</strong></p> <p>In this article, we&rsquo;re missing one essential part: tests. Now adding tests should be much easier, but it still may not be obvious how to do it properly.</p> <p>To not make a &ldquo;monster&rdquo; article, I will cover it in the next 1-2 weeks. 🙂 The entire diff of this refactoring, including tests, is <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F34c74e9d2cbc80160b4ff26e59818a89d10aa1eb" target="_blank">available on GitHub</a>.</p> <p>And just to remind &ndash; you can also run the application <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%23running">with one command</a> and find the entire source code on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">GitHub</a>!</p> <p>Another technique that works pretty well is Clean/Hexagonal architecture: Miłosz covers that in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-clean-architecture">Introducing Clean Architecture</a>. And to see how to combine repositories with transactions in Clean Architecture, see <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fdatabase-transactions-in-go%2F" target="_blank">Database Transactions in Go with Layered Architecture</a>.</p> Do you think this is helpful in your application? Are you already using the repository pattern differently? <strong>Let us know in the comments!</strong>Introduction to DDD Lite: When microservices in Go are not enoughhttps://threedots.tech/post/ddd-lite-in-go-introduction/Wed, 01 Jul 2020 00:00:00 +0200https://threedots.tech/post/ddd-lite-in-go-introduction/<p>When I started working in Go, the community did not look favorably on techniques like DDD (Domain-Driven Design) and Clean Architecture. I heard multiple times: <em>&ldquo;Don&rsquo;t do Java in Golang!&rdquo;, &ldquo;I&rsquo;ve seen that in Java, please don&rsquo;t!&rdquo;</em>.</p> <p>At the time, I already had almost 10 years of experience in PHP and Python. I&rsquo;d seen too many bad things there. I remember all those &ldquo;Eight-thousanders&rdquo; (methods with 8k+ lines of code &#x1f609;) and applications that nobody wanted to maintain. When I checked the git history of these ugly monsters, they all looked harmless in the beginning. But over time, small, innocent problems grew more significant and more serious. <strong>I&rsquo;d also seen how DDD and Clean Architecture solved these issues.</strong></p> <p>Maybe Golang is different? Maybe writing microservices in Golang will fix this issue?</p> <h3 id="it-was-supposed-to-be-so-beautiful">It was supposed to be so beautiful</h3> <p>Now, after exchanging experience with many people and seeing a lot of codebases, my point of view is clearer than 3 years ago. Unfortunately, I&rsquo;m far from thinking that just using Go and microservices will save us from all the issues I encountered earlier. I started having flashbacks from the old, bad times.</p> <p>It&rsquo;s less visible because of relatively younger codebases. It&rsquo;s less visible because of Go&rsquo;s design. But I&rsquo;m sure that over time, we&rsquo;ll have more and more legacy Go applications that nobody wants to maintain.</p> <p>Fortunately, 3 years ago, despite the chilly reception, I didn&rsquo;t give up. I decided to try using DDD and related techniques that had worked for me previously in Go. With Miłosz, we led teams for 3 years that were all successfully using DDD, Clean Architecture, and other not-popular-enough techniques in Go. <strong>They gave us the ability to develop our applications and products at constant velocity, regardless of the code&rsquo;s age.</strong></p> <p>It was obvious from the beginning that <strong>moving patterns 1:1 from other technologies would not work</strong>. Crucially, we did not abandon idiomatic Go code and microservices architecture: they fit together perfectly!</p> <p>Today I&rsquo;d like to share with you the first, most straightforward technique: DDD Lite.</p> <h3 id="state-of-ddd-in-golang">State of DDD in Golang</h3> <p>Before sitting down to write this article, I checked a couple of articles about DDD in Go on Google. I&rsquo;ll be brutal here: they all miss the most critical points that make DDD work. <strong>If I imagined reading these articles without any DDD knowledge, I would not be encouraged to use them in my team. This superficial approach may also be why DDD is still not widely adopted in the Go community.</strong></p> <p>In this series, we try to show all essential techniques in the most pragmatic way. Before describing any patterns, we start with a question: what does this give us? It&rsquo;s an excellent way to challenge our current thinking.</p> <p>I&rsquo;m sure we can change how the Go community receives these techniques. We believe they are the best way to implement complex business projects. <strong>I believe we will help establish Go as a great language for building not only infrastructure but also business software.</strong></p> <h3 id="you-need-to-go-slow-to-go-fast">You need to go slow, to go fast</h3> <p>It may be tempting to implement your project in the simplest way. It&rsquo;s even more tempting when you feel pressure from &ldquo;the top&rdquo;. We&rsquo;re using microservices, though, right? If needed, we&rsquo;ll just rewrite the service? I&rsquo;ve heard that story many times, and it rarely had a happy ending. &#x1f609; <strong>It&rsquo;s true that taking shortcuts saves time. But only in the short term.</strong></p> <p>Consider the example of tests of any kind. You can skip writing tests at the beginning of the project. You&rsquo;ll obviously save some time, and management will be happy. <strong>The calculation seems simple: the project was delivered faster.</strong></p> <p>But this shortcut is not worthwhile in the long term. As the project grows, your team will become afraid of making any changes. In the end, you&rsquo;ll spend more total time than if you had implemented tests from the beginning. <strong>You will slow down in the long term because you sacrificed quality for a quick performance boost at the start.</strong> On the other hand, if a project is not critical and needs to ship fast, you can skip tests. It should be a pragmatic decision, not just <em>&ldquo;we know better, and we don&rsquo;t create bugs&rdquo;</em>.</p> <p>The case is the same for DDD. When you use DDD, you need a bit more time in the beginning, but the long-term savings are enormous. However, not every project is complex enough to warrant advanced techniques like DDD.</p> <p><strong>There is no quality vs. speed tradeoff in the long run. If you want to go fast in the long term, you need to maintain high quality.</strong></p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="857" height="406" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fddd-lite%2Fquality-fowler_huf6a9cee687d22c08ae4dced93101e0b2_62824_857x406_resize_q80_h2_lanczos_3.webp" alt="Is quality worth cost?" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fddd-lite%5C%2Fquality-fowler_huf6a9cee687d22c08ae4dced93101e0b2_62824_857x406_resize_lanczos_3.png"" /> <div class="code-ref"> <i>'Is High Quality Software Worth the Cost?'</i> from <a target='_blank' href="proxy.php?url=https%3A%2F%2Fmartinfowler.com%2Farticles%2Fis-quality-worth-cost.html">martinfowler.com</a> </div></p> <h3 id="thats-great-but-do-you-have-any-evidence-it-works">That&rsquo;s great, but do you have any evidence it works?</h3> <p>If you asked me this question two years ago, I would have said: <em>&ldquo;Well, I feel that it works better!&rdquo;</em>. But just trusting my words may not be enough. &#x1f609; <strong>Many tutorials show dumb ideas and claim they work without any evidence. Let&rsquo;s not trust them blindly!</strong></p> <p>Just a reminder: if someone has a couple thousand Twitter followers, <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FAuthority_bias" target="_blank">that alone is not a reason to trust them</a>!</p> <p>Fortunately, 2 years ago <a href="proxy.php?url=https%3A%2F%2Fwww.amazon.com%2FAccelerate-Software-Performing-Technology-Organizations%2Fdp%2F1942788339" target="_blank"><em>Accelerate: The Science of Lean Software and DevOps: Building and Scaling High Performing Technology Organizations</em></a> was released. In brief, this book describes what factors affect development teams&rsquo; performance. But the reason this book became famous is that it&rsquo;s not just a set of unvalidated ideas: <strong>it&rsquo;s based on scientific research.</strong></p> <p><strong>I was most interested in the part showing what allows teams to be top performers.</strong> This book shows some obvious facts, like introducing DevOps, CI/CD, and loosely coupled architecture, which are all essential factors in high-performing teams.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If things like DevOps and CI/CD are not obvious to you, you can start with these books: <a href="proxy.php?url=https%3A%2F%2Fwww.amazon.com%2FPhoenix-Project-DevOps-Helping-Business%2Fdp%2F0988262592" target="_blank">The Phoenix Project</a> and <a href="proxy.php?url=https%3A%2F%2Fwww.amazon.com%2FDevOps-Handbook-World-Class-Reliability-Organizations%2Fdp%2F1942788002" target="_blank">The DevOps Handbook</a>. </p></div> </div> <p>What does Accelerate tell us about top-performing teams?</p> <blockquote> <p>We found that high performance is possible with all kinds of systems, provided that systems and the teams that build and maintain them—are loosely coupled.</p> </blockquote> <blockquote> <p>This key architectural property enables teams to easily test and deploy individual components or services even as the organization and the number of systems it operates grow—that is, it allows organizations to increase their productivity as they scale.</p> </blockquote> <p>So let&rsquo;s use microservices, and we are done? I would not be writing this article if it was enough. &#x1f609;</p> <blockquote> <ul> <li>Make large-scale changes to the design of their system without depending on other teams to make changes in their systems or creating significant work for other teams</li> <li>Complete their work without communicating and coordinating with people outside their team</li> <li>Deploy and release their product or service on demand, regardless of other services it depends upon</li> <li>Do most of their testing on demand, without requiring an integrated test environment Perform deployments during normal business hours with negligible downtime</li> </ul> </blockquote> <blockquote> <p>Unfortunately, in real life, many so-called service-oriented architectures don’t permit testing and deploying services independently of each other, and thus will not enable teams to achieve higher performance.</p> </blockquote> <blockquote> <p>[&hellip;] employing the latest whizzy microservices architecture deployed on containers is no guarantee of higher performance if you ignore these characteristics. [&hellip;] To achieve these characteristics, design systems are loosely coupled — that is, can be changed and validated independently of each other.</p> </blockquote> <p><strong>Using microservices architecture and splitting services into small pieces is not enough. If done the wrong way, it adds extra complexity and slows teams down.</strong> DDD can help us here.</p> <p>I&rsquo;m mentioning DDD multiple times. What is DDD, actually?</p> <h2 id="what-is-ddd-domain-driven-design">What is DDD (Domain-Driven Design)</h2> <p>Let&rsquo;s start with the Wikipedia definition:</p> <blockquote> <p>Domain-driven design (DDD) is the concept that the structure and language of your code (class names, class methods, class variables) should match the business domain. For example, if your software processes loan applications, it might have classes such as LoanApplication and Customer, and methods such as AcceptOffer and Withdraw.</p> </blockquote> <img title="" loading="lazy" decoding="async" class="img img-center" width="800" height="450" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fddd-lite%2Fno-god-please-no_hu3a6e6ee9061d9dabb5d5af755397fa76_63870_800x450_resize_q80_h2_lanczos.webp" alt="No, God please no!" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fddd-lite%5C%2Fno-god-please-no_hu3a6e6ee9061d9dabb5d5af755397fa76_63870_800x450_resize_q80_lanczos.jpg"" /> <p>Well, it&rsquo;s not the perfect definition. &#x1f605; It&rsquo;s still missing some of the most important points.</p> <p>It&rsquo;s also worth mentioning that DDD was introduced in 2003. That&rsquo;s a pretty long time ago. Some distillation may help put DDD in the 2020 and Go context.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If you are interested in some historical context on when DDD was created, you should check <a href="proxy.php?url=https%3A%2F%2Fyoutu.be%2FdnUFEg68ESM%3Ft%3D1109" target="_blank">Tackling Complexity in the Heart of Software</a> by the DDD creator - Eric Evans </p></div> </div> <img title="" loading="lazy" decoding="async" class="img img-center" width="400" height="534" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fddd-lite%2Feric-evans_hueeda4ad23d32101f137bb1f44992ad51_38556_400x534_resize_q80_h2_lanczos.webp" alt="Eric Evans - creator of DDD" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fddd-lite%5C%2Feric-evans_hueeda4ad23d32101f137bb1f44992ad51_38556_400x534_resize_q80_lanczos.jpg"" /> <div class="code-ref"> Eric Evans - the creator of DDD.<br>Please print this and hang over the bed to receive +10 DDD blessing. </div> <p>My simple DDD definition is: Ensure that you solve a <strong>valid problem</strong> in the <strong>optimal way</strong>. Then <strong>implement the solution so that your business stakeholders understand it without any translation from technical language</strong>.</p> <p>How to achieve that?</p> <h3 id="coding-is-a-war-to-win-you-need-a-strategy">Coding is a war, to win you need a strategy!</h3> <p>I like to say that <em>&ldquo;5 days of coding can save 15 minutes of planning&rdquo;</em>.</p> <p>Before starting to write any code, you should ensure that you&rsquo;re solving a valid problem. This may sound obvious, but from my experience, it&rsquo;s not as easy as it sounds. It&rsquo;s often the case that the solution engineers create doesn&rsquo;t actually solve the problem the business requested. A set of patterns that helps us in this area is called <strong>Strategic DDD patterns</strong>.</p> <p>From my experience, DDD Strategic Patterns are often skipped. The reason is simple: we&rsquo;re all developers, and we like to write code rather than talk to the <em>&ldquo;business people&rdquo;</em>. &#x1f609; Unfortunately, this approach of staying closed in a basement without talking to any business people has many downsides. Lack of trust from the business, lack of knowledge of how the system works (from both business and engineering sides), solving the wrong problems: these are just some of the most common issues.</p> <p>The good news is that in most cases, this is caused by a lack of proper techniques like Event Storming. They can benefit both sides. What&rsquo;s also surprising is that talking to the business may be one of the most enjoyable parts of the work!</p> <p>That said, we&rsquo;ll start with patterns that apply to the code. They can give us <strong>some</strong> advantages of DDD. They&rsquo;ll also be useful for you sooner. <strong>Without Strategic patterns, I&rsquo;d say you&rsquo;ll only get 30% of the advantages that DDD can offer. We&rsquo;ll return to Strategic patterns in the next articles.</strong></p> <h2 id="ddd-lite-in-go">DDD Lite in Go</h2> <p>After a pretty long introduction, it&rsquo;s finally time to touch some code! In this article, we&rsquo;ll cover some basics of <strong>Tactical Domain-Driven Design patterns in Go</strong>. Keep in mind that this is just the beginning. We&rsquo;ll need a few more articles to cover the entire topic.</p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/ddd-lite-in-go-introduction/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;ddd;domain-driven design"> <input type="hidden" name="fields[blog_series]" value="Modern Business Software in Go"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <p>One of the most crucial parts of Tactical DDD is reflecting the domain logic directly in the code.</p> <p>But that&rsquo;s still a vague definition, and it&rsquo;s not needed at this point. I also don&rsquo;t want to start by describing what <em>Value Objects, Entities, and Aggregates</em> are. Let&rsquo;s start with practical examples instead.</p> <h3 id="wild-workouts">Wild workouts</h3> <p>I haven&rsquo;t mentioned yet that we created an entire application called Wild Workouts especially for these articles. Interestingly, we introduced some subtle issues in this application to have something to refactor. If Wild Workouts looks like an application you&rsquo;re working on, you&rsquo;d better stay with us for a moment. &#x1f609;</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="this-is-not-just-another-article-with-random-code-snippets">This is not just another article with random code snippets.</h4> <p>This post is part of a bigger series where we show how to build <strong>Go applications that are easy to develop, maintain, and fun to work with in the long term.</strong> We are doing it by sharing proven techniques based on many experiments we did with teams we lead and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%3Futm_source%3Dabout-wild-workouts%23thats-great-but-do-you-have-any-evidence-it-works">scientific research</a>.</p> <p>You can learn these patterns by building with us a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%3Futm_source%3Dabout-wild-workouts%23what-wild-workouts-can-do">fully functional</a> example Go web application &ndash; <strong>Wild Workouts</strong>.</p> <p>We did one thing differently &ndash; <strong>we included some subtle issues to the initial Wild Workouts implementation</strong>. Have we lost our minds to do that? Not yet. 😉 These issues are common for many Go projects. <strong>In the long term, these small issues become critical and stop adding new features.</strong></p> <p><strong>It&rsquo;s one of the essential skills of a senior or lead developer; you always need to keep long-term implications in mind.</strong></p> <p>We will fix them by <strong>refactoring</strong> Wild Workouts. In that way, you will quickly understand the techniques we share.</p> <p>Do you know that feeling after reading an article about some technique and trying implement it only to be blocked by some issues skipped in the guide? Cutting these details makes articles shorter and increases page views, but this is not our goal. Our goal is to create content that provides enough know-how to apply presented techniques. If you did not read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">previous articles from the series</a> yet, we highly recommend doing that.</p> <p>We believe that in some areas, there are no shortcuts. If you want to build complex applications in a fast and efficient way, you need to spend some time learning that. If it was simple, we wouldn&rsquo;t have large amounts of scary legacy code.</p> <p>Here&rsquo;s <strong><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">the full list of 14 articles</a></strong> released so far.</p> <p><strong>The full source code</strong> of Wild Workouts is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%3Futm_source%3Dabout-wild-workouts" target="_blank">GitHub</a>. Don&rsquo;t forget to leave a star for our project! ⭐</p> </p></div> </div> <h3 id="refactoring-of-trainer-service">Refactoring of <code>trainer</code> service</h3> <p>The first (micro)service that we&rsquo;ll refactor is <code>trainer</code>. We&rsquo;ll leave other services untouched for now and return to them later.</p> <p>This service is responsible for keeping the trainer schedule and ensuring that we can have only one training scheduled in one hour. It also keeps the information about available hours (trainer&rsquo;s schedule).</p> <p>The initial implementation was not the best. Even though there&rsquo;s not a lot of logic, some parts of the code started to get messy. I also have a gut feeling, based on experience, that over time it will get worse. &#x1f609;</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="nx">GrpcServer</span><span class="p">)</span> <span class="nf">UpdateHour</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">req</span> <span class="o">*</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">UpdateHourRequest</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">EmptyResponse</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingTime</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">grpcTimestampToTime</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">InvalidArgument</span><span class="p">,</span> <span class="s">&#34;unable to parse time&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">date</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">g</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">DateModel</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">trainingTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">Internal</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;unable to get data model: %s&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">hour</span><span class="p">,</span> <span class="nx">found</span> <span class="o">:=</span> <span class="nx">date</span><span class="p">.</span><span class="nf">FindHourInDate</span><span class="p">(</span><span class="nx">trainingTime</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">found</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">NotFound</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;%s hour not found in schedule&#34;</span><span class="p">,</span> <span class="nx">trainingTime</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">req</span><span class="p">.</span><span class="nx">HasTrainingScheduled</span> <span class="o">&amp;&amp;</span> <span class="p">!</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Available</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">FailedPrecondition</span><span class="p">,</span> <span class="s">&#34;hour is not available for training&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">req</span><span class="p">.</span><span class="nx">Available</span> <span class="o">&amp;&amp;</span> <span class="nx">req</span><span class="p">.</span><span class="nx">HasTrainingScheduled</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">FailedPrecondition</span><span class="p">,</span> <span class="s">&#34;cannot set hour as available when it have training scheduled&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">req</span><span class="p">.</span><span class="nx">Available</span> <span class="o">&amp;&amp;</span> <span class="p">!</span><span class="nx">req</span><span class="p">.</span><span class="nx">HasTrainingScheduled</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">FailedPrecondition</span><span class="p">,</span> <span class="s">&#34;cannot set hour as unavailable when it have no training scheduled&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">hour</span><span class="p">.</span><span class="nx">Available</span> <span class="p">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">Available</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">HasTrainingScheduled</span> <span class="o">&amp;&amp;</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">HasTrainingScheduled</span> <span class="o">==</span> <span class="nx">req</span><span class="p">.</span><span class="nx">HasTrainingScheduled</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">FailedPrecondition</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;hour HasTrainingScheduled is already %t&#34;</span><span class="p">,</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">HasTrainingScheduled</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">hour</span><span class="p">.</span><span class="nx">HasTrainingScheduled</span> <span class="p">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">HasTrainingScheduled</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fef6056fdeb39b89009127e07600f1b9ec87e717c%2Finternal%2Ftrainer%2Fgrpc.go%23L20" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/grpc.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fef6056fdeb39b89009127e07600f1b9ec87e717c%2Finternal%2Ftrainer%2Fgrpc.go%23L20" target="_blank">Full source</a> </div> <p>Even though it&rsquo;s not the worst code ever, it reminds me of what I saw when I checked the git history of the code I worked on. I can imagine that after some time, new features will arrive and it will become much worse.</p> <p>It&rsquo;s also hard to mock dependencies here, so there are also no unit tests.</p> <h4 id="the-first-rule---reflect-your-business-logic-literally">The First Rule - reflect your business logic literally</h4> <p>While implementing your domain, you should stop thinking about structs as dummy data structures or &ldquo;ORM-like&rdquo; entities with a list of setters and getters. Instead, think about them as <strong>types with behavior.</strong></p> <p>When you talk with your business stakeholders, they say <em>&ldquo;I&rsquo;m scheduling training at 13:00&rdquo;</em>, rather than <em>&ldquo;I&rsquo;m setting the attribute state to &rsquo;training scheduled&rsquo; for hour 13:00.&rdquo;</em>.</p> <p>They also don&rsquo;t say: <em>&ldquo;you can&rsquo;t set attribute status to &rsquo;training_scheduled&rsquo;&rdquo;</em>. It is rather: <em>&ldquo;You can&rsquo;t schedule training if the hour is not available&rdquo;</em>. How to put it directly in the code?</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Hour</span><span class="p">)</span> <span class="nf">ScheduleTraining</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">h</span><span class="p">.</span><span class="nf">IsAvailable</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ErrHourNotAvailable</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">h</span><span class="p">.</span><span class="nx">availability</span> <span class="p">=</span> <span class="nx">TrainingScheduled</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Favailability.go%23L58" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour/availability.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Favailability.go%23L58" target="_blank">Full source</a> </div> <p>One question that can help us with implementation is: <em>&ldquo;Will business stakeholders understand my code without any translation of technical terms?&rdquo;</em>. You can see in that snippet that <strong>even a non-technical person will be able to understand when you can schedule training</strong>.</p> <p>This approach&rsquo;s cost is low, and it helps tackle complexity by making rules much easier to understand. Even though the change is not big, we got rid of this wall of <code>if</code>s that would become much more complicated in the future.</p> <p>We&rsquo;re also now able to easily add unit tests. What&rsquo;s good: we don&rsquo;t need to mock anything here. The tests also serve as documentation that helps us understand how <code>Hour</code> behaves.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestHour_ScheduleTraining</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">h</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">hour</span><span class="p">.</span><span class="nf">NewAvailableHour</span><span class="p">(</span><span class="nf">validTrainingHour</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">require</span><span class="p">.</span><span class="nf">NoError</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">h</span><span class="p">.</span><span class="nf">ScheduleTraining</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">assert</span><span class="p">.</span><span class="nf">True</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">h</span><span class="p">.</span><span class="nf">HasTrainingScheduled</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="nx">assert</span><span class="p">.</span><span class="nf">False</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">h</span><span class="p">.</span><span class="nf">IsAvailable</span><span class="p">())</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">TestHour_ScheduleTraining_with_not_available</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">h</span> <span class="o">:=</span> <span class="nf">newNotAvailableHour</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">assert</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">ErrHourNotAvailable</span><span class="p">,</span> <span class="nx">h</span><span class="p">.</span><span class="nf">ScheduleTraining</span><span class="p">())</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Favailability_test.go%23L41" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour/availability_test.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Favailability_test.go%23L41" target="_blank">Full source</a> </div> <p>Now, if anybody asks the question &ldquo;When can I schedule training?&rdquo;, you can quickly answer. In larger systems, the answer to this kind of question is even less obvious. Multiple times I spent hours trying to find all places where some objects were used in unexpected ways. The next rule will help us with that even more.</p> <h4 id="testing-helpers">Testing Helpers</h4> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Testing helpers were introduced in the next article. They are not available in this article&rsquo;s diff. &#x1f609; </p></div> </div> <p>It&rsquo;s useful to have helpers in tests for creating our domain entities. For example: <code>newExampleTrainingWithTime</code>, <code>newCanceledTraining</code>, etc. This also makes our domain tests much more readable.</p> <p>Custom asserts, like <code>assertTrainingsEquals</code>, can also save a lot of duplication. The <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgoogle%2Fgo-cmp" target="_blank">github.com/google/go-cmp</a> library is extremely useful for comparing complex structs. It allows us to compare our domain types with private fields, skip <a href="proxy.php?url=https%3A%2F%2Fgodoc.org%2Fgithub.com%2Fgoogle%2Fgo-cmp%2Fcmp%2Fcmpopts%23IgnoreFields" target="_blank">some field validation</a>, or implement <a href="proxy.php?url=https%3A%2F%2Fgodoc.org%2Fgithub.com%2Fgoogle%2Fgo-cmp%2Fcmp%23Comparer" target="_blank">custom validation functions</a>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">assertTrainingsEquals</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">,</span> <span class="nx">tr1</span><span class="p">,</span> <span class="nx">tr2</span> <span class="o">*</span><span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmpOpts</span> <span class="o">:=</span> <span class="p">[]</span><span class="nx">cmp</span><span class="p">.</span><span class="nx">Option</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmpRoundTimeOpt</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmp</span><span class="p">.</span><span class="nf">AllowUnexported</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">training</span><span class="p">.</span><span class="nx">UserType</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="nx">training</span><span class="p">.</span><span class="nx">Training</span><span class="p">{},</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">assert</span><span class="p">.</span><span class="nf">True</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">t</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmp</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">tr1</span><span class="p">,</span> <span class="nx">tr2</span><span class="p">,</span> <span class="nx">cmpOpts</span><span class="o">...</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmp</span><span class="p">.</span><span class="nf">Diff</span><span class="p">(</span><span class="nx">tr1</span><span class="p">,</span> <span class="nx">tr2</span><span class="p">,</span> <span class="nx">cmpOpts</span><span class="o">...</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>It&rsquo;s also a good idea to provide a <code>Must</code> version of frequently used constructors, for example <code>MustNewUser</code>. Unlike normal constructors, these panic if parameters are not valid (for tests, that&rsquo;s not a problem).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewUser</span><span class="p">(</span><span class="nx">userUUID</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">userType</span> <span class="nx">UserType</span><span class="p">)</span> <span class="p">(</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">userUUID</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">User</span><span class="p">{},</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;missing user UUID&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">userType</span><span class="p">.</span><span class="nf">IsZero</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">User</span><span class="p">{},</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;missing user type&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">User</span><span class="p">{</span><span class="nx">userUUID</span><span class="p">:</span> <span class="nx">userUUID</span><span class="p">,</span> <span class="nx">userType</span><span class="p">:</span> <span class="nx">userType</span><span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">MustNewUser</span><span class="p">(</span><span class="nx">userUUID</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">userType</span> <span class="nx">UserType</span><span class="p">)</span> <span class="nx">User</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">u</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">NewUser</span><span class="p">(</span><span class="nx">userUUID</span><span class="p">,</span> <span class="nx">userType</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">u</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h4 id="the-second-rule-always-keep-a-valid-state-in-the-memory">The Second Rule: always keep a valid state in the memory</h4> <blockquote> <p>I recognize that my code will be used in ways I cannot anticipate, in ways it was not designed, and for longer than it was ever intended.</p> <footer> <strong></strong> <cite> <a href="proxy.php?url=https%3A%2F%2Fruggedsoftware.org%2F" title="https://ruggedsoftware.org/" target="_blank">The Rugged Manifesto</a> </cite> </footer> </blockquote> <p>The world would be better if everyone took this quote into account. I&rsquo;m not without fault here either. &#x1f609;</p> <p>From my observation, when you&rsquo;re sure that the object you use is always valid, it helps avoid a lot of <code>if</code>s and bugs. You&rsquo;ll also feel much more confident knowing that you can&rsquo;t do anything stupid with the current code.</p> <p>I have many flashbacks of being afraid to make changes because I wasn&rsquo;t sure of the side effects. <strong>Developing new features is much slower without confidence that you&rsquo;re using the code correctly!</strong></p> <p>Our goal is to do validation in only one place (good DRY) and ensure that nobody can change the internal state of <code>Hour</code>. The only public API of the object should be methods describing behaviors. No dumb getters and setters! We also need to put our types in a separate package and make all attributes private.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Hour</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">hour</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">availability</span> <span class="nx">Availability</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewAvailableHour</span><span class="p">(</span><span class="nx">hour</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">validateTime</span><span class="p">(</span><span class="nx">hour</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">Hour</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">hour</span><span class="p">:</span> <span class="nx">hour</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">availability</span><span class="p">:</span> <span class="nx">Available</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Fhour.go%23L9" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour/hour.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Fhour.go%23L9" target="_blank">Full source</a> </div> <p>We should also ensure that we&rsquo;re not breaking any rules inside our type.</p> <p>Bad example:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">h</span> <span class="o">:=</span> <span class="nx">hour</span><span class="p">.</span><span class="nf">NewAvailableHour</span><span class="p">(</span><span class="s">&#34;13:00&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">h</span><span class="p">.</span><span class="nf">HasTrainingScheduled</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">h</span><span class="p">.</span><span class="nf">SetState</span><span class="p">(</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Available</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;unable to cancel training&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Good example:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Hour</span><span class="p">)</span> <span class="nf">CancelTraining</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">!</span><span class="nx">h</span><span class="p">.</span><span class="nf">HasTrainingScheduled</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">ErrNoTrainingScheduled</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">h</span><span class="p">.</span><span class="nx">availability</span> <span class="p">=</span> <span class="nx">Available</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="nx">h</span> <span class="o">:=</span> <span class="nx">hour</span><span class="p">.</span><span class="nf">NewAvailableHour</span><span class="p">(</span><span class="s">&#34;13:00&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nf">CancelTraining</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h4 id="the-third-rule---domain-needs-to-be-database-agnostic">The Third Rule - domain needs to be database agnostic</h4> <p>There are multiple schools of thought here. Some say it&rsquo;s fine to have the domain impacted by the database client. From our experience, keeping the domain strictly free of any database influence works best.</p> <p>The most important reasons are:</p> <ul> <li>domain types are not shaped by the database solution used: they should be shaped only by business rules</li> <li>we can store data in the database in a more optimal way</li> <li>because of Go&rsquo;s design and lack of &ldquo;magic&rdquo; like annotations, ORMs or any database solutions have an even more significant impact</li> </ul> <div class="notice tip"> <div class="notice-head"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillrule="evenodd" cliprule="evenodd" d="M12 0c6.6274.0 12 5.37258 12 12 0 6.6274-5.3726 12-12 12C5.37258 24 0 18.6274.0 12 0 5.37258 5.37258.0 12 0zm0 2.4C6.69807 2.4 2.4 6.69807 2.4 12c0 5.3019 4.29807 9.6 9.6 9.6 5.3019.0 9.6-4.2981 9.6-9.6.0-5.30193-4.2981-9.6-9.6-9.6zm3.9515 5.15147L9.6 13.9029 8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515c-.46863.4686-.46863 1.2284.0 1.697l2.4 2.4C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485l7.2-7.19997C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147c-.468599999999999-.46863-1.2284-.46863-1.697.0z" fill="currentcolor"></path> </svg> <p>Tip</p></div> <div class="notice-body"> <p> <h4 id="domain-first-approach">Domain-First approach</h4> <p>If the project is complex enough, we can spend 2-4 weeks working on the domain layer with just an in-memory database implementation. In that case, we can explore the idea deeper and defer the decision about which database to choose. All our implementation is based on unit tests.</p> <p>We&rsquo;ve tried this approach several times, and it always worked nicely. It&rsquo;s also a good idea to set a timebox here to avoid spending too much time.</p> <p>Keep in mind that this approach requires a good relationship and a lot of trust from the business! <strong>If your relationship with business is far from good, Strategic DDD patterns will improve it. Been there, done that!</strong></p> </p> </div> </div> <p>To keep this article from getting too long, let&rsquo;s just introduce the Repository interface and assume that it works. &#x1f609; I&rsquo;ll cover this topic more in-depth in the next article.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Repository</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">GetOrCreateHour</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">time</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">UpdateHour</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">hourTime</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">updateFn</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Frepository.go%23L8" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour/repository.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fdomain%2Fhour%2Frepository.go%23L8" target="_blank">Full source</a> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> You may ask why `UpdateHour` has `updateFn func(h *Hour) (*Hour, error)`: we'll use that for handling transactions nicely. You can learn more in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go">the article about repositories</a>. </p></div> </div> <h4 id="using-domain-objects">Using domain objects</h4> <p>I did a small refactor of our gRPC endpoints to provide an API that is more &ldquo;behavior-oriented&rdquo; rather than <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FCreate%2C_read%2C_update_and_delete" target="_blank">CRUD</a>. It better reflects the new characteristics of the domain. From my experience, it&rsquo;s also much easier to maintain multiple small methods than one &ldquo;god&rdquo; method that allows us to update everything.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">--- a/api/protobuf/trainer.proto </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+++ b/api/protobuf/trainer.proto </span></span></span><span class="line"><span class="cl"><span class="gi"></span><span class="gu">@@ -6,7 +6,9 @@ import &#34;google/protobuf/timestamp.proto&#34;; </span></span></span><span class="line"><span class="cl"><span class="gu"></span> </span></span><span class="line"><span class="cl"> service TrainerService { </span></span><span class="line"><span class="cl"> rpc IsHourAvailable(IsHourAvailableRequest) returns (IsHourAvailableResponse) {} </span></span><span class="line"><span class="cl"><span class="gd">- rpc UpdateHour(UpdateHourRequest) returns (EmptyResponse) {} </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+ rpc ScheduleTraining(UpdateHourRequest) returns (EmptyResponse) {} </span></span></span><span class="line"><span class="cl"><span class="gi">+ rpc CancelTraining(UpdateHourRequest) returns (EmptyResponse) {} </span></span></span><span class="line"><span class="cl"><span class="gi">+ rpc MakeHourAvailable(UpdateHourRequest) returns (EmptyResponse) {} </span></span></span><span class="line"><span class="cl"><span class="gi"></span> } </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> message IsHourAvailableRequest { </span></span><span class="line"><span class="cl"><span class="gu">@@ -19,9 +21,6 @@ message IsHourAvailableResponse { </span></span></span><span class="line"><span class="cl"><span class="gu"></span> </span></span><span class="line"><span class="cl"> message UpdateHourRequest { </span></span><span class="line"><span class="cl"> google.protobuf.Timestamp time = 1; </span></span><span class="line"><span class="cl"><span class="gd">- </span></span></span><span class="line"><span class="cl"><span class="gd">- bool has_training_scheduled = 2; </span></span></span><span class="line"><span class="cl"><span class="gd">- bool available = 3; </span></span></span><span class="line"><span class="cl"><span class="gd"></span> } </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> message EmptyResponse {} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F0249977c58a310d343ca2237c201b9ba016b148e%23diff-15fd9ad3f3992b0210090109b82c5594" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/commit/0249977c58a310d343ca2237c201b9ba016b148e#diff-15fd9ad3f3992b0210090109b82c5594</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F0249977c58a310d343ca2237c201b9ba016b148e%23diff-15fd9ad3f3992b0210090109b82c5594" target="_blank">Full source</a> </div> <p>The implementation is now much simpler and easier to understand. We also have no logic here: just some orchestration. Our gRPC handler now has 18 lines and no domain logic!</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="nx">GrpcServer</span><span class="p">)</span> <span class="nf">MakeHourAvailable</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">request</span> <span class="o">*</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">UpdateHourRequest</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">EmptyResponse</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingTime</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">protoTimestampToTime</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">InvalidArgument</span><span class="p">,</span> <span class="s">&#34;unable to parse time&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">g</span><span class="p">.</span><span class="nx">hourRepository</span><span class="p">.</span><span class="nf">UpdateHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">trainingTime</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">hour</span><span class="p">.</span><span class="nx">Hour</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nf">MakeAvailable</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">h</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">Internal</span><span class="p">,</span> <span class="nx">err</span><span class="p">.</span><span class="nf">Error</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">EmptyResponse</span><span class="p">{},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fgrpc.go%23L20" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/grpc.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F0249977c58a310d343ca2237c201b9ba016b148e%2Finternal%2Ftrainer%2Fgrpc.go%23L20" target="_blank">Full source</a> </div> <div class="notice tip"> <div class="notice-head"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fillrule="evenodd" cliprule="evenodd" d="M12 0c6.6274.0 12 5.37258 12 12 0 6.6274-5.3726 12-12 12C5.37258 24 0 18.6274.0 12 0 5.37258 5.37258.0 12 0zm0 2.4C6.69807 2.4 2.4 6.69807 2.4 12c0 5.3019 4.29807 9.6 9.6 9.6 5.3019.0 9.6-4.2981 9.6-9.6.0-5.30193-4.2981-9.6-9.6-9.6zm3.9515 5.15147L9.6 13.9029 8.04853 12.3515C7.5799 11.8828 6.8201 11.8828 6.35147 12.3515c-.46863.4686-.46863 1.2284.0 1.697l2.4 2.4C9.2201 16.9172 9.9799 16.9172 10.4485 16.4485l7.2-7.19997C18.1172 8.7799 18.1172 8.0201 17.6485 7.55147c-.468599999999999-.46863-1.2284-.46863-1.697.0z" fill="currentcolor"></path> </svg> <p>Tip</p></div> <div class="notice-body"> <p> <h4 id="no-more-eight-thousanders">No more Eight-thousanders</h4> <p>As I remember from the old times, many Eight-thousanders were actually HTTP controllers with a lot of domain logic.</p> <p>By hiding complexity inside our domain types and keeping the rules I described, we can prevent uncontrolled growth here.</p> </p> </div> </div> <h2 id="thats-all-for-today">That&rsquo;s all for today</h2> <p>I don&rsquo;t want to make this article too long. Let&rsquo;s go step by step!</p> <p>If you can&rsquo;t wait, the entire working diff for the refactor is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F0249977c58a310d343ca2237c201b9ba016b148e" target="_blank">GitHub</a>. In the next article, I&rsquo;ll cover one part of the diff that isn&rsquo;t explained here: repositories.</p> <p>Even though it&rsquo;s still the beginning, some simplifications in our code are already visible.</p> <p>The current implementation of the model is also not perfect: that&rsquo;s good! You&rsquo;ll never implement the perfect model from the beginning. <strong>It&rsquo;s better to be prepared to change this model easily than to waste time trying to make it perfect.</strong> After I added tests for the model and separated it from the rest of the application, I can change it without any fear.</p> <h3 id="can-i-already-put-that-i-know-ddd-to-my-cv">Can I already put that I know DDD to my CV?</h3> <p>Not yet.</p> <p>I needed 3 years after I heard about DDD to connect all the dots (this was before I heard about Go). &#x1f609; After that, I saw why all the techniques we&rsquo;ll describe in the next articles are so important. But connecting the dots requires some patience and trust that it will work. It&rsquo;s worth it! You won&rsquo;t need 3 years like me, but we&rsquo;ve currently planned about 10 articles on both strategic and tactical patterns. &#x1f609; There are still a lot of new features and parts to refactor in Wild Workouts!</p> <p>I know that nowadays many people promise you can become an expert in some area after 10 minutes with an article or video. The world would be beautiful if that were possible, but in reality, it&rsquo;s not that simple.</p> <p>Fortunately, a big part of the knowledge we share is universal and can be applied to multiple technologies, not only Go. You can treat these learnings as an investment in your career and mental health in the long term. &#x1f609; There&rsquo;s nothing better than solving the right problems without fighting unmaintainable code!</p> <p><strong>What is your experience with DDD in Go? Was it good or bad? Was it different from how we&rsquo;re doing it? Do you think DDD might be useful in your project? Please let us know in the comments!</strong></p> <p><strong>Do you have any colleagues who might be interested in this topic? Please share this article with them! Even if they don&rsquo;t work in Go. &#x1f609;</strong></p>When to avoid DRY in Gohttps://threedots.tech/post/things-to-know-about-dry/Wed, 17 Jun 2020 00:00:00 +0200https://threedots.tech/post/things-to-know-about-dry/If you&rsquo;re here for the first time, this post is part of our <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F">Business Applications in Go</a> series. Previously, we introduced <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F">Wild Workouts</a>, our example application built with modern tools but containing some subtle anti-patterns. We added them on purpose to show common pitfalls and how to avoid them. <p>In this post, we begin refactoring Wild Workouts. Previous articles will give you more context, but reading them isn&rsquo;t necessary to understand this one.</p> <h3 id="background-story">Background story</h3> <p>Meet Susan, a software engineer bored at her current job working with legacy enterprise software. Susan started looking for a new gig and found the Wild Workouts startup, which uses serverless Go microservices. This seemed fresh and modern, so after a smooth hiring process, she started her first day at the company.</p> <p>There were just a few engineers on the team, so Susan&rsquo;s onboarding was blazing fast. On her first day, she was assigned a task meant to make her familiar with the application.</p> <blockquote> <p>We need to store each user&rsquo;s last IP address. This will enable new security features in the future, like extra confirmation when logging in from a new location. For now, we just want to keep it in the database.</p> </blockquote> <p>Susan explored the application for a while, trying to understand what was happening in each service and where to add a new field for the IP address. Finally, she discovered a <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Finternal%2Fusers%2Fopenapi_types.gen.go%23L7" target="_blank"><code>User</code></a> structure that she could extend.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> // User defines model for User. </span></span><span class="line"><span class="cl"> type User struct { </span></span><span class="line"><span class="cl"> Balance int `json:&#34;balance&#34;` </span></span><span class="line"><span class="cl"> DisplayName string `json:&#34;displayName&#34;` </span></span><span class="line"><span class="cl"> Role string `json:&#34;role&#34;` </span></span><span class="line"><span class="cl"><span class="gi">+ LastIp string `json:&#34;lastIp&#34;` </span></span></span><span class="line"><span class="cl"><span class="gi"></span> } </span></span></code></pre></div><p>It didn&rsquo;t take Susan long to post her first pull request. Soon, Dave, a senior engineer, added a comment during code review.</p> <blockquote> <p>I don&rsquo;t think we&rsquo;re supposed to expose this field via the REST API.</p> </blockquote> <p>Susan was surprised since she was sure she&rsquo;d updated the database model. Confused, she asked Dave if this was the correct place to add a new field.</p> <p>Dave explained that Firestore, the application&rsquo;s database, stores documents marshaled from Go structures. So the <code>User</code> struct is compatible with both frontend responses and storage.</p> <p>&ldquo;<em>Thanks to this approach, you don&rsquo;t need to duplicate the code. It&rsquo;s enough to change the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fapi%2Fopenapi%2Fusers.yml%23L36" target="_blank">YAML definition</a> once and regenerate the file</em>&rdquo;, he said enthusiastically.</p> <p>Grateful for the tip, Susan added one more change to hide the new field from the API response.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gh">diff --git a/internal/users/http.go b/internal/users/http.go </span></span></span><span class="line"><span class="cl"><span class="gh">index 9022e5d..cd8fbdc 100644 </span></span></span><span class="line"><span class="cl"><span class="gh"></span><span class="gd">--- a/internal/users/http.go </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+++ b/internal/users/http.go </span></span></span><span class="line"><span class="cl"><span class="gi"></span><span class="gu">@@ -27,5 +30,8 @@ func (h HttpServer) GetCurrentUser(w http.ResponseWriter, r *http.Request) { </span></span></span><span class="line"><span class="cl"><span class="gu"></span> user.Role = authUser.Role </span></span><span class="line"><span class="cl"> user.DisplayName = authUser.DisplayName </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="gi">+ // Don&#39;t expose the user&#39;s last IP externally </span></span></span><span class="line"><span class="cl"><span class="gi">+ user.LastIp = nil </span></span></span><span class="line"><span class="cl"><span class="gi">+ </span></span></span><span class="line"><span class="cl"><span class="gi"></span> render.Respond(w, r, user) </span></span><span class="line"><span class="cl"> } </span></span></code></pre></div><p>One line was enough to fix this issue. That was quite a productive first day for Susan.</p> <h3 id="second-thoughts">Second thoughts</h3> <p>Even though Susan&rsquo;s solution was approved and merged, something bothered her on her commute home. <strong>Is it the right approach to keep the same struct for both API response and database model?</strong> Don&rsquo;t we risk accidentally exposing users&rsquo; private details if we keep extending it as the application grows? What if we want to change only the API response without changing the database fields?</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="4492" height="890" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fthings-to-know-about-dry%2Fdiagram-1_hu3a1ca2bf613ec34065caf73694d71f90_116069_4492x890_resize_q80_h2_lanczos.webp" alt="Original solution" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fthings-to-know-about-dry%5C%2Fdiagram-1_hu3a1ca2bf613ec34065caf73694d71f90_116069_4492x890_resize_q80_lanczos.jpg"" /> <p>What about splitting the two structures? That&rsquo;s what Susan would do at her old job, but maybe these were enterprise patterns that should not be used in a Go microservice. Also, the team seemed very rigorous about the <strong>Don&rsquo;t Repeat Yourself</strong> principle.</p> <h3 id="refactoring">Refactoring</h3> <p>The next day, Susan explained her doubts to Dave and asked for his opinion. At first, he didn&rsquo;t understand the concern and mentioned that maybe she needed to get used to the &ldquo;Go way&rdquo; of doing things.</p> <p>Susan pointed to another piece of code in Wild Workouts that used a similar ad hoc solution. She shared that, from her experience, such code can quickly get out of control.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"> <span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">GetUser</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">(),</span> <span class="nx">authUser</span><span class="p">.</span><span class="nx">UUID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">InternalError</span><span class="p">(</span><span class="s">&#34;cannot-get-user&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">user</span><span class="p">.</span><span class="nx">Role</span> <span class="p">=</span> <span class="nx">authUser</span><span class="p">.</span><span class="nx">Role</span> </span></span><span class="line"><span class="cl"> <span class="nx">user</span><span class="p">.</span><span class="nx">DisplayName</span> <span class="p">=</span> <span class="nx">authUser</span><span class="p">.</span><span class="nx">DisplayName</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">render</span><span class="p">.</span><span class="nf">Respond</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="nx">user</span><span class="p">)</span> </span></span></code></pre></div><div class="code-ref"> It seems the HTTP handler modifies the user in-place. </div> <p>Eventually, they agreed to discuss this again over a new PR. Susan prepared a refactoring proposal.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gh">diff --git a/internal/users/firestore.go b/internal/users/firestore.go </span></span></span><span class="line"><span class="cl"><span class="gh">index 7f3fca0..670bfaa 100644 </span></span></span><span class="line"><span class="cl"><span class="gh"></span><span class="gd">--- a/internal/users/firestore.go </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+++ b/internal/users/firestore.go </span></span></span><span class="line"><span class="cl"><span class="gi"></span><span class="gu">@@ -9,6 +9,13 @@ import ( </span></span></span><span class="line"><span class="cl"><span class="gu"></span> &#34;google.golang.org/grpc/status&#34; </span></span><span class="line"><span class="cl"> ) </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="gi">+type UserModel struct { </span></span></span><span class="line"><span class="cl"><span class="gi">+ Balance int </span></span></span><span class="line"><span class="cl"><span class="gi">+ DisplayName string </span></span></span><span class="line"><span class="cl"><span class="gi">+ Role string </span></span></span><span class="line"><span class="cl"><span class="gi">+ LastIP string </span></span></span><span class="line"><span class="cl"><span class="gi">+} </span></span></span><span class="line"><span class="cl"><span class="gi">+ </span></span></span><span class="line"><span class="cl"><span class="gi"></span><span class="gh">diff --git a/internal/users/http.go b/internal/users/http.go </span></span></span><span class="line"><span class="cl"><span class="gh">index 9022e5d..372b5ca 100644 </span></span></span><span class="line"><span class="cl"><span class="gh"></span><span class="gd">--- a/internal/users/http.go </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+++ b/internal/users/http.go </span></span></span><span class="line"><span class="cl"><span class="gi"></span><span class="gu">@@ -1,6 +1,7 @@ </span></span></span><span class="line"><span class="cl"><span class="gu"></span><span class="gd">- user.Role = authUser.Role </span></span></span><span class="line"><span class="cl"><span class="gd">- user.DisplayName = authUser.DisplayName </span></span></span><span class="line"><span class="cl"><span class="gd"></span> </span></span><span class="line"><span class="cl"><span class="gd">- render.Respond(w, r, user) </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+ userResponse := User{ </span></span></span><span class="line"><span class="cl"><span class="gi">+ DisplayName: authUser.DisplayName, </span></span></span><span class="line"><span class="cl"><span class="gi">+ Balance: user.Balance, </span></span></span><span class="line"><span class="cl"><span class="gi">+ Role: authUser.Role, </span></span></span><span class="line"><span class="cl"><span class="gi">+ } </span></span></span><span class="line"><span class="cl"><span class="gi">+ </span></span></span><span class="line"><span class="cl"><span class="gi">+ render.Respond(w, r, userResponse) </span></span></span><span class="line"><span class="cl"><span class="gi"></span> } </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F14d9e7badcf5a91811059d377cfa847ec7b4592f" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/commit/14d9e7badcf5a91811059d377cfa847ec7b4592f</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fcommit%2F14d9e7badcf5a91811059d377cfa847ec7b4592f" target="_blank">Full source</a> </div> <p>This time, Susan didn&rsquo;t touch the OpenAPI definition. After all, she wasn&rsquo;t supposed to change the REST API. Instead, she manually created another structure, just like <code>User</code>, but exclusive to the database model. Then she extended it with a new field.</p> <p>The new solution is a bit longer in lines of code, but it removed coupling between the REST API and the database layer (all without introducing another microservice). The next time someone wants to add a field, they can do it by updating the proper structure.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="4789" height="835" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fthings-to-know-about-dry%2Fdiagram-2_hu867163815cadc55e53cc3e00eac6b01f_143507_4789x835_resize_q80_h2_lanczos.webp" alt="Refactored solution" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fthings-to-know-about-dry%5C%2Fdiagram-2_hu867163815cadc55e53cc3e00eac6b01f_143507_4789x835_resize_q80_lanczos.jpg"" /> <h3 id="the-clash-of-principles">The Clash of Principles</h3> <p>Dave&rsquo;s biggest concern was that the second solution breaks the <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FDon%2527t_repeat_yourself" target="_blank">DRY</a> principle and introduces boilerplate. On the other hand, Susan was afraid that the original approach violates the <strong>Single Responsibility Principle</strong> (the &ldquo;S&rdquo; in <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FSOLID" target="_blank">SOLID</a>). Who&rsquo;s right?</p> <p>It&rsquo;s tough to come up with strict rules. Sometimes code duplication seems like boilerplate, but it&rsquo;s one of the best tools to fight code coupling. It helps to ask yourself if the code using the common structure is likely to change together. If not, it&rsquo;s safe to assume duplication is the right choice.</p> <p>Usually, DRY is better applied to behaviors, not data. For example, extracting common code to a separate function doesn&rsquo;t have the downsides we&rsquo;ve discussed.</p> <h3 id="whats-the-big-deal">What&rsquo;s the big deal?</h3> <p>Is such a minor change even &ldquo;architecture&rdquo;?</p> <p>Susan introduced a small change that had consequences she didn&rsquo;t anticipate. It was obvious to other engineers, but not to a newcomer. I guess you also know <strong>the feeling of being afraid to make changes in an unknown system because you can&rsquo;t predict what it might trigger</strong>.</p> <p>If you make many wrong decisions, even small ones, they tend to compound. Eventually, developers start complaining that the application is hard to work with. The turning point is when someone mentions a &ldquo;rewrite&rdquo;, and suddenly you know you have a big problem.</p> <blockquote> <p>The alternative to good design is always bad design. There is no such thing as no design.</p> <footer> <strong>Adam Judge</strong> </footer> </blockquote> <p>It&rsquo;s worth discussing architecture decisions before you run into issues. &ldquo;No architecture&rdquo; will just leave you with bad architecture.</p> <h3 id="can-microservices-save-you">Can microservices save you?</h3> <p>With all the benefits that microservices gave us, a dangerous idea also appeared, preached by some &ldquo;how to build microservices&rdquo; guides. <strong>It says that microservices will simplify your application.</strong> Because building large software projects is hard, some promise that you won&rsquo;t need to worry about it if you split your application into tiny chunks.</p> <p>This sounds good on paper, but it misses the point of splitting software. How do you know where to put the boundaries? Will you separate a service based on each database entity? REST endpoints? Features? <strong>How do you ensure low coupling between services?</strong></p> <h3 id="the-distributed-monolith">The Distributed Monolith</h3> <p>If you start with poorly separated services, you&rsquo;re likely to end up with the same monolith you tried to avoid, plus network overhead and complex tooling to manage the mess (also known as a <strong>distributed monolith</strong>). You&rsquo;ll just replace highly coupled modules with highly coupled services. And because everyone runs a Kubernetes cluster now, you may even think you&rsquo;re following industry standards.</p> <p>Even if you can rewrite a single service in one afternoon, can you as quickly change how services communicate with each other? What if they&rsquo;re owned by multiple teams, based on wrong boundaries? Consider how much simpler it is to refactor a single application.</p> <p>None of this negates other benefits of microservices, like independent deployments (crucial for Continuous Delivery) and easier horizontal scaling. As with all patterns, make sure you use the right tool for the job at the right time.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>We introduced similar issues on purpose in Wild Workouts. We will look into this in future posts and discuss other splitting techniques.</p> <p>You can find some of the ideas in Robert’s post: <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-or-monolith-its-detail%2F"><em>Why using Microservices or Monolith can be just a detail?</em></a>.</p> </p></div> </div> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/things-to-know-about-dry/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;firestore;microservices;architecture;DRY"> <input type="hidden" name="fields[blog_series]" value="Modern Business Software in Go"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h3 id="does-this-all-apply-to-go">Does this all apply to Go?</h3> <p>Open-source Go projects are usually low-level applications and infrastructure tools. This was especially true in the early days, and it&rsquo;s still hard to find good examples of Go applications that handle <strong>domain logic</strong>.</p> <p>By &ldquo;domain logic,&rdquo; I don&rsquo;t mean financial applications or complex business software. <strong>If you develop any kind of web application, there&rsquo;s a good chance you have some complex domain cases you need to model somehow.</strong></p> <p>Following some DDD examples can make you feel like you&rsquo;re no longer writing Go. I&rsquo;m well aware that forcing OOP patterns straight from Java is not fun to work with. However, <strong>some language-agnostic ideas are worth considering.</strong></p> <h3 id="whats-the-alternative">What&rsquo;s the alternative?</h3> <p>We spent the last few years exploring this topic. We love the simplicity of Go, but we&rsquo;ve also had success with ideas from Domain-Driven Design and <a href="proxy.php?url=https%3A%2F%2Fblog.cleancoder.com%2Funcle-bob%2F2012%2F08%2F13%2Fthe-clean-architecture.html" target="_blank">Clean Architecture</a>.</p> <p>For some reason, developers didn&rsquo;t stop talking about technical debt and legacy software with the arrival of microservices. Instead of looking for a silver bullet, we prefer to use business-oriented patterns together with microservices in a pragmatic way.</p> <p>We&rsquo;re not done refactoring Wild Workouts yet. In one of the following posts, we&rsquo;ll see how to introduce Clean Architecture to the project.</p>You should not build your own authenticationhttps://threedots.tech/post/firebase-cloud-run-authentication/Wed, 03 Jun 2020 00:00:00 +0200https://threedots.tech/post/firebase-cloud-run-authentication/Welcome to the third and <strong>last article covering how to build <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Ftags%2Ftoo-modern-go-app%2F">&ldquo;Too Modern Go application&rdquo;</a></strong>. But don&rsquo;t worry. This doesn&rsquo;t mean we&rsquo;re done showing you how to build applications that are easy to develop, maintain, and fun to work with in the long term. <strong>It&rsquo;s actually just the beginning of a bigger series!</strong> <p>We intentionally built the current version of the application to make it hard to maintain and develop in the future. In a subtle way. &#x1f609; In the next article, we will start the refactoring process. <strong>We will show you how subtle issues and shortcuts can become a problem in the long term. Even adding a new feature now may not be as easy as developing greenfield features (even if the application is still pretty new).</strong></p> <p>But before that, we have one important topic to cover: authentication. This is the part of the application that will not need refactoring. &#x1f609;</p> <h2 id="you-shouldnt-reinvent-the-wheel">You shouldn&rsquo;t reinvent the wheel</h2> <p>(I hope that) you are not creating a programming language and a framework for every project you work on. Even if you do that, <strong>besides losing a lot of time, it&rsquo;s not harmful. That&rsquo;s not the case for authentication. Why?</strong></p> <p>Let&rsquo;s imagine that you are working in a company implementing a cryptocurrency exchange. At the beginning of the project, you decided to build your own authentication mechanism. The first reason is your boss, who doesn&rsquo;t trust external authentication providers. The second reason is that you believe it should be simple.</p> <p><em>&ldquo;You did it many times&rdquo;</em> &ndash; this is what strange voices in your head repeat every time.</p> <p>Have you seen the <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FList_of_Mayday_episodes" target="_blank"><em>Mayday / Air Crash Investigation</em></a> documentary TV series? In the Polish edition, every episode starts with something like: <em>&ldquo;Catastrophes are not a matter of coincidence, but a series of unfortunate events.&rdquo;</em> In programming, this statement is surprisingly true.</p> <p>In our hypothetical project, your company decided to introduce a service account that could move funds between any account. Even from a wallet that, in theory, should be offline. Of course, only temporarily. &#x1f606; That doesn&rsquo;t sound like a good idea, but it simplified customer support from the very beginning.</p> <p>With time, everyone forgot about this feature. Until one day, when hackers found a bug in the authentication mechanism that allowed them to hijack any account on our exchange. Including the service account.</p> <p>Our boss and coworkers were less happy than the hackers because they lost all the money. The problem wouldn&rsquo;t be so big if we&rsquo;d lost &ldquo;only&rdquo; the company&rsquo;s money. In this case, we also lost all our customers&rsquo; money. &#x1f609;</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1280" height="720" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Faannnd-its-gone_hu33c95f31ab48c82d4f970636724dfeff_195764_1280x720_resize_q80_h2_lanczos.webp" alt="Aannnd it&#39;s gone" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Faannnd-its-gone_hu33c95f31ab48c82d4f970636724dfeff_195764_1280x720_resize_q80_lanczos.jpg"" /> <p>This example may sound extreme and unlikely to happen. Is it a rare scenario? The <a href="proxy.php?url=https%3A%2F%2Fowasp.org%2Fwww-project-top-ten%2F" target="_blank">OWASP Top Ten report</a> lists Broken Authentication in <strong>second place</strong>!</p> <blockquote> <p><ol start="2"> <li><strong>Broken Authentication.</strong> Application functions related to authentication and session management are often implemented incorrectly, allowing attackers to compromise passwords, keys, or session tokens, or to exploit other implementation flaws to assume other users’ identities temporarily or permanently.</li> </ol> </p> <footer> <strong></strong> <cite> <a href="proxy.php?url=https%3A%2F%2Fowasp.org%2Fwww-project-top-ten%2F" title="https://owasp.org/www-project-top-ten/" target="_blank">owasp.org/www-project-top-ten/</a> </cite> </footer> </blockquote> <p>You may say: <em>&ldquo;My company is just selling toasters! Why should I care?&rdquo;</em> I&rsquo;m pretty sure you still care about your company&rsquo;s image. <strong>The reputational damage after an incident caused by hackers hijacking your customers&rsquo; accounts is always embarrassing.</strong></p> <p>Do you still feel that you can implement perfect authentication? Even giants with hundreds of researchers and developers working just on authentication are unable to do that. In March 2020, a researcher <a href="proxy.php?url=https%3A%2F%2Flatesthackingnews.com%2F2020%2F03%2F03%2F10-year-old-facebook-oauth-framework-flaw-discovered%2F" target="_blank">found a Facebook vulnerability</a> that could allow hijacking anyone&rsquo;s Facebook account. Just a few months later, a bug in Apple Sign In allowed <a href="proxy.php?url=https%3A%2F%2Fbhavukjain.com%2Fblog%2F2020%2F05%2F30%2Fzeroday-signin-with-apple%2F" target="_blank">full account takeover of user accounts on third-party applications</a>.</p> <p>If you&rsquo;re still not convinced, consider saving your time. I&rsquo;m sure <strong>your customers will be much happier to receive a long-awaited feature than fancy, custom authentication</strong>. &#x1f609;</p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/firebase-cloud-run-authentication/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;serverless;firebase;authentication;cloudrun;googlecloud;gcloud;openapi;swagger;javascript"> <input type="hidden" name="fields[blog_series]" value="Modern Business Software in Go"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h2 id="using-firebase-authentication">Using Firebase authentication</h2> <p>Many solutions can be used for implementing authentication.</p> <p>The Wild Workouts example application that we created for this article series is now hosted on Firebase hosting. We will use <a href="proxy.php?url=https%3A%2F%2Ffirebase.google.com%2Fdocs%2Fauth" target="_blank">Firebase Authentication</a>. There is one significant advantage of this solution: it works almost entirely out of the box, both on the backend and frontend. Using an external identity provider shouldn&rsquo;t introduce significant vendor lock-in, as long as you design your integration to be easily switchable. We&rsquo;ll cover how to do that in a future article. &#x1f609;</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="authentication-recommendation-in-2026">Authentication recommendation in 2026</h4> <p>As of 2026, we recommend using <strong>passwordless login combined with SSO</strong> (Google, Facebook, GitHub) for most applications. This approach offers the best balance of security and user experience: no passwords to remember or leak, and users sign in with accounts they already have.</p> <p>You can easily switch authentication providers when you use an Identity Provider (IDP) correctly. Design your system so the IDP handles <em>only</em> authentication, while your application manages authorization and user data separately. This keeps your core business logic decoupled from the IDP, making future migrations straightforward.</p> </p></div> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="this-is-not-just-another-article-with-random-code-snippets">This is not just another article with random code snippets.</h4> <p>This post is part of a bigger series where we show how to build <strong>Go applications that are easy to develop, maintain, and fun to work with in the long term.</strong> We are doing it by sharing proven techniques based on many experiments we did with teams we lead and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%3Futm_source%3Dabout-wild-workouts%23thats-great-but-do-you-have-any-evidence-it-works">scientific research</a>.</p> <p>You can learn these patterns by building with us a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%3Futm_source%3Dabout-wild-workouts%23what-wild-workouts-can-do">fully functional</a> example Go web application &ndash; <strong>Wild Workouts</strong>.</p> <p>We did one thing differently &ndash; <strong>we included some subtle issues to the initial Wild Workouts implementation</strong>. Have we lost our minds to do that? Not yet. 😉 These issues are common for many Go projects. <strong>In the long term, these small issues become critical and stop adding new features.</strong></p> <p><strong>It&rsquo;s one of the essential skills of a senior or lead developer; you always need to keep long-term implications in mind.</strong></p> <p>We will fix them by <strong>refactoring</strong> Wild Workouts. In that way, you will quickly understand the techniques we share.</p> <p>Do you know that feeling after reading an article about some technique and trying implement it only to be blocked by some issues skipped in the guide? Cutting these details makes articles shorter and increases page views, but this is not our goal. Our goal is to create content that provides enough know-how to apply presented techniques. If you did not read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">previous articles from the series</a> yet, we highly recommend doing that.</p> <p>We believe that in some areas, there are no shortcuts. If you want to build complex applications in a fast and efficient way, you need to spend some time learning that. If it was simple, we wouldn&rsquo;t have large amounts of scary legacy code.</p> <p>Here&rsquo;s <strong><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">the full list of 14 articles</a></strong> released so far.</p> <p><strong>The full source code</strong> of Wild Workouts is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%3Futm_source%3Dabout-wild-workouts" target="_blank">GitHub</a>. Don&rsquo;t forget to leave a star for our project! ⭐</p> </p></div> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Deployment of the project is described in detail in the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcomplete-setup-of-serverless-application%2F"><em>A complete Terraform setup of a serverless application on Google Cloud Run and Firebase</em></a> article. In <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F">the first article</a> you can find deployment tl;dr at the end. During the setup, don't forget about enabling Email/Password Sign-in provider in <b>Authentication / Sign-in method</b> tab in the [Firebase Console](https://console.firebase.google.com/project/_/authentication/providers)! <a href="proxy.php?url=https%3A%2F%2Fconsole.firebase.google.com%2Fproject%2F_%2Fauthentication%2Fproviders"> <img title="" loading="lazy" decoding="async" class="img img-center" width="703" height="224" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Ffirebase-auth-providers_hu2f24140f776c892ff64f4e5f4d8caa5c_12750_703x224_resize_q80_h2_lanczos_3.webp" alt="Firebase Console" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Ffirebase-auth-providers_hu2f24140f776c892ff64f4e5f4d8caa5c_12750_703x224_resize_lanczos_3.png"" /> </a> </p></div> </div> <h3 id="frontend">Frontend</h3> <p>The first thing that we need to do is <a href="proxy.php?url=https%3A%2F%2Ffirebase.google.com%2Fdocs%2Fweb%2Fsetup%23add-sdks-initialize" target="_blank">the initialization of Firebase SDK</a>.</p> <p>Next, in the form on the main page, we call the <code>loginUser</code> function. This function calls <code>Auth.login</code>, <code>Auth.waitForAuthReady</code>, and <code>Auth.getJwtToken</code>. The result is set to OpenAPI-generated clients by <code>setApiClientsAuth</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">export</span> <span class="kd">function</span> <span class="nx">loginUser</span><span class="p">(</span><span class="nx">login</span><span class="p">,</span> <span class="nx">password</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">login</span><span class="p">(</span><span class="nx">login</span><span class="p">,</span> <span class="nx">password</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">waitForAuthReady</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Auth</span><span class="p">.</span><span class="nx">getJwtToken</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">token</span> <span class="p">=&gt;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">setApiClientsAuth</span><span class="p">(</span><span class="nx">token</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F4de7a36ee40e4cff5334bc33597592cc8468df54%2Fweb%2Fsrc%2Frepositories%2Fuser.js%23L36" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/web/src/repositories/user.js</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F4de7a36ee40e4cff5334bc33597592cc8468df54%2Fweb%2Fsrc%2Frepositories%2Fuser.js%23L36" target="_blank">Full source</a> </div> <p><code>Auth</code> is a class with two implementations: Firebase and mock. We will go through the mock implementation later. Let&rsquo;s focus on Firebase now.</p> <p>To log in, we need to call <code>firebase.auth().signInWithEmailAndPassword</code>. If everything is fine, <code>auth().currentUser.getIdToken</code> returns our JWT token.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">class</span> <span class="nx">FirebaseAuth</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">login</span><span class="p">(</span><span class="nx">login</span><span class="p">,</span> <span class="nx">password</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">firebase</span><span class="p">.</span><span class="nx">auth</span><span class="p">().</span><span class="nx">signInWithEmailAndPassword</span><span class="p">(</span><span class="nx">login</span><span class="p">,</span> <span class="nx">password</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">waitForAuthReady</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">firebase</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="nx">auth</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="nx">onAuthStateChanged</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">resolve</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">getJwtToken</span><span class="p">(</span><span class="nx">required</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">((</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">firebase</span><span class="p">.</span><span class="nx">auth</span><span class="p">().</span><span class="nx">currentUser</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="nx">required</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">reject</span><span class="p">(</span><span class="s1">&#39;no user found&#39;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">resolve</span><span class="p">(</span><span class="kc">null</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">firebase</span><span class="p">.</span><span class="nx">auth</span><span class="p">().</span><span class="nx">currentUser</span><span class="p">.</span><span class="nx">getIdToken</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">idToken</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">resolve</span><span class="p">(</span><span class="nx">idToken</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">reject</span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">});</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F4de7a36ee40e4cff5334bc33597592cc8468df54%2Fweb%2Fsrc%2Frepositories%2Fauth.js%23L6" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/web/src/repositories/auth.js</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F4de7a36ee40e4cff5334bc33597592cc8468df54%2Fweb%2Fsrc%2Frepositories%2Fauth.js%23L6" target="_blank">Full source</a> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Firebase also provides out of the box support for logging in with most popular OAuth providers like Facebook, Gmail, or GitHub. </p></div> </div> <p>Next, we need to set <code>authentications['bearerAuth'].accessToken</code> of <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%23public-http-api">OpenAPI-generated</a> clients to the JWT token received from <code>Auth.getJwtToken(false)</code>. Once we set this token on the OpenAPI clients, voila! All our requests are now authenticated.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">export</span> <span class="kd">function</span> <span class="nx">setApiClientsAuth</span><span class="p">(</span><span class="nx">idToken</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">usersClient</span><span class="p">.</span><span class="nx">authentications</span><span class="p">[</span><span class="s1">&#39;bearerAuth&#39;</span><span class="p">].</span><span class="nx">accessToken</span> <span class="o">=</span> <span class="nx">idToken</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainerClient</span><span class="p">.</span><span class="nx">authentications</span><span class="p">[</span><span class="s1">&#39;bearerAuth&#39;</span><span class="p">].</span><span class="nx">accessToken</span> <span class="o">=</span> <span class="nx">idToken</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainingsClient</span><span class="p">.</span><span class="nx">authentications</span><span class="p">[</span><span class="s1">&#39;bearerAuth&#39;</span><span class="p">].</span><span class="nx">accessToken</span> <span class="o">=</span> <span class="nx">idToken</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F4de7a36ee40e4cff5334bc33597592cc8468df54%2Fweb%2Fsrc%2Frepositories%2Fauth.js%23L118" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/web/src/repositories/auth.js</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F4de7a36ee40e4cff5334bc33597592cc8468df54%2Fweb%2Fsrc%2Frepositories%2Fauth.js%23L118" target="_blank">Full source</a> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If you are creating your own OpenAPI spec, it will not work without proper <a href="proxy.php?url=https%3A%2F%2Fswagger.io%2Fdocs%2Fspecification%2Fauthentication%2Fbearer-authentication%2F" target="_blank">authentication definition</a>. In Wild Workouts&rsquo; <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Fapi%2Fopenapi%2Ftrainings.yml%23L14" target="_blank">OpenAPI spec</a> it&rsquo;s already done. </p></div> </div> <p>If you would like to know more, I recommend checking the <a href="proxy.php?url=https%3A%2F%2Ffirebase.google.com%2Fdocs%2Freference%2Fjs%2Ffirebase.auth" target="_blank">Firebase Auth API reference</a>.</p> <h3 id="backend">Backend</h3> <p>The last part is using this authentication in our HTTP server. I created a simple HTTP middleware that will do this for us.</p> <p>This middleware does three things:</p> <ol> <li>Get token from HTTP header</li> <li>Verify token with Firebase auth client</li> <li>Save user data in credentials</li> </ol> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">auth</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;context&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;net/http&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;strings&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="s">&#34;firebase.google.com/go/auth&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/pkg/errors&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/pkg/internal/server/httperr&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">FirebaseHttpMiddleware</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">AuthClient</span> <span class="o">*</span><span class="nx">auth</span><span class="p">.</span><span class="nx">Client</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">a</span> <span class="nx">FirebaseHttpMiddleware</span><span class="p">)</span> <span class="nf">Middleware</span><span class="p">(</span><span class="nx">next</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">http</span><span class="p">.</span><span class="nf">HandlerFunc</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ctx</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">bearerToken</span> <span class="o">:=</span> <span class="nx">a</span><span class="p">.</span><span class="nf">tokenFromHeader</span><span class="p">(</span><span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">bearerToken</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">Unauthorised</span><span class="p">(</span><span class="s">&#34;empty-bearer-token&#34;</span><span class="p">,</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">token</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">a</span><span class="p">.</span><span class="nx">AuthClient</span><span class="p">.</span><span class="nf">VerifyIDToken</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">bearerToken</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">Unauthorised</span><span class="p">(</span><span class="s">&#34;unable-to-verify-jwt&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// it&#39;s always a good idea to use custom type as context value (in this case ctxKey) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// because nobody from the outside of the package will be able to override/read this value </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">ctx</span> <span class="p">=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithValue</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">userContextKey</span><span class="p">,</span> <span class="nx">User</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">UUID</span><span class="p">:</span> <span class="nx">token</span><span class="p">.</span><span class="nx">UID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Email</span><span class="p">:</span> <span class="nx">token</span><span class="p">.</span><span class="nx">Claims</span><span class="p">[</span><span class="s">&#34;email&#34;</span><span class="p">].(</span><span class="kt">string</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">Role</span><span class="p">:</span> <span class="nx">token</span><span class="p">.</span><span class="nx">Claims</span><span class="p">[</span><span class="s">&#34;role&#34;</span><span class="p">].(</span><span class="kt">string</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">DisplayName</span><span class="p">:</span> <span class="nx">token</span><span class="p">.</span><span class="nx">Claims</span><span class="p">[</span><span class="s">&#34;name&#34;</span><span class="p">].(</span><span class="kt">string</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span> <span class="p">=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">WithContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">next</span><span class="p">.</span><span class="nf">ServeHTTP</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">ctxKey</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">const</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">userContextKey</span> <span class="nx">ctxKey</span> <span class="p">=</span> <span class="kc">iota</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">UserFromCtx</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">(</span><span class="nx">User</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">u</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nf">Value</span><span class="p">(</span><span class="nx">userContextKey</span><span class="p">).(</span><span class="nx">User</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">ok</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">u</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">User</span><span class="p">{},</span> <span class="nx">NoUserInContextError</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Finternal%2Fcommon%2Fauth%2Fhttp.go%23L13" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/auth/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Finternal%2Fcommon%2Fauth%2Fhttp.go%23L13" target="_blank">Full source</a> </div> <p>User data can now be accessed in every HTTP request by using <code>auth.UserFromCtx</code> function.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">HttpServer</span><span class="p">)</span> <span class="nf">GetTrainings</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">auth</span><span class="p">.</span><span class="nf">UserFromCtx</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">Unauthorised</span><span class="p">(</span><span class="s">&#34;no-user-found&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Finternal%2Ftrainings%2Fhttp.go%23L26" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Finternal%2Ftrainings%2Fhttp.go%23L26" target="_blank">Full source</a> </div> <p>We can also limit access to some resources based on the user role.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">HttpServer</span><span class="p">)</span> <span class="nf">MakeHourAvailable</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">auth</span><span class="p">.</span><span class="nf">UserFromCtx</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">Unauthorised</span><span class="p">(</span><span class="s">&#34;no-user-found&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Role</span> <span class="o">!=</span> <span class="s">&#34;trainer&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">Unauthorised</span><span class="p">(</span><span class="s">&#34;invalid-role&#34;</span><span class="p">,</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fef6056fdeb39b89009127e07600f1b9ec87e717c%2Finternal%2Ftrainer%2Fhttp.go%23L32" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fef6056fdeb39b89009127e07600f1b9ec87e717c%2Finternal%2Ftrainer%2Fhttp.go%23L32" target="_blank">Full source</a> </div> <h3 id="adding-users">Adding users</h3> <p>In our case, we add users during the start of the <code>users</code> service. You can also add them from the Firebase UI. Unfortunately, via the UI you cannot set all required data, like claims: you need to do it via API.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">config</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">firebase</span><span class="p">.</span><span class="nx">Config</span><span class="p">{</span><span class="nx">ProjectID</span><span class="p">:</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;GCP_PROJECT&#34;</span><span class="p">)}</span> </span></span><span class="line"><span class="cl"><span class="nx">firebaseApp</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">firebase</span><span class="p">.</span><span class="nf">NewApp</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="nx">config</span><span class="p">,</span> <span class="nx">opts</span><span class="o">...</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nx">authClient</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">firebaseApp</span><span class="p">.</span><span class="nf">Auth</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">user</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">usersToCreate</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">userToCreate</span> <span class="o">:=</span> <span class="p">(</span><span class="o">&amp;</span><span class="nx">auth</span><span class="p">.</span><span class="nx">UserToCreate</span><span class="p">{}).</span> </span></span><span class="line"><span class="cl"> <span class="nf">Email</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">Email</span><span class="p">).</span> </span></span><span class="line"><span class="cl"> <span class="nf">Password</span><span class="p">(</span><span class="s">&#34;123456&#34;</span><span class="p">).</span> </span></span><span class="line"><span class="cl"> <span class="nf">DisplayName</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">DisplayName</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">createdUser</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">authClient</span><span class="p">.</span><span class="nf">CreateUser</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="nx">userToCreate</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">authClient</span><span class="p">.</span><span class="nf">SetCustomUserClaims</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="nx">createdUser</span><span class="p">.</span><span class="nx">UID</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kd">interface</span><span class="p">{}{</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;role&#34;</span><span class="p">:</span> <span class="nx">user</span><span class="p">.</span><span class="nx">Role</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fef6056fdeb39b89009127e07600f1b9ec87e717c%2Finternal%2Fusers%2Ffixtures.go%23L103" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/users/fixtures.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fef6056fdeb39b89009127e07600f1b9ec87e717c%2Finternal%2Fusers%2Ffixtures.go%23L103" target="_blank">Full source</a> </div> <p><a href="proxy.php?url=https%3A%2F%2Fconsole.firebase.google.com%2Fproject%2F_%2Fauthentication%2Fusers" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="938" height="320" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Ffirebase-auth_hud8875e309c69259367bc9f47db115314_32335_938x320_resize_q80_h2_lanczos_3.webp" alt="Firestore Console" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Ffirebase-auth_hud8875e309c69259367bc9f47db115314_32335_938x320_resize_lanczos_3.png"" /> </a></p> <h3 id="mock-authentication-for-local-dev">Mock Authentication for local dev</h3> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <strong>Update (2026):</strong> Since this article was written, Firebase has released the <a href="proxy.php?url=https%3A%2F%2Ffirebase.google.com%2Fdocs%2Femulator-suite%2Fconnect_auth" target="_blank">Authentication Emulator</a> as part of the Local Emulator Suite. You can now run Firebase Authentication locally without the workarounds described below. </p></div> </div> <p>There is high demand and a <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Ffirebase%2Ffirebase-tools%2Fissues%2F1677" target="_blank">discussion going on</a> about Firebase Authentication emulator support. Unfortunately, it doesn&rsquo;t exist yet. The situation is similar to Firestore: I want to be able to run my application locally without any external dependencies. As long as there is no emulator, there is no other way than to implement a simple mock.</p> <p>There is nothing complicated in either implementation. The <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fef6056fdeb39b89009127e07600f1b9ec87e717c%2Fweb%2Fsrc%2Frepositories%2Fauth.js%23L81" target="_blank">frontend&rsquo;s</a> <code>getJwtToken</code> is implemented by generating a JWT token with <code>mock secret</code>. The <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fef6056fdeb39b89009127e07600f1b9ec87e717c%2Finternal%2Fcommon%2Fauth%2Fhttp_mock.go%23L13" target="_blank">backend</a>, instead of calling Firebase to verify the token, checks if the JWT was generated with <code>mock secret</code>.</p> <p>This gives us some confidence that our flow is implemented more or less correctly. But is there any way to test it with production Firebase locally?</p> <h3 id="firebase-authentication-for-local-dev">Firebase Authentication for local dev</h3> <p>Mock authentication does not give us 100% confidence that the flow is working correctly. We should be able to test Firebase Authentication locally. To do that, you need to take some extra steps.</p> <p><strong>It is not straightforward. If you are not changing anything in the authentication, you can probably <a href="#and-thats-all">skip this part</a>.</strong></p> <p>First of all, you need to generate a service account file into the repository.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Remember to not share it with anyone! Don&rsquo;t worry, it&rsquo;s already in <code>.gitignore</code>. </p></div> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcloud auth login </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">gcloud iam service-accounts keys create service-account-file.json --project <span class="o">[</span>YOUR PROJECT ID<span class="o">]</span> --iam-account <span class="o">[</span>YOUR PROJECT ID<span class="o">]</span>@appspot.gserviceaccount.com </span></span></code></pre></div><p>Next, you need to uncomment <strong>all</strong> lines linking service account in <code>docker-compose.yml</code></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"> volumes: </span></span><span class="line"><span class="cl"> - ./pkg:/pkg </span></span><span class="line"><span class="cl"><span class="gd">-# - ./service-account-file.json:$SERVICE_ACCOUNT_FILE </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+ - ./service-account-file.json:$SERVICE_ACCOUNT_FILE </span></span></span><span class="line"><span class="cl"><span class="gi"></span> working_dir: /pkg/trainer </span></span><span class="line"><span class="cl"> ports: </span></span><span class="line"><span class="cl"> - &#34;127.0.0.1:3000:$PORT&#34; </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> context: ./dev/docker/app </span></span><span class="line"><span class="cl"> volumes: </span></span><span class="line"><span class="cl"> - ./pkg:/pkg </span></span><span class="line"><span class="cl"><span class="gd">-# - ./service-account-file.json:$SERVICE_ACCOUNT_FILE </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+ - ./service-account-file.json:$SERVICE_ACCOUNT_FILE </span></span></span><span class="line"><span class="cl"><span class="gi"></span> working_dir: /pkg/trainer </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="gu">@ ... do it for all services! </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fdocker-compose.yml" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/docker-compose.yml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fdocker-compose.yml" target="_blank">Full source</a> </div> <p>After that, you should set <code>GCP_PROJECT</code> to your project id, uncomment <code>SERIVCE_ACCOUNT_FILE</code> and set <code>MOCK_AUTH</code> to <code>false</code> in <code>.env</code></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">-GCP_PROJECT=threedotslabs-cloudnative </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+GCP_PROJECT=YOUR-PROJECT-ID </span></span></span><span class="line"><span class="cl"><span class="gi"></span> </span></span><span class="line"><span class="cl"> PORT=3000 </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="gd">-FIRESTORE_PROJECT_ID=threedotslabs-cloudnative </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+FIRESTORE_PROJECT_ID=YOUR-PROJECT-ID </span></span></span><span class="line"><span class="cl"><span class="gi"></span> FIRESTORE_EMULATOR_HOST=firestore:8787 </span></span><span class="line"><span class="cl"> # env used by karhoo/firestore-emulator container </span></span><span class="line"><span class="cl"><span class="gd">-GCP_PROJECT_ID=threedotslabs-cloudnative </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+GCP_PROJECT_ID=YOUR-PROJECT-ID </span></span></span><span class="line"><span class="cl"><span class="gi"></span> </span></span><span class="line"><span class="cl"> TRAINER_GRPC_ADDR=trainer-grpc:3000 </span></span><span class="line"><span class="cl"> USERS_GRPC_ADDR=users-grpc:3000 </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="gu">@ ... </span></span></span><span class="line"><span class="cl"><span class="gu"></span> </span></span><span class="line"><span class="cl"> CORS_ALLOWED_ORIGINS=http://localhost:8080 </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="gd">-#SERVICE_ACCOUNT_FILE=/service-account-file.json </span></span></span><span class="line"><span class="cl"><span class="gd">-MOCK_AUTH=true </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+SERVICE_ACCOUNT_FILE=/service-account-file.json </span></span></span><span class="line"><span class="cl"><span class="gi">+MOCK_AUTH=false </span></span></span><span class="line"><span class="cl"><span class="gi"></span> LOCAL_ENV=true </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2F.env" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/.env</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2F.env" target="_blank">Full source</a> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">-const MOCK_AUTH = process.env.NODE_ENV === &#39;development&#39; </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+const MOCK_AUTH = false </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F4de7a36ee40e4cff5334bc33597592cc8468df54%2Fweb%2Fsrc%2Frepositories%2Fauth.js%23L124" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/web/src/repositories/auth.js</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F4de7a36ee40e4cff5334bc33597592cc8468df54%2Fweb%2Fsrc%2Frepositories%2Fauth.js%23L124" target="_blank">Full source</a> </div> <p>Now you need to go to <a href="proxy.php?url=https%3A%2F%2Fconsole.firebase.google.com%2Fproject%2F_%2Fsettings%2Fgeneral" target="_blank">Project Settings</a> (you can find it after clicking the gear icon right to <em>Project overview</em>) and update your Firebase settings in <code>web/public/__/firebase/init.json</code>.</p> <p><a href="proxy.php?url=https%3A%2F%2Fconsole.firebase.google.com%2Fproject%2F_%2Fsettings%2Fgeneral" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="1227" height="730" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Ffirebase-app-config_hufa43b9249a0289dfabe244bacfeaf487_138304_1227x730_resize_q80_h2_lanczos_3.webp" alt="Firestore Console" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Ffirebase-app-config_hufa43b9249a0289dfabe244bacfeaf487_138304_1227x730_resize_lanczos_3.png"" /> </a></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">- &#34;apiKey&#34;: &#34;&#34;, </span></span></span><span class="line"><span class="cl"><span class="gd">- &#34;appId&#34;: &#34;&#34;, </span></span></span><span class="line"><span class="cl"><span class="gd">- &#34;authDomain&#34;: &#34;&#34;, </span></span></span><span class="line"><span class="cl"><span class="gd">- &#34;databaseURL&#34;: &#34;&#34;, </span></span></span><span class="line"><span class="cl"><span class="gd">- &#34;messagingSenderId&#34;: &#34;&#34;, </span></span></span><span class="line"><span class="cl"><span class="gd">- &#34;projectId&#34;: &#34;&#34;, </span></span></span><span class="line"><span class="cl"><span class="gd">- &#34;storageBucket&#34;: &#34;&#34; </span></span></span><span class="line"><span class="cl"><span class="gd"></span><span class="gi">+ &#34;apiKey&#34;: &#34;AIzaSyBW6TNG5Xdq1F7MleYml8CovOej5SbxXhw&#34;, </span></span></span><span class="line"><span class="cl"><span class="gi">+ &#34;authDomain&#34;: &#34;wild-workouts-6.firebaseapp.com&#34;, </span></span></span><span class="line"><span class="cl"><span class="gi">+ &#34;databaseURL&#34;: &#34;https://wild-workouts-6.firebaseio.com&#34;, </span></span></span><span class="line"><span class="cl"><span class="gi">+ &#34;projectId&#34;: &#34;wild-workouts-6&#34;, </span></span></span><span class="line"><span class="cl"><span class="gi">+ &#34;storageBucket&#34;: &#34;wild-workouts-6.appspot.com&#34;, </span></span></span><span class="line"><span class="cl"><span class="gi">+ &#34;messagingSenderId&#34;: &#34;519519305315&#34;, </span></span></span><span class="line"><span class="cl"><span class="gi">+ &#34;appId&#34;: &#34;1:519519305315:web:cde76e4847f0d95cc84256&#34; </span></span></span><span class="line"><span class="cl"><span class="gi"></span> } </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F4de7a36ee40e4cff5334bc33597592cc8468df54%2Fweb%2Fpublic%2F__%2Ffirebase%2Finit.json" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/web/public/__/firebase/init.json</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F4de7a36ee40e4cff5334bc33597592cc8468df54%2Fweb%2Fpublic%2F__%2Ffirebase%2Finit.json" target="_blank">Full source</a> </div> <p><strong>The last thing is stopping and starting again docker-compose</strong> to reload envs and links.</p> <h3 id="and-thats-all">And that&rsquo;s all</h3> <p>As you can see, the setup was straightforward. We were able to save a lot of time. <strong>The situation could be more complicated if we didn&rsquo;t want to host our application on Firebase.</strong> Our idea from the beginning was to prepare a simple base for future articles.</p> <p>We didn&rsquo;t exhaust the topic of authentication in this article. But for now, we don&rsquo;t plan to explore authentication any deeper.</p> <strong>Maybe you have some good or bad experiences with any authentication solution in Go? Please share in the comments, so all readers can profit from that! &#x1f44d;</strong> <p>In this series, we would like to show you how to build applications that are easy to develop, maintain, and fun to work with in the long term. Maybe you&rsquo;ve seen examples of Domain-Driven Design or Clean Architecture in Go. Most of them are not done in a pragmatic way that works within the language context.</p> <p><strong>In following articles, we will show patterns that we have successfully used in teams we&rsquo;ve led for a couple of years.</strong> We will also show when applying them makes sense and when it&rsquo;s over-engineering or CV-Driven Development. I&rsquo;m sure that it will make you rethink your point of view on these techniques. &#x1f609;</p> That&rsquo;s all for today. The next article will be available in 1-2 weeks. <strong>Please join our newsletter if you don&rsquo;t want to miss it!</strong> &#x1f609;Robust gRPC communication on Google Cloud Run (but not only!)https://threedots.tech/post/robust-grpc-google-cloud-run/Wed, 27 May 2020 00:00:00 +0200https://threedots.tech/post/robust-grpc-google-cloud-run/<p>Welcome to the third article in the series on building business-oriented applications in Go! In this series, we show you how to build applications that are easy to develop, maintain, and fun to work with in the long term.</p> <p>In this article, I describe how to <strong>build robust internal communication between your services using gRPC</strong>. I also cover the extra configuration required to set up authentication and TLS for Cloud Run.</p> <h2 id="why-grpc">Why gRPC?</h2> <p>Let&rsquo;s imagine a story that is true for many companies:</p> <p><em>Meet Dave. Dave works at a company that spent about two years building their product from scratch. During this time, they found thousands of customers who wanted to use their product. They started developing this application during the biggest microservices &ldquo;boom.&rdquo; It was an obvious choice to use that architecture. Now they have more than 50 microservices using HTTP calls to communicate with each other.</em></p> <p><em>Of course, Dave&rsquo;s company didn&rsquo;t do everything perfectly. The biggest pain is that all engineers are now afraid to change anything in the HTTP contracts. It&rsquo;s easy to make incompatible changes or return invalid data. The entire application breaking because of this isn&rsquo;t rare. &ldquo;Didn&rsquo;t we build microservices to avoid that?&rdquo; is a question the scary voices in Dave&rsquo;s head ask every day.</em></p> <p><em>Dave already proposed using OpenAPI to generate HTTP server responses and clients. But he quickly found that he could still return invalid data from the API.</em></p> <p>Whether this story sounds familiar or not, the solution for Dave&rsquo;s company is simple and straightforward to implement. <strong>You can easily achieve robust contracts between your services by using gRPC.</strong></p> <p><a href="proxy.php?url=https%3A%2F%2Fgrpc.io%2F" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="1112" height="433" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Fgrpc-logo_hua6f976174e193f017b23a393e304958b_74096_1112x433_resize_q80_h2_lanczos.webp" alt="gRPC logo" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Fgrpc-logo_hua6f976174e193f017b23a393e304958b_74096_1112x433_resize_q80_lanczos.jpg"" /> </a></p> <p>The way gRPC generates servers and clients is much stricter than OpenAPI. It&rsquo;s far better than OpenAPI&rsquo;s client and server, which just copy structures.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>gRPC doesn&rsquo;t solve <strong>data quality problems</strong>. You can still send data that is not empty but doesn&rsquo;t make sense.</p> <p>Make sure your data is valid using robust contracts, contract testing, and end-to-end tests.</p> </p></div> </div> <p>Another reason to consider gRPC is performance. Studies show <strong>gRPC can be up to 10x faster than REST</strong>. When your API handles millions of requests per second, this becomes a cost optimization opportunity. For applications like Wild Workouts, where traffic may be less than 10 requests/sec, <strong>it doesn&rsquo;t matter</strong>.</p> <p>To avoid bias, I tried to find reasons not to use gRPC for internal communication. I couldn&rsquo;t find any:</p> <ul> <li><strong>The barrier to entry is low.</strong></li> <li>Adding a gRPC server requires no extra infrastructure work: it works on top of HTTP/2.</li> <li>It works with many languages like Java, C/C++, Python, C#, JS, and <a href="proxy.php?url=https%3A%2F%2Fgrpc.io%2Fabout%2F%23officially-supported-languages-and-platforms" target="_blank">more</a>.</li> <li>In theory, you can even use gRPC for <a href="proxy.php?url=https%3A%2F%2Fgrpc.io%2Fblog%2Fstate-of-grpc-web%2F" target="_blank">frontend communication</a> (I haven&rsquo;t tested that).</li> <li>It&rsquo;s &ldquo;Goish&rdquo;: <strong>the compiler ensures you don&rsquo;t return anything invalid.</strong></li> </ul> <p>Sounds promising? Let&rsquo;s verify this with the implementation in Wild Workouts!</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="this-is-not-just-another-article-with-random-code-snippets">This is not just another article with random code snippets.</h4> <p>This post is part of a bigger series where we show how to build <strong>Go applications that are easy to develop, maintain, and fun to work with in the long term.</strong> We are doing it by sharing proven techniques based on many experiments we did with teams we lead and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%3Futm_source%3Dabout-wild-workouts%23thats-great-but-do-you-have-any-evidence-it-works">scientific research</a>.</p> <p>You can learn these patterns by building with us a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%3Futm_source%3Dabout-wild-workouts%23what-wild-workouts-can-do">fully functional</a> example Go web application &ndash; <strong>Wild Workouts</strong>.</p> <p>We did one thing differently &ndash; <strong>we included some subtle issues to the initial Wild Workouts implementation</strong>. Have we lost our minds to do that? Not yet. 😉 These issues are common for many Go projects. <strong>In the long term, these small issues become critical and stop adding new features.</strong></p> <p><strong>It&rsquo;s one of the essential skills of a senior or lead developer; you always need to keep long-term implications in mind.</strong></p> <p>We will fix them by <strong>refactoring</strong> Wild Workouts. In that way, you will quickly understand the techniques we share.</p> <p>Do you know that feeling after reading an article about some technique and trying implement it only to be blocked by some issues skipped in the guide? Cutting these details makes articles shorter and increases page views, but this is not our goal. Our goal is to create content that provides enough know-how to apply presented techniques. If you did not read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">previous articles from the series</a> yet, we highly recommend doing that.</p> <p>We believe that in some areas, there are no shortcuts. If you want to build complex applications in a fast and efficient way, you need to spend some time learning that. If it was simple, we wouldn&rsquo;t have large amounts of scary legacy code.</p> <p>Here&rsquo;s <strong><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">the full list of 14 articles</a></strong> released so far.</p> <p><strong>The full source code</strong> of Wild Workouts is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%3Futm_source%3Dabout-wild-workouts" target="_blank">GitHub</a>. Don&rsquo;t forget to leave a star for our project! ⭐</p> </p></div> </div> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/robust-grpc-google-cloud-run/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;serverless;grpc;cloudrun;googlecloud;gcloud"> <input type="hidden" name="fields[blog_series]" value="Modern Business Software in Go"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h2 id="generated-server">Generated server</h2> <p>Currently, Wild Workouts doesn&rsquo;t have many gRPC endpoints. We can update trainer hours availability and user training balance (credits).</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="2195" height="1821" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fcomplete-setup-of-serverless-application%2Fservices-diagram_hu7a5b5d5cc96d9ada230aeb87ed5b9159_202896_2195x1821_resize_q80_h2_lanczos.webp" alt="Architecture" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fcomplete-setup-of-serverless-application%5C%2Fservices-diagram_hu7a5b5d5cc96d9ada230aeb87ed5b9159_202896_2195x1821_resize_q80_lanczos.jpg"" /> <p>Let&rsquo;s look at the <code>Trainer gRPC service</code>. To define our gRPC server, we need to create a <code>trainer.proto</code> file.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-proto" data-lang="proto"><span class="line"><span class="cl"><span class="n">syntax</span> <span class="o">=</span> <span class="s">&#34;proto3&#34;</span><span class="p">;</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="kn">package</span> <span class="nn">trainer</span><span class="p">;</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">import</span> <span class="s">&#34;google/protobuf/timestamp.proto&#34;</span><span class="p">;</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="kd">service</span> <span class="n">TrainerService</span> <span class="p">{</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span> <span class="k">rpc</span> <span class="n">IsHourAvailable</span><span class="p">(</span><span class="n">IsHourAvailableRequest</span><span class="p">)</span> <span class="k">returns</span> <span class="p">(</span><span class="n">IsHourAvailableResponse</span><span class="p">)</span> <span class="p">{}</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span> <span class="k">rpc</span> <span class="n">UpdateHour</span><span class="p">(</span><span class="n">UpdateHourRequest</span><span class="p">)</span> <span class="k">returns</span> <span class="p">(</span><span class="n">EmptyResponse</span><span class="p">)</span> <span class="p">{}</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="p">}</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="kd">message</span> <span class="nc">IsHourAvailableRequest</span> <span class="p">{</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span> <span class="n">google.protobuf.Timestamp</span> <span class="n">time</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="p">}</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="kd">message</span> <span class="nc">IsHourAvailableResponse</span> <span class="p">{</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span> <span class="kt">bool</span> <span class="n">is_available</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="p">}</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="kd">message</span> <span class="nc">UpdateHourRequest</span> <span class="p">{</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span> <span class="n">google.protobuf.Timestamp</span> <span class="n">time</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span> <span class="kt">bool</span> <span class="n">has_training_scheduled</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span> <span class="kt">bool</span> <span class="n">available</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="p">}</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="kd">message</span> <span class="nc">EmptyResponse</span> <span class="p">{}</span><span class="err"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Fapi%2Fprotobuf%2Ftrainer.proto" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/api/protobuf/trainer.proto</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Fapi%2Fprotobuf%2Ftrainer.proto" target="_blank">Full source</a> </div> <p>The <code>.proto</code> definition is converted into Go code using the <em>Protocol Buffer Compiler</em> (protoc).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nf">.PHONY</span><span class="o">:</span> <span class="n">proto</span> </span></span><span class="line"><span class="cl"><span class="nf">proto</span><span class="o">:</span> </span></span><span class="line"><span class="cl"> protoc --go_out<span class="o">=</span><span class="nv">plugins</span><span class="o">=</span>grpc:internal/common/genproto/trainer -I api/protobuf api/protobuf/trainer.proto </span></span><span class="line"><span class="cl"> protoc --go_out<span class="o">=</span><span class="nv">plugins</span><span class="o">=</span>grpc:internal/common/genproto/users -I api/protobuf api/protobuf/users.proto </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2FMakefile" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/Makefile</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2FMakefile" target="_blank">Full source</a> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>To generate Go code from <code>.proto</code>, you need to install <a href="proxy.php?url=https%3A%2F%2Fgrpc.io%2Fdocs%2Fprotoc-installation%2F" target="_blank">protoc</a> and the <a href="proxy.php?url=https%3A%2F%2Fgrpc.io%2Fdocs%2Fquickstart%2Fgo%2F" target="_blank">protoc Go Plugin</a>.</p> <p>A list of supported types can be found in the <a href="proxy.php?url=https%3A%2F%2Fdevelopers.google.com%2Fprotocol-buffers%2Fdocs%2Freference%2Fproto3-spec%23fields" target="_blank">Protocol Buffers Version 3 Language Specification</a>. More complex built-in types like Timestamp can be found in the <a href="proxy.php?url=https%3A%2F%2Fdevelopers.google.com%2Fprotocol-buffers%2Fdocs%2Freference%2Fgoogle.protobuf" target="_blank">Well-Known Types list</a>.</p> </p></div> </div> <p>Here&rsquo;s an example of a generated model:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">UpdateHourRequest</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Time</span> <span class="o">*</span><span class="nx">timestamp</span><span class="p">.</span><span class="nx">Timestamp</span> <span class="s">`protobuf:&#34;bytes,1,opt,name=time,proto3&#34; json:&#34;time,omitempty&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">HasTrainingScheduled</span> <span class="kt">bool</span> <span class="s">`protobuf:&#34;varint,2,opt,name=has_training_scheduled,json=hasTrainingScheduled,proto3&#34; json:&#34;has_training_scheduled,omitempty&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Available</span> <span class="kt">bool</span> <span class="s">`protobuf:&#34;varint,3,opt,name=available,proto3&#34; json:&#34;available,omitempty&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">XXX_NoUnkeyedLiteral</span> <span class="kd">struct</span><span class="p">{}</span> <span class="s">`json:&#34;-&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... more proto garbage ;) </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Finternal%2Fcommon%2Fgenproto%2Ftrainer%2Ftrainer.pb.go%23L106" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/trainer/trainer.pb.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Finternal%2Fcommon%2Fgenproto%2Ftrainer%2Ftrainer.pb.go%23L106" target="_blank">Full source</a> </div> <p>And the server:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">TrainerServiceServer</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">IsHourAvailable</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="o">*</span><span class="nx">IsHourAvailableRequest</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">IsHourAvailableResponse</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">UpdateHour</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="o">*</span><span class="nx">UpdateHourRequest</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">EmptyResponse</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Finternal%2Fcommon%2Fgenproto%2Ftrainer%2Ftrainer.pb.go%23L269" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/trainer/trainer.pb.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Finternal%2Fcommon%2Fgenproto%2Ftrainer%2Ftrainer.pb.go%23L269" target="_blank">Full source</a> </div> <p>The difference between HTTP and gRPC is that with gRPC, we don&rsquo;t need to worry about what to return or how to do it. If I were to <strong>compare the level of confidence between HTTP and gRPC, it would be like comparing Python and Go</strong>. gRPC is much stricter, and it&rsquo;s impossible to return or receive invalid values: <strong>the compiler will let us know</strong>.</p> <p>Protobuf also has built-in support for field deprecation and <a href="proxy.php?url=https%3A%2F%2Fdevelopers.google.com%2Fprotocol-buffers%2Fdocs%2Fproto3%23backwards-compatibility-issues" target="_blank">backward compatibility</a>. This helps in environments with many independent teams.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h3 id="protobuf-vs-grpc">Protobuf vs gRPC</h3> <p>Protobuf (Protocol Buffers) is the Interface Definition Language used by default for defining the service interface and payload structure. Protobuf also serializes these models to binary format.</p> <p>You can find more details about gRPC and Protobuf on the <a href="proxy.php?url=https%3A%2F%2Fgrpc.io%2Fdocs%2Fguides%2Fconcepts%2F" target="_blank">gRPC Concepts</a> page.</p> </p></div> </div> <p>Implementing the server works almost the same as HTTP <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%23public-http-api">generated by OpenAPI</a>: we need to implement an interface (<code>TrainerServiceServer</code> in this case).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">GrpcServer</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span> <span class="nx">db</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">g</span> <span class="nx">GrpcServer</span><span class="p">)</span> <span class="nf">IsHourAvailable</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">req</span> <span class="o">*</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">IsHourAvailableRequest</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">IsHourAvailableResponse</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">timeToCheck</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">grpcTimestampToTime</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">InvalidArgument</span><span class="p">,</span> <span class="s">&#34;unable to parse time&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">model</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">g</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">DateModel</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">timeToCheck</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">status</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">codes</span><span class="p">.</span><span class="nx">Internal</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;unable to get data model: %s&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">hour</span><span class="p">,</span> <span class="nx">found</span> <span class="o">:=</span> <span class="nx">model</span><span class="p">.</span><span class="nf">FindHourInDate</span><span class="p">(</span><span class="nx">timeToCheck</span><span class="p">);</span> <span class="nx">found</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">IsHourAvailableResponse</span><span class="p">{</span><span class="nx">IsAvailable</span><span class="p">:</span> <span class="nx">hour</span><span class="p">.</span><span class="nx">Available</span> <span class="o">&amp;&amp;</span> <span class="p">!</span><span class="nx">hour</span><span class="p">.</span><span class="nx">HasTrainingScheduled</span><span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">IsHourAvailableResponse</span><span class="p">{</span><span class="nx">IsAvailable</span><span class="p">:</span> <span class="kc">false</span><span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Finternal%2Ftrainer%2Fgrpc.go%23L16" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/grpc.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Finternal%2Ftrainer%2Fgrpc.go%23L16" target="_blank">Full source</a> </div> <p>As you can see, you cannot return anything other than <code>IsHourAvailableResponse</code>, and you can always be sure you&rsquo;ll receive <code>IsHourAvailableRequest</code>. For errors, you can return one of the predefined <a href="proxy.php?url=https%3A%2F%2Fgodoc.org%2Fgoogle.golang.org%2Fgrpc%2Fcodes%23Code" target="_blank">error codes</a>. These are more modern than HTTP status codes.</p> <p>Starting the gRPC server works the same as an HTTP server:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">server</span><span class="p">.</span><span class="nf">RunGRPCServer</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">server</span> <span class="o">*</span><span class="nx">grpc</span><span class="p">.</span><span class="nx">Server</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">svc</span> <span class="o">:=</span> <span class="nx">GrpcServer</span><span class="p">{</span><span class="nx">firebaseDB</span><span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainer</span><span class="p">.</span><span class="nf">RegisterTrainerServiceServer</span><span class="p">(</span><span class="nx">server</span><span class="p">,</span> <span class="nx">svc</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Finternal%2Ftrainer%2Fmain.go%23L38" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Finternal%2Ftrainer%2Fmain.go%23L38" target="_blank">Full source</a> </div> <h2 id="internal-grpc-client">Internal gRPC client</h2> <p>After our server is running, it&rsquo;s time to use it. First, we need to create a client instance. <code>trainer.NewTrainerServiceClient</code> is generated from <code>.proto</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">TrainerServiceClient</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">IsHourAvailable</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">in</span> <span class="o">*</span><span class="nx">IsHourAvailableRequest</span><span class="p">,</span> <span class="nx">opts</span> <span class="o">...</span><span class="nx">grpc</span><span class="p">.</span><span class="nx">CallOption</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">IsHourAvailableResponse</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">UpdateHour</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">in</span> <span class="o">*</span><span class="nx">UpdateHourRequest</span><span class="p">,</span> <span class="nx">opts</span> <span class="o">...</span><span class="nx">grpc</span><span class="p">.</span><span class="nx">CallOption</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="nx">EmptyResponse</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">trainerServiceClient</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">cc</span> <span class="nx">grpc</span><span class="p">.</span><span class="nx">ClientConnInterface</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewTrainerServiceClient</span><span class="p">(</span><span class="nx">cc</span> <span class="nx">grpc</span><span class="p">.</span><span class="nx">ClientConnInterface</span><span class="p">)</span> <span class="nx">TrainerServiceClient</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="o">&amp;</span><span class="nx">trainerServiceClient</span><span class="p">{</span><span class="nx">cc</span><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Finternal%2Fcommon%2Fgenproto%2Ftrainer%2Ftrainer.pb.go%23L237" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/trainer/trainer.pb.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Finternal%2Fcommon%2Fgenproto%2Ftrainer%2Ftrainer.pb.go%23L237" target="_blank">Full source</a> </div> <p>To make the generated client work, we need to pass a few extra options that handle the following:</p> <ul> <li>Authentication.</li> <li>TLS encryption.</li> <li>&ldquo;Service discovery&rdquo; (we use hardcoded names of services provided by <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcomplete-setup-of-serverless-application%2F%23cloud-run">Terraform</a> via the <code>TRAINER_GRPC_ADDR</code> env variable).</li> </ul> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="s">&#34;github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/pkg/internal/genproto/trainer&#34;</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewTrainerClient</span><span class="p">()</span> <span class="p">(</span><span class="nx">client</span> <span class="nx">trainer</span><span class="p">.</span><span class="nx">TrainerServiceClient</span><span class="p">,</span> <span class="nx">close</span> <span class="kd">func</span><span class="p">()</span> <span class="kt">error</span><span class="p">,</span> <span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">grpcAddr</span> <span class="o">:=</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;TRAINER_GRPC_ADDR&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">grpcAddr</span> <span class="o">==</span> <span class="s">&#34;&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="kd">func</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">},</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;empty env TRAINER_GRPC_ADDR&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">opts</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">grpcDialOpts</span><span class="p">(</span><span class="nx">grpcAddr</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="kd">func</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">},</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">conn</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">grpc</span><span class="p">.</span><span class="nf">Dial</span><span class="p">(</span><span class="nx">grpcAddr</span><span class="p">,</span> <span class="nx">opts</span><span class="o">...</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="kd">func</span><span class="p">()</span> <span class="kt">error</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">},</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">trainer</span><span class="p">.</span><span class="nf">NewTrainerServiceClient</span><span class="p">(</span><span class="nx">conn</span><span class="p">),</span> <span class="nx">conn</span><span class="p">.</span><span class="nx">Close</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Finternal%2Fcommon%2Fclient%2Fgrpc.go%23L17" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/client/grpc.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Finternal%2Fcommon%2Fclient%2Fgrpc.go%23L17" target="_blank">Full source</a> </div> <p>After creating our client, we can call any of its methods. In this example, we call <code>UpdateHour</code> when creating a training.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">main</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="s">&#34;github.com/pkg/errors&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/golang/protobuf/ptypes&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/pkg/internal/genproto/trainer&#34;</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">HttpServer</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span> <span class="nx">db</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainerClient</span> <span class="nx">trainer</span><span class="p">.</span><span class="nx">TrainerServiceClient</span> </span></span><span class="line"><span class="cl"> <span class="nx">usersClient</span> <span class="nx">users</span><span class="p">.</span><span class="nx">UsersServiceClient</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">HttpServer</span><span class="p">)</span> <span class="nf">CreateTraining</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">timestamp</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">ptypes</span><span class="p">.</span><span class="nf">TimestampProto</span><span class="p">(</span><span class="nx">postTraining</span><span class="p">.</span><span class="nx">Time</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;unable to convert time to proto timestamp&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">trainerClient</span><span class="p">.</span><span class="nf">UpdateHour</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">trainer</span><span class="p">.</span><span class="nx">UpdateHourRequest</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Time</span><span class="p">:</span> <span class="nx">timestamp</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">HasTrainingScheduled</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Available</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;unable to update trainer hour&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Finternal%2Ftrainings%2Fhttp.go%23L94" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Finternal%2Ftrainings%2Fhttp.go%23L94" target="_blank">Full source</a> </div> <h2 id="cloud-run-authentication--tls">Cloud Run authentication &amp; TLS</h2> <p>Authentication of the client is handled by Cloud Run out of the box.</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="561" height="658" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Fcloud-run-grpc-auth_hu664bdef2cfe33076ad35eabbbbbbb951_48085_561x658_resize_q80_h2_lanczos_3.webp" alt="Cloud Run Authentication" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Fcloud-run-grpc-auth_hu664bdef2cfe33076ad35eabbbbbbb951_48085_561x658_resize_lanczos_3.png"" /> <div class="code-ref"> You can also enable Authentication from Cloud Run UI. <br>You need to also grant the <a href="proxy.php?url=https%3A%2F%2Fcloud.google.com%2Frun%2Fdocs%2Fauthenticating%2Fservice-to-service" target='_blank'>roles/run.invoker</a> role to service's service account. </div></p> <p><strong>The simpler (and recommended by us) way is using Terraform.</strong> Miłosz described it in detail in the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fcomplete-setup-of-serverless-application%2F%23cloud-run-permissions">previous article</a>. </p> <p>One thing that doesn&rsquo;t work out of the box is sending authentication with the request. Did I mention that the standard gRPC transport is HTTP/2? For that reason, we can use good old JWT (JSON Web Tokens) for authentication.</p> <p>To make it work, we need to implement the <code>google.golang.org/grpc/credentials.PerRPCCredentials</code> interface. The implementation is based on the official guide from <a href="proxy.php?url=https%3A%2F%2Fcloud.google.com%2Frun%2Fdocs%2Fauthenticating%2Fservice-to-service%23go" target="_blank">Google Cloud Documentation</a>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">metadataServerToken</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">serviceURL</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">newMetadataServerToken</span><span class="p">(</span><span class="nx">grpcAddr</span> <span class="kt">string</span><span class="p">)</span> <span class="nx">credentials</span><span class="p">.</span><span class="nx">PerRPCCredentials</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// based on https://cloud.google.com/run/docs/authenticating/service-to-service#go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// service need to have https prefix without port </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">serviceURL</span> <span class="o">:=</span> <span class="s">&#34;https://&#34;</span> <span class="o">+</span> <span class="nx">strings</span><span class="p">.</span><span class="nf">Split</span><span class="p">(</span><span class="nx">grpcAddr</span><span class="p">,</span> <span class="s">&#34;:&#34;</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">metadataServerToken</span><span class="p">{</span><span class="nx">serviceURL</span><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// GetRequestMetadata is called on every request, so we are sure that token is always not expired </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="p">(</span><span class="nx">t</span> <span class="nx">metadataServerToken</span><span class="p">)</span> <span class="nf">GetRequestMetadata</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">in</span> <span class="o">...</span><span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// based on https://cloud.google.com/run/docs/authenticating/service-to-service#go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">tokenURL</span> <span class="o">:=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;/instance/service-accounts/default/identity?audience=%s&#34;</span><span class="p">,</span> <span class="nx">t</span><span class="p">.</span><span class="nx">serviceURL</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">idToken</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">metadata</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="nx">tokenURL</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;cannot query id token for gRPC&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;authorization&#34;</span><span class="p">:</span> <span class="s">&#34;Bearer &#34;</span> <span class="o">+</span> <span class="nx">idToken</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">metadataServerToken</span><span class="p">)</span> <span class="nf">RequireTransportSecurity</span><span class="p">()</span> <span class="kt">bool</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Finternal%2Fcommon%2Fclient%2Fauth.go%23L13" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/client/auth.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Finternal%2Fcommon%2Fclient%2Fauth.go%23L13" target="_blank">Full source</a> </div> <p>The last step is passing it to the <code>[]grpc.DialOption</code> list when creating all gRPC clients.</p> <p>It&rsquo;s also a good idea to ensure our server&rsquo;s certificate is valid with <code>grpc.WithTransportCredentials</code>.</p> <p>Authentication and TLS encryption are disabled in the local Docker environment.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">grpcDialOpts</span><span class="p">(</span><span class="nx">grpcAddr</span> <span class="kt">string</span><span class="p">)</span> <span class="p">([]</span><span class="nx">grpc</span><span class="p">.</span><span class="nx">DialOption</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">noTLS</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">strconv</span><span class="p">.</span><span class="nf">ParseBool</span><span class="p">(</span><span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;GRPC_NO_TLS&#34;</span><span class="p">));</span> <span class="nx">noTLS</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">[]</span><span class="nx">grpc</span><span class="p">.</span><span class="nx">DialOption</span><span class="p">{</span><span class="nx">grpc</span><span class="p">.</span><span class="nf">WithInsecure</span><span class="p">()},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">systemRoots</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">x509</span><span class="p">.</span><span class="nf">SystemCertPool</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;cannot load root CA cert&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">creds</span> <span class="o">:=</span> <span class="nx">credentials</span><span class="p">.</span><span class="nf">NewTLS</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">tls</span><span class="p">.</span><span class="nx">Config</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">RootCAs</span><span class="p">:</span> <span class="nx">systemRoots</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">[]</span><span class="nx">grpc</span><span class="p">.</span><span class="nx">DialOption</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">grpc</span><span class="p">.</span><span class="nf">WithTransportCredentials</span><span class="p">(</span><span class="nx">creds</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">grpc</span><span class="p">.</span><span class="nf">WithPerRPCCredentials</span><span class="p">(</span><span class="nf">newMetadataServerToken</span><span class="p">(</span><span class="nx">grpcAddr</span><span class="p">)),</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Finternal%2Fcommon%2Fclient%2Fgrpc.go%23L63" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/client/grpc.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F33be9ae12a031b85b09fd8779fc445445785b106%2Finternal%2Fcommon%2Fclient%2Fgrpc.go%23L63" target="_blank">Full source</a> </div> <h2 id="are-all-the-problems-of-internal-communication-solved">Are all the problems of internal communication solved?</h2> <p><strong>A hammer is great for hammering nails but awful for cutting a tree. The same applies to gRPC or any other technique.</strong></p> <p><strong>gRPC works great for synchronous communication, but not every process is synchronous by nature. Applying synchronous communication everywhere will create a slow, unstable system.</strong> Currently, Wild Workouts doesn&rsquo;t have any flow that should be asynchronous. We will cover this topic in more depth in the next articles by implementing new features. In the meantime, check out the <a href="proxy.php?url=http%3A%2F%2Fwatermill.io%2F" target="_blank">Watermill</a> library, which we also created. &#x1f609; It helps with building asynchronous, event-driven applications the easy way.</p> <p><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="1018" height="550" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Fwatermill_hu9d0ee5ae6ba37b67d52b7eb6ea76a10e_127506_1018x550_resize_q80_h2_lanczos_3.webp" alt="Watermill" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Fwatermill_hu9d0ee5ae6ba37b67d52b7eb6ea76a10e_127506_1018x550_resize_lanczos_3.png"" /> </a></p> <h3 id="whats-next">What&rsquo;s next?</h3> <p>Having robust contracts doesn&rsquo;t mean we aren&rsquo;t introducing unnecessary internal communication. In some cases, operations can be handled in one service in a simpler, more pragmatic way.</p> <p>Avoiding these issues isn&rsquo;t simple. Fortunately, we know techniques that help. We&rsquo;ll share them with you soon. &#x1f609;</p> <p><strong>Until then, we still have one article left about our &ldquo;Too modern application.&rdquo; It will cover Firebase HTTP authentication. After that, we&rsquo;ll start the refactoring!</strong> As I mentioned in the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F">first article</a>, we intentionally introduced some issues in Wild Workouts. Are you curious about what&rsquo;s wrong with Wild Workouts? Let us know in the comments! &#x1f609;</p> <p>See you next week &#x1f44b;</p>A complete Terraform setup of a serverless application on Google Cloud Run and Firebasehttps://threedots.tech/post/complete-setup-of-serverless-application/Tue, 19 May 2020 00:00:00 +0200https://threedots.tech/post/complete-setup-of-serverless-application/In the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F">previous post</a>, <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Froblaszczak" target="_blank">Robert</a> introduced <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">Wild Workouts</a>, our example serverless application. Every week or two, we release new articles about this project, focusing on creating business-oriented applications in Go. In this post, I continue where Robert left off and describe the infrastructure setup. <p>We picked <strong>Google Cloud Platform (GCP)</strong> as the provider of all infrastructure parts of the project. We use <strong>Cloud Run</strong> for running Go services, <strong>Firestore</strong> as the database, <strong>Cloud Build</strong> as CI/CD, and <strong>Firebase</strong> for web hosting and authentication. The project is based on <a href="proxy.php?url=https%3A%2F%2Fwww.terraform.io%2F" target="_blank">Terraform</a>.</p> <h2 id="infrastructure-as-code">Infrastructure as Code</h2> <p>If you&rsquo;re not familiar with Terraform, it&rsquo;s a tool for storing infrastructure configuration as text files. This technique is also known as &ldquo;Infrastructure as Code&rdquo;. It&rsquo;s a broad topic, so I&rsquo;ll mention just a few benefits:</p> <ul> <li><strong>Storing infrastructure configuration in a repository</strong>. This gives you all the benefits of version control: commit history, a single source of truth, and code reviews.</li> <li><strong>Consistency over time</strong>. Ideally, there are no manual changes to infrastructure other than those described in the configuration.</li> <li><strong>Multiple environments</strong>. When you stop treating your servers as pets, it&rsquo;s much easier to create identical environments, sometimes even on demand.</li> </ul> <h2 id="terraform-101">Terraform 101</h2> <p><em>You can skip this part if you already know Terraform basics.</em></p> <p>In Terraform, you define a set of <strong>resources</strong> in <code>.tf</code> files using <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fhashicorp%2Fhcl" target="_blank">HCL</a> syntax. A resource can be a database, network configuration, compute instance, or even a set of permissions. Available resource types depend on the provider you use (e.g., AWS, GCP, Digital Ocean).</p> <p>All resources are defined <strong>declaratively</strong>. You don&rsquo;t write pseudocode that creates servers. Instead, you define the desired outcome. It&rsquo;s up to Terraform to figure out how to configure what you specified. For example, it&rsquo;s smart enough to create a resource only if it doesn&rsquo;t exist.</p> <p>Resources can refer to each other using the full name.</p> <p>Besides resources, a project can also specify <strong>input variables</strong> to be filled by the user and <strong>output values</strong> that can be printed on the screen or stored in a file.</p> <p>There are also <strong>data sources</strong> that don&rsquo;t create anything but read existing remote resources.</p> <p>You can find all the available resources in the specific provider documentation. Here&rsquo;s an example for <a href="proxy.php?url=https%3A%2F%2Fwww.terraform.io%2Fdocs%2Fproviders%2Fgoogle%2Findex.html" target="_blank">Google Cloud Platform</a>.</p> <p>Two basic commands that you need to know are <code>terraform plan</code> and <code>terraform apply</code>.</p> <p><code>apply</code> applies all resources defined in the current directory across all files with the <code>.tf</code> extension. The <code>plan</code> command is a &ldquo;dry-run&rdquo; mode that prints all changes that would be applied by <code>apply</code>.</p> <p>After applying changes, you will find a <code>terraform.tfstate</code> file in the same directory. This file holds a local &ldquo;<a href="proxy.php?url=https%3A%2F%2Fwww.terraform.io%2Fdocs%2Fstate%2Findex.html" target="_blank">state</a>&rdquo; of your infrastructure.</p> <h2 id="google-cloud-project">Google Cloud Project</h2> <p>Our Terraform configuration creates a new GCP project. It&rsquo;s completely separated from your other projects and is easy to clean up.</p> <p>Because some resources go beyond the free tier, you need a <em>Billing Account</em>. It can be an account with a linked credit card, but the $300 credit for new accounts also works.</p> <p>The basic part of the project definition looks like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">provider</span> <span class="s2">&#34;google&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> project</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">project</span> </span></span><span class="line"><span class="cl"><span class="n"> region</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">region</span> </span></span><span class="line"><span class="cl">} </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;google_billing_account&#34; &#34;account&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> display_name</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">billing_account</span> </span></span><span class="line"><span class="cl">} </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_project&#34; &#34;project&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> name</span> <span class="o">=</span> <span class="s2">&#34;Wild Workouts&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> project_id</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">project</span> </span></span><span class="line"><span class="cl"><span class="n"> billing_account</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">google_billing_account</span><span class="p">.</span><span class="k">account</span><span class="p">.</span><span class="k">id</span> </span></span><span class="line"><span class="cl">} </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_project_iam_member&#34; &#34;owner&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> role</span> <span class="o">=</span> <span class="s2">&#34;roles/owner&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> member</span> <span class="o">=</span> <span class="s2">&#34;user:${var.user}&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> depends_on</span> <span class="o">=</span> <span class="p">[</span><span class="k">google_project</span><span class="p">.</span><span class="k">project</span><span class="p">]</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fproject.tf%23L1" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/project.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fproject.tf%23L1" target="_blank">Full source</a> </div> <p>Let&rsquo;s consider what each <em>block</em> does:</p> <ol> <li>Enables the GCP provider. Sets <strong>project</strong> name and chosen <strong>region</strong> from variables. These two fields are &ldquo;inherited&rdquo; by all resources unless overridden.</li> <li>Finds a billing account with the display name provided by the variable.</li> <li>Creates a new google project linked with the billing account. <strong>Note the reference to the account ID.</strong></li> <li>Adds your user as the owner of the project. <strong>Note the string interpolation.</strong></li> </ol> <h2 id="enabling-apis">Enabling APIs</h2> <p>On a fresh GCP project, you can&rsquo;t use most services right away. You first have to enable the API for each of them. You can enable an API by clicking a button in the GCP Console or do the same in <a href="proxy.php?url=https%3A%2F%2Fwww.terraform.io%2Fdocs%2Fproviders%2Fgoogle%2Fr%2Fgoogle_project_service.html" target="_blank">Terraform</a>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_project_service&#34; &#34;compute&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> service</span> <span class="o">=</span> <span class="s2">&#34;[compute.googleapis.com](http://compute.googleapis.com/)&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> depends_on</span> <span class="o">=</span> <span class="p">[</span><span class="k">google_project</span><span class="p">.</span><span class="k">project</span><span class="p">]</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fproject.tf%23L22" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/project.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fproject.tf%23L22" target="_blank">Full source</a> </div> <p>We enable the following APIs:</p> <ul> <li>Cloud Source Repositories</li> <li>Cloud Build</li> <li>Container Registry</li> <li>Compute</li> <li>Cloud Run</li> <li>Firebase</li> <li>Firestore</li> </ul> <h3 id="a-note-on-dependencies">A note on dependencies</h3> <p>If you&rsquo;re wondering about the <code>depends_on</code> line, it sets an <strong>explicit dependency</strong> between the service and the project. Terraform detects dependencies between resources if they refer to each other. In the first snippet, you can see this with the billing account that&rsquo;s referenced by the project:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;google_billing_account&#34; &#34;account&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> display_name</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">billing_account</span> </span></span><span class="line"><span class="cl">} </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_project&#34; &#34;project&#34;</span> {<span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> # ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n"> billing_account</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">google_billing_account</span><span class="p">.</span><span class="k">account</span><span class="p">.</span><span class="k">id</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><p>In the <code>google_project_service</code> resource, we don&rsquo;t use <code>project</code> anywhere because it&rsquo;s already set in the <code>provider</code> block. Instead, we use <code>depends_on</code> to specify an explicit dependency. This line tells Terraform to wait before creating the resource until the project is correctly created.</p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/complete-setup-of-serverless-application/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;serverless;firebase;firestore;cloudrun;googlecloud;gcloud;terraform;cicd"> <input type="hidden" name="fields[blog_series]" value="Modern Business Software in Go"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h2 id="cloud-run">Cloud Run</h2> <img title="" loading="lazy" decoding="async" class="img img-center" width="1170" height="406" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fcomplete-setup-of-serverless-application%2Fcloud-run_hu4f90fb7ac7bfe252290525205339b5e3_71629_1170x406_resize_q80_h2_lanczos_3.webp" alt="Cloud Run services" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fcomplete-setup-of-serverless-application%5C%2Fcloud-run_hu4f90fb7ac7bfe252290525205339b5e3_71629_1170x406_resize_lanczos_3.png"" /> <p>In Cloud Run, a service is a set of Docker containers with a common endpoint, exposing a single port (HTTP or gRPC). Each service is automatically scaled, depending on the incoming traffic. You can choose the maximum number of containers and how many requests each container can handle.</p> <p>It&rsquo;s also possible to connect services with Google Cloud Pub/Sub. Our project doesn&rsquo;t use it yet, but we will introduce it in future versions.</p> <p>You are charged only for the computing resources you use, so when a request is being processed or when the container starts.</p> <p>Wild Workouts consists of 3 services: <em>trainer</em>, <em>trainings</em>, and <em>users</em>. We decided to serve public API with HTTP and internal API with gRPC.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="2195" height="1821" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fcomplete-setup-of-serverless-application%2Fservices-diagram_hu7a5b5d5cc96d9ada230aeb87ed5b9159_202896_2195x1821_resize_q80_h2_lanczos.webp" alt="Services" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fcomplete-setup-of-serverless-application%5C%2Fservices-diagram_hu7a5b5d5cc96d9ada230aeb87ed5b9159_202896_2195x1821_resize_q80_lanczos.jpg"" /> <p>Because you can&rsquo;t expose two separate ports from a single service, we have to expose two containers per service (except trainings, which doesn&rsquo;t have an internal API at the moment).</p> <p>We deploy 5 services in total, each with a similar configuration. Following the <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FDon%2527t_repeat_yourself" target="_blank">DRY</a> principle, the common Terraform configuration is encapsulated in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Ftree%2Fmaster%2Fterraform%2Fservice" target="_blank">service module</a>.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>A <a href="proxy.php?url=https%3A%2F%2Fwww.terraform.io%2Fdocs%2Fconfiguration%2Fmodules.html" target="_blank">Module</a> in Terraform is a separate set of files in a subdirectory. Think of it as a container for a group of resources. It can have its own input variables and output values.</p> <p>Any module can call other modules by using the <code>module</code> block and passing a path to the directory in the <code>source</code> field. A single module can be called multiple times.</p> <p>Resources defined in the main working directory are considered to be in a <em>root module</em>.</p> </p></div> </div> <p>The module holds a definition of a single generic Cloud Run service and is used multiple times in <code>cloud-run.tf</code>. It exposes several variables, e.g., name and type of the server (HTTP or gRPC).</p> <p>A service exposing gRPC is the simpler one:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">module</span> <span class="k">cloud_run_trainer_grpc</span> { </span></span><span class="line"><span class="cl"><span class="n"> source</span> <span class="o">=</span> <span class="s2">&#34;./service&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> project</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">project</span> </span></span><span class="line"><span class="cl"><span class="n"> location</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">region</span> </span></span><span class="line"><span class="cl"><span class="n"> dependency</span> <span class="o">=</span> <span class="k">null_resource</span><span class="p">.</span><span class="k">init_docker_images</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> name</span> <span class="o">=</span> <span class="s2">&#34;trainer&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> protocol</span> <span class="o">=</span> <span class="s2">&#34;grpc&#34;</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fcloud-run.tf%23L1" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/cloud-run.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fcloud-run.tf%23L1" target="_blank">Full source</a> </div> <p>The <code>protocol</code> is passed to the <code>SERVER_TO_RUN</code> environment variable, which decides what server is run by the service.</p> <p>Compare this with an HTTP service definition. We use the same module, but with additional environment variables. We need to add them because public services contact internal services via the gRPC API and need to know their addresses.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">module</span> <span class="k">cloud_run_trainings_http</span> { </span></span><span class="line"><span class="cl"><span class="n"> source</span> <span class="o">=</span> <span class="s2">&#34;./service&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> project</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">project</span> </span></span><span class="line"><span class="cl"><span class="n"> location</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">region</span> </span></span><span class="line"><span class="cl"><span class="n"> dependency</span> <span class="o">=</span> <span class="k">null_resource</span><span class="p">.</span><span class="k">init_docker_images</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> name</span> <span class="o">=</span> <span class="s2">&#34;trainings&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> protocol</span> <span class="o">=</span> <span class="s2">&#34;http&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> auth</span> <span class="o">=</span> <span class="kt">false</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> envs</span> <span class="o">=</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> { </span></span><span class="line"><span class="cl"><span class="n"> name</span> <span class="o">=</span> <span class="s2">&#34;TRAINER_GRPC_ADDR&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> value</span> <span class="o">=</span> <span class="k">module</span><span class="p">.</span><span class="k">cloud_run_trainer_grpc</span><span class="p">.</span><span class="k">endpoint</span> </span></span><span class="line"><span class="cl"> }<span class="p">,</span> </span></span><span class="line"><span class="cl"> { </span></span><span class="line"><span class="cl"><span class="n"> name</span> <span class="o">=</span> <span class="s2">&#34;USERS_GRPC_ADDR&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> value</span> <span class="o">=</span> <span class="k">module</span><span class="p">.</span><span class="k">cloud_run_users_grpc</span><span class="p">.</span><span class="k">endpoint</span> </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fcloud-run.tf%23L31" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/cloud-run.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fcloud-run.tf%23L31" target="_blank">Full source</a> </div> <p>The reference: <code>module.cloud_run_trainer_grpc.endpoint</code> points to <code>endpoint</code> output defined in the <code>service</code> module:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">output</span> <span class="k">endpoint</span> { </span></span><span class="line"><span class="cl"><span class="n"> value</span> <span class="o">=</span> <span class="s2">&#34;${trimprefix(google_cloud_run_service.service.status[0].url, &#34;https://&#34;)}:443&#34;</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fservice%2Foutputs.tf%23L5" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/service/outputs.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fservice%2Foutputs.tf%23L5" target="_blank">Full source</a> </div> <p>Using environment variables is an easy way to make services aware of each other. With more complex connections and more services, it would probably be best to implement some kind of service discovery. We may cover this in a future post.</p> <p>If you&rsquo;re curious about the <code>dependency</code> variable, see <em>In-depth</em> below for details.</p> <h3 id="cloud-run-permissions">Cloud Run Permissions</h3> <p>Cloud Run services have authentication enabled by default. Setting the <code>auth = false</code> variable in the <code>service</code> module adds additional IAM policy for the service, making it accessible for the public. We do this just for the HTTP APIs.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">data</span> <span class="s2">&#34;google_iam_policy&#34; &#34;noauth&#34;</span> { </span></span><span class="line"><span class="cl"> <span class="k">binding</span> { </span></span><span class="line"><span class="cl"><span class="n"> role</span> <span class="o">=</span> <span class="s2">&#34;roles/run.invoker&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> members</span> <span class="o">=</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;allUsers&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl">} </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_cloud_run_service_iam_policy&#34; &#34;noauth_policy&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> count</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">auth</span> <span class="err">?</span> <span class="m">0</span> <span class="err">:</span> <span class="m">1</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> location</span> <span class="o">=</span> <span class="k">google_cloud_run_service</span><span class="p">.</span><span class="k">service</span><span class="p">.</span><span class="k">location</span> </span></span><span class="line"><span class="cl"><span class="n"> service</span> <span class="o">=</span> <span class="k">google_cloud_run_service</span><span class="p">.</span><span class="k">service</span><span class="p">.</span><span class="k">name</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> policy_data</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">google_iam_policy</span><span class="p">.</span><span class="k">noauth</span><span class="p">.</span><span class="k">policy_data</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fservice%2Fservice.tf%23L49" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/service/service.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fservice%2Fservice.tf%23L49" target="_blank">Full source</a> </div> <p>Note the following line:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="n">count</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">auth</span> <span class="err">?</span> <span class="m">0</span> <span class="err">:</span> <span class="m">1</span> </span></span></code></pre></div><p>This line is Terraform&rsquo;s way of making an <code>if</code> statement. <code>count</code> defines how many copies of a resource Terraform should create. It skips resources with a count of <code>0</code>.</p> <h2 id="firestore">Firestore</h2> <p>We picked Firestore as the database for Wild Workouts. See <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F">our first post</a> for reasons behind this.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Since writing this article, our recommendation has changed: <strong>just use PostgreSQL</strong>. It&rsquo;s easier to operate and simpler to migrate between cloud providers if needed. See the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%23state-of-this-article-in-2026">State of this article in 2026</a> note in our first post for more context. </p></div> </div> <p>Firestore works in two modes: Native or Datastore. You have to decide early on, as the choice is permanent after your first write to the database.</p> <p>You can pick the mode in the GCP Console GUI, but we wanted to make the setup fully automated. In Terraform, there&rsquo;s a <code>google_project_service</code> resource available, but it enables Datastore mode. Sadly, we can&rsquo;t use it since we want to use Native mode.</p> <p>A workaround is to use the <code>gcloud</code> command to enable it (note the <code>alpha</code> version).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcloud alpha firestore databases create </span></span></code></pre></div><p>To run this command, we use the <a href="proxy.php?url=https%3A%2F%2Fwww.terraform.io%2Fdocs%2Fproviders%2Fnull%2Fresource.html" target="_blank">null resource</a>. It&rsquo;s a special kind of resource that lets you run custom provisioners locally or on remote servers. A <a href="proxy.php?url=https%3A%2F%2Fwww.terraform.io%2Fdocs%2Fprovisioners%2Findex.html" target="_blank">provisioner</a> is a command or other software that makes changes to the system.</p> <p>We use <a href="proxy.php?url=https%3A%2F%2Fwww.terraform.io%2Fdocs%2Fprovisioners%2Flocal-exec.html" target="_blank">local-exec provisioner</a>, which is simply executing a bash command on the local system. In our case, it&rsquo;s one of the targets defined in <code>Makefile</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;null_resource&#34; &#34;enable_firestore&#34;</span> { </span></span><span class="line"><span class="cl"> <span class="k">provisioner</span> <span class="s2">&#34;local-exec&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> command</span> <span class="o">=</span> <span class="s2">&#34;make firestore&#34;</span> </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> depends_on</span> <span class="o">=</span> <span class="p">[</span><span class="k">google_firebase_project_location</span><span class="p">.</span><span class="k">default</span><span class="p">]</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Ffirestore.tf%23L1" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/firestore.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Ffirestore.tf%23L1" target="_blank">Full source</a> </div> <p>Firestore requires creating all composite indexes upfront. It&rsquo;s also available as a <a href="proxy.php?url=https%3A%2F%2Fwww.terraform.io%2Fdocs%2Fproviders%2Fgoogle%2Fr%2Ffirestore_index.html" target="_blank">Terraform resource</a>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_firestore_index&#34; &#34;trainings_user_time&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> collection</span> <span class="o">=</span> <span class="s2">&#34;trainings&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">fields</span> { </span></span><span class="line"><span class="cl"><span class="n"> field_path</span> <span class="o">=</span> <span class="s2">&#34;UserUuid&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> order</span> <span class="o">=</span> <span class="s2">&#34;ASCENDING&#34;</span> </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">fields</span> { </span></span><span class="line"><span class="cl"><span class="n"> field_path</span> <span class="o">=</span> <span class="s2">&#34;Time&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> order</span> <span class="o">=</span> <span class="s2">&#34;ASCENDING&#34;</span> </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">fields</span> { </span></span><span class="line"><span class="cl"><span class="n"> field_path</span> <span class="o">=</span> <span class="s2">&#34;__name__&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> order</span> <span class="o">=</span> <span class="s2">&#34;ASCENDING&#34;</span> </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> depends_on</span> <span class="o">=</span> <span class="p">[</span><span class="k">null_resource</span><span class="p">.</span><span class="k">enable_firestore</span><span class="p">]</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Ffirestore.tf%23L9" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/firestore.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Ffirestore.tf%23L9" target="_blank">Full source</a> </div> <p>Note the explicit <code>depends_on</code> that points to the <code>null_resource</code> creating database.</p> <h2 id="firebase">Firebase</h2> <p>Firebase provides us with frontend application hosting and authentication.</p> <p>Currently, Terraform supports only part of Firebase API, and some of it is still in beta. That&rsquo;s why we need to enable the <code>google-beta</code> provider:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">provider</span> <span class="s2">&#34;google-beta&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> project</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">project</span> </span></span><span class="line"><span class="cl"><span class="n"> region</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">region</span> </span></span><span class="line"><span class="cl"><span class="n"> credentials</span> <span class="o">=</span> <span class="k">base64decode</span><span class="p">(</span><span class="k">google_service_account_key</span><span class="p">.</span><span class="k">firebase_key</span><span class="p">.</span><span class="k">private_key</span><span class="p">)</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Ffirebase.tf%23L1" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/firebase.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Ffirebase.tf%23L1" target="_blank">Full source</a> </div> <p>Then we define the project, <a href="proxy.php?url=https%3A%2F%2Ffirebase.google.com%2Fdocs%2Fprojects%2Flocations" target="_blank">project&rsquo;s location</a> and the web application.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_firebase_project&#34; &#34;default&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> provider</span> <span class="o">=</span> <span class="k">google</span><span class="err">-</span><span class="k">beta</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> depends_on</span> <span class="o">=</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="k">google_project_service</span><span class="p">.</span><span class="k">firebase</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="k">google_project_iam_member</span><span class="p">.</span><span class="k">service_account_firebase_admin</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl">} </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_firebase_project_location&#34; &#34;default&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> provider</span> <span class="o">=</span> <span class="k">google</span><span class="err">-</span><span class="k">beta</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> location_id</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">firebase_location</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> depends_on</span> <span class="o">=</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="k">google_firebase_project</span><span class="p">.</span><span class="k">default</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl">} </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_firebase_web_app&#34; &#34;wild_workouts&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> provider</span> <span class="o">=</span> <span class="k">google</span><span class="err">-</span><span class="k">beta</span> </span></span><span class="line"><span class="cl"><span class="n"> display_name</span> <span class="o">=</span> <span class="s2">&#34;Wild Workouts&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> depends_on</span> <span class="o">=</span> <span class="p">[</span><span class="k">google_firebase_project</span><span class="p">.</span><span class="k">default</span><span class="p">]</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Ffirebase.tf%23L25" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/firebase.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Ffirebase.tf%23L25" target="_blank">Full source</a> </div> <p>Authentication management still lacks a Terraform API, so you have to enable it manually in the <a href="proxy.php?url=https%3A%2F%2Fconsole.firebase.google.com%2Fproject%2F_%2Fauthentication%2Fproviders" target="_blank">Firebase Console</a>. Firebase authentication is the only thing we found no way to automate.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="2398" height="1178" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fcomplete-setup-of-serverless-application%2Ffirebase-authentication_hue2a9da57b5a91fa5db9e24d59c290335_175263_2398x1178_resize_q80_h2_lanczos_3.webp" alt="Cloud Run services" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fcomplete-setup-of-serverless-application%5C%2Ffirebase-authentication_hue2a9da57b5a91fa5db9e24d59c290335_175263_2398x1178_resize_lanczos_3.png"" /> <h3 id="firebase-routing">Firebase Routing</h3> <p>Firebase also handles public routing to services. Thanks to this the frontend application can call API with <code>/api/trainer</code> instead of <a href="proxy.php?url=https%3A%2F%2Ftrainer-grpc-lmned2eqeq-ew.a.run.app%2F" target="_blank"><code>https://trainer-http-smned2eqeq-ew.a.run.app</code></a>. These routes are defined in <code>web/firebase.json</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;rewrites&#34;</span><span class="err">:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="s2">&#34;/api/trainer{,/**}&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;run&#34;</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;serviceId&#34;</span><span class="p">:</span> <span class="s2">&#34;trainer-http&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;region&#34;</span><span class="p">:</span> <span class="s2">&#34;europe-west1&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">]</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fweb%2Ffirebase.json%23L8" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/web/firebase.json</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fweb%2Ffirebase.json%23L8" target="_blank">Full source</a> </div> <h2 id="cloud-build">Cloud Build</h2> <p>Cloud Build is our Continuous Delivery pipeline. It has to be enabled for a repository, so we define a <a href="proxy.php?url=https%3A%2F%2Fwww.terraform.io%2Fdocs%2Fproviders%2Fgoogle%2Fr%2Fcloudbuild_trigger.html" target="_blank">trigger</a> in Terraform and the <a href="proxy.php?url=https%3A%2F%2Fwww.terraform.io%2Fdocs%2Fproviders%2Fgoogle%2Fr%2Fsourcerepo_repository.html" target="_blank">repository</a> itself.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_sourcerepo_repository&#34; &#34;wild_workouts&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> name</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">repository_name</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> depends_on</span> <span class="o">=</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="k">google_project_service</span><span class="p">.</span><span class="k">source_repo</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl">} </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_cloudbuild_trigger&#34; &#34;trigger&#34;</span> { </span></span><span class="line"><span class="cl"> <span class="k">trigger_template</span> { </span></span><span class="line"><span class="cl"><span class="n"> branch_name</span> <span class="o">=</span> <span class="s2">&#34;master&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> repo_name</span> <span class="o">=</span> <span class="k">google_sourcerepo_repository</span><span class="p">.</span><span class="k">wild</span><span class="err">-</span><span class="k">workouts</span><span class="p">.</span><span class="k">name</span> </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> filename</span> <span class="o">=</span> <span class="s2">&#34;cloudbuild.yaml&#34;</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Frepo.tf%23L1" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/repo.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Frepo.tf%23L1" target="_blank">Full source</a> </div> <p>The build configuration has to be defined in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fcloudbuild.yaml" target="_blank"><code>cloudbuild.yaml</code></a> file committed to the repository. We defined a couple of steps in the pipeline:</p> <ul> <li>Linting (go vet): this step could be extended in the future with running tests and all kinds of static checks and linters</li> <li>Building docker images</li> <li>Deploying docker images to Cloud Run</li> <li>Deploying web app to Firebase hosting</li> </ul> <p>We keep several services in one repository, and their building and deployment are almost the same. To reduce repetition, there are a few helper bash scripts in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Ftree%2Fmaster%2Fscripts" target="_blank"><code>scripts</code></a> directory.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h3 id="should-i-use-a-monorepo">Should I use a monorepo?</h3> <p>We decided to keep all services in one repository. We did this mainly because Wild Workouts is an example project, and it&rsquo;s much easier to set up everything this way. We can also easily share common code (e.g., setting up gRPC and HTTP servers).</p> <p>From our experience, using a single repository is a great starting point for most projects. It&rsquo;s essential, though, that all services are entirely isolated from each other. If needed, we could easily split them into separate repositories. We might show this in future articles in this series.</p> <p>The biggest disadvantage of the current setup is that all services and the frontend application are deployed at the same time. That&rsquo;s usually not what you want when working with microservices with a bigger team. But it&rsquo;s probably acceptable when you&rsquo;re just starting out and creating an MVP.</p> </p></div> </div> <p>As usual in CI/CD tools, the <a href="proxy.php?url=https%3A%2F%2Fcloud.google.com%2Fcloud-build%2Fdocs%2Fbuild-config" target="_blank">Build configuration</a> is defined in YAML. Let&rsquo;s see the whole configuration for just one service, to reduce some noise.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">steps</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainer-lint</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">golang</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/lint.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainer-docker</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloud-builders/docker</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/build-docker.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;trainer&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;$PROJECT_ID&#34;</span><span class="p">]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer-lint]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainer-http-deploy</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloud-builders/gcloud</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/deploy.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer, http, &#34;$PROJECT_ID&#34;]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer-docker]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainer-grpc-deploy</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloud-builders/gcloud</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/deploy.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer, grpc, &#34;$PROJECT_ID&#34;]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer-docker]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">options</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">env</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s1">&#39;GO111MODULE=on&#39;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">machineType</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;N1_HIGHCPU_8&#39;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">images</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="s1">&#39;gcr.io/$PROJECT_ID/trainer&#39;</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fcloudbuild.yaml%23L1" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/cloudbuild.yaml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fcloudbuild.yaml%23L1" target="_blank">Full source</a> </div> <p>A single step definition is pretty short:</p> <ul> <li><code>id</code> is a unique identifier of a step. It can be used in <code>waitFor</code> array to specify dependencies between steps (steps are running in parallel by default).</li> <li><code>name</code> is the name of a docker image that will be run for this step.</li> <li><code>entrypoint</code> works like in Docker images, so it&rsquo;s a command that executes when the container starts. We use bash scripts to keep the YAML definition short, but this can be any bash command.</li> <li><code>args</code> will be passed to <code>entrypoint</code> as arguments.</li> </ul> <p>In <code>options</code>, we override the machine type to make our builds run faster. There&rsquo;s also an environment variable to force the use of Go modules.</p> <p>The <code>images</code> list defines Docker images that should be pushed to Container Registry. In the example above, the Docker image is built in the <code>trainer-docker</code> step.</p> <h3 id="deploys">Deploys</h3> <p>Deploys to Cloud Run use the <code>gcloud run deploy</code> command. Cloud Build builds the Docker image in the previous step, so we can use the latest image from the registry.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">trainer-http-deploy</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/cloud-builders/gcloud</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">entrypoint</span><span class="p">:</span><span class="w"> </span><span class="l">./scripts/deploy.sh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer, http, &#34;$PROJECT_ID&#34;]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainer-docker]</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fcloudbuild.yaml%23L31" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/cloudbuild.yaml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fcloudbuild.yaml%23L31" target="_blank">Full source</a> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcloud run deploy <span class="s2">&#34;</span><span class="nv">$service</span><span class="s2">-</span><span class="nv">$server_to_run</span><span class="s2">&#34;</span> <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> --image <span class="s2">&#34;gcr.io/</span><span class="nv">$project_id</span><span class="s2">/</span><span class="nv">$service</span><span class="s2">&#34;</span> <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> --region europe-west1 <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> --platform managed </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fscripts%2Fdeploy.sh%23L1" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/scripts/deploy.sh</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fscripts%2Fdeploy.sh%23L1" target="_blank">Full source</a> </div> <p>The frontend is deployed with the <code>firebase</code> command. There&rsquo;s no need to use a helper script here, as there&rsquo;s just one frontend application.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">gcr.io/$PROJECT_ID/firebase</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;deploy&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;--project=$PROJECT_ID&#39;</span><span class="p">]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">dir</span><span class="p">:</span><span class="w"> </span><span class="l">web</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">waitFor</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">web-build]</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fcloudbuild.yaml%23L73" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/cloudbuild.yaml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fcloudbuild.yaml%23L73" target="_blank">Full source</a> </div> <p>This step uses the <a href="proxy.php?url=http%3A%2F%2Fgcr.io%2F%24PROJECT_ID%2Ffirebase" target="_blank"><code>gcr.io/$PROJECT_ID/firebase</code></a> Docker image. It doesn&rsquo;t exist by default, so we use another <code>null_resource</code> to build it from <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FGoogleCloudPlatform%2Fcloud-builders-community.git" target="_blank">cloud-builders-community</a>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;null_resource&#34; &#34;firebase_builder&#34;</span> { </span></span><span class="line"><span class="cl"> <span class="k">provisioner</span> <span class="s2">&#34;local-exec&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> command</span> <span class="o">=</span> <span class="s2">&#34;make firebase_builder&#34;</span> </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> depends_on</span> <span class="o">=</span> <span class="p">[</span><span class="k">google_project_service</span><span class="p">.</span><span class="k">container_registry</span><span class="p">]</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Frepo.tf%23L20" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/repo.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Frepo.tf%23L20" target="_blank">Full source</a> </div> <h3 id="cloud-build-permissions">Cloud Build Permissions</h3> <p>All required permissions are defined in <code>iam.tf</code>. Cloud Build needs permissions for <a href="proxy.php?url=https%3A%2F%2Fcloud.google.com%2Fcloud-build%2Fdocs%2Fdeploying-builds%2Fdeploy-cloud-run" target="_blank">Cloud Run</a> (to deploy backend services) and <a href="proxy.php?url=https%3A%2F%2Fcloud.google.com%2Fcloud-build%2Fdocs%2Fdeploying-builds%2Fdeploy-firebase" target="_blank">Firebase</a> (to deploy frontend application).</p> <p>First, we define account names as local variables to make the file more readable. If you&rsquo;re wondering where these names come from, you can find them in the <a href="proxy.php?url=https%3A%2F%2Fcloud.google.com%2Fcloud-build%2Fdocs%2Fdeploying-builds%2Fdeploy-cloud-run" target="_blank">Cloud Build documentation</a>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">locals</span> { </span></span><span class="line"><span class="cl"><span class="n"> cloud_build_member</span> <span class="o">=</span> <span class="s2">&#34;serviceAccount:${google_project.project.number}@cloudbuild.gserviceaccount.com&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> compute_account</span> <span class="o">=</span> <span class="s2">&#34;projects/${var.project}/serviceAccounts/${google_project.project.number}[email protected]&#34;</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fiam.tf%23L1" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/iam.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fiam.tf%23L1" target="_blank">Full source</a> </div> <p>We then define all permissions, as described in the documentation. For example, here&rsquo;s <strong>Cloud Build member</strong> with <strong>Cloud Run Admin role</strong>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_project_iam_member&#34; &#34;cloud_run_admin&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> role</span> <span class="o">=</span> <span class="s2">&#34;roles/run.admin&#34;</span> </span></span><span class="line"><span class="cl"><span class="n"> member</span> <span class="o">=</span> <span class="k">local</span><span class="p">.</span><span class="k">cloud_build_member</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> depends_on</span> <span class="o">=</span> <span class="p">[</span><span class="k">google_project_service</span><span class="p">.</span><span class="k">cloud_build</span><span class="p">]</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fiam.tf%23L20" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/iam.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fiam.tf%23L20" target="_blank">Full source</a> </div> <h2 id="dockerfiles">Dockerfiles</h2> <p>We have several Dockerfiles defined in the project in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Ftree%2Fmaster%2Fdocker" target="_blank"><code>docker</code></a> directory.</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fdocker%2Fapp%2FDockerfile" target="_blank"><code>app</code></a> - Go service image for local development. It uses reflex for hot code recompilation. See <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-docker-dev-environment-with-go-modules-and-live-code-reloading%2F">our post about local Go environment</a> to learn how it works.</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fdocker%2Fapp-prod%2FDockerfile" target="_blank"><code>app-prod</code></a> - production image for Go services. It builds the Go binary for a given service and runs it.</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fdocker%2Fweb%2FDockerfile" target="_blank"><code>web</code></a> - frontend application image for local development.</li> </ul> <h2 id="setup">Setup</h2> <p>Usually, you would execute a Terraform project with <code>terraform apply</code>. This command applies all resources in the current directory.</p> <p>Our example is a bit more complex because it sets up the whole GCP project along with dependencies. The Makefile orchestrates the setup.</p> <p>Requirements:</p> <ul> <li><code>git</code>, <code>gcloud</code>, and <code>terraform</code> installed on your system.</li> <li>Docker</li> <li>A GCP account with a Billing Account.</li> </ul> <h3 id="authentication">Authentication</h3> <p>There are two credentials that you need for the setup.</p> <p>Start with logging in with <code>gcloud</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcloud auth login </span></span></code></pre></div><p>Then you need to obtain <strong>Application Default Credentials</strong>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">gcloud auth application-default login </span></span></code></pre></div><p>This stores your credentials in a well-known place, so Terraform can use it.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h3 id="security-warning">Security warning!</h3> <p>Authenticating this way is the easiest approach and fine for local development, but you probably don&rsquo;t want to use it in a production setup.</p> <p>Instead, consider creating a service account with only the necessary permissions (depending on what your Terraform configuration does). You can then download the JSON key and store its path in the <code>GOOGLE_CLOUD_KEYFILE_JSON</code> environment variable.</p> </p></div> </div> <h3 id="picking-a-region">Picking a Region</h3> <p>During the setup, you need to pick <a href="proxy.php?url=https%3A%2F%2Fcloud.google.com%2Frun%2Fdocs%2Flocations" target="_blank">Cloud Run region</a> and <a href="proxy.php?url=https%3A%2F%2Ffirebase.google.com%2Fdocs%2Fprojects%2Flocations" target="_blank">Firebase location</a>. These two are not the same thing (see the lists on linked documentation).</p> <p>We picked <code>europe-west1</code> as the default region for Cloud Run. It&rsquo;s hardcoded on the repository in two files:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fscripts%2Fdeploy.sh%23L8" target="_blank"><code>scripts/deploy.sh</code></a> - for deploying the Cloud Run services</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fweb%2Ffirebase.json%23L13" target="_blank"><code>web/firebase.json</code></a> - for routing Cloud Run public APIs</li> </ul> <p>If you&rsquo;re fine with the defaults, you don&rsquo;t need to change anything. Just pick Firebase location that&rsquo;s close, i.e., <code>europe-west</code>.</p> <p>If you&rsquo;d like to use a different region, you need to update the files mentioned above. It&rsquo;s important to <strong>commit your changes</strong> on <code>master</code> before you run the setup. It&rsquo;s not enough to change them locally!</p> <h3 id="run-it">Run it!</h3> <p>Clone the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example" target="_blank">project repository</a> and enter the <code>terraform</code> directory from the command line. A single <code>make</code> command should be enough to start. See the included <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2FREADME.md" target="_blank">README</a> for more details.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h3 id="billing-warning">Billing warning!</h3> <p>This project goes beyond the Google Cloud Platform free tier. You need to have a billing account to use it. You can use the $300 free credit given to new accounts or create a new account in the <a href="proxy.php?url=https%3A%2F%2Fconsole.cloud.google.com%2Fbilling%2Fcreate" target="_blank">Billing section</a>.</p> <p>For pricing, see <a href="proxy.php?url=https%3A%2F%2Fcloud.google.com%2Frun%2Fpricing" target="_blank">Cloud Run Pricing</a>. The best way to estimate your project cost is to use the <a href="proxy.php?url=https%3A%2F%2Fcloud.google.com%2Fproducts%2Fcalculator" target="_blank">official calculator</a>.</p> <p>Wild Workouts should cost you up to $1 per month if you use it just occasionally. We recommend deleting the project after you&rsquo;re done playing with it.</p> <p>If you want to run it continuously, you should know that most of the cost comes from Cloud Build and depends on the number of builds (triggered by new commits on master). You can downgrade the <code>machineType</code> in <code>cloudbuild.yaml</code> to reduce some costs at the expense of longer build times.</p> <p>It&rsquo;s also a good idea to set up a Budget Alert in the Billing Account settings. You will then receive a notification if something triggers unexpected charges.</p> </p></div> </div> <p>At the end of the setup, a new Git remote called <code>google</code> will be added to your local repository. The master branch will be pushed to this remote, triggering your first Cloud Build.</p> <p>If you&rsquo;d like to make any changes to the project, you need to push to the correct origin, i.e., <code>git push google</code>. You can also update the <code>origin</code> remote with <code>git remote set-url</code>.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If you keep your code on GitHub, GitLab, or another platform, you can set up a mirror instead of hosting the repository on Cloud Source Repositories. </p></div> </div> <h2 id="in-depth">In-depth</h2> <h3 id="env-variables">Env variables</h3> <p>All available variables are defined in the <code>vars.tf</code> file. If a variable has no default value and you don&rsquo;t provide it, Terraform won&rsquo;t let you apply anything.</p> <p>There are several ways to supply variables for <code>terraform apply</code>. You can pass them individually with <code>-var</code> flag or all at once with <code>-var-file</code> flag. Terraform also looks for them in <code>TF_VAR_name</code> environment variables.</p> <p>We&rsquo;ve picked the last option for this project. We need to use the environment variables anyway in the Makefile and bash scripts. This way, there&rsquo;s just one source of truth.</p> <p>Make runs <code>set-envs.sh</code> at the beginning of the setup. It&rsquo;s a helper script that asks the user for all the required variables. These are then saved to the <code>.env</code> file. You can edit this file manually as well.</p> <p>Note Terraform does not automatically read it. It&rsquo;s sourced just before running <code>apply</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-makefile" data-lang="makefile"><span class="line"><span class="cl"><span class="nv">load_envs</span><span class="o">:=</span><span class="nb">source</span> ./.env </span></span><span class="line"><span class="cl"><span class="c"># ... </span></span></span><span class="line"><span class="cl"><span class="c"></span> </span></span><span class="line"><span class="cl"><span class="nf">apply</span><span class="o">:</span> </span></span><span class="line"><span class="cl"> <span class="si">${</span><span class="nv">load_envs</span><span class="si">}</span> <span class="o">&amp;&amp;</span> terraform apply </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2FMakefile%23L1" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/Makefile</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2FMakefile%23L1" target="_blank">Full source</a> </div> <h3 id="building-docker-images">Building docker images</h3> <p>Because we&rsquo;re setting up the project from scratch, we run into a dilemma: we need Docker images in the project&rsquo;s Container Registry to create Cloud Run instances, but we can&rsquo;t create the images without a project.</p> <p>You could set the image name in Terraform definition to an example image and then let Cloud Build overwrite it. But then each <code>terraform apply</code> would try to change them back to the original value.</p> <p>Our solution is to build the images based on the &ldquo;hello&rdquo; image first and then deploy Cloud Run instances. Then Cloud Build builds and deploys proper images, but the name stays the same.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;null_resource&#34; &#34;init_docker_images&#34;</span> { </span></span><span class="line"><span class="cl"> <span class="k">provisioner</span> <span class="s2">&#34;local-exec&#34;</span> { </span></span><span class="line"><span class="cl"><span class="n"> command</span> <span class="o">=</span> <span class="s2">&#34;make docker_images&#34;</span> </span></span><span class="line"><span class="cl"> } </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n"> depends_on</span> <span class="o">=</span> <span class="p">[</span><span class="k">google_project_service</span><span class="p">.</span><span class="k">container_registry</span><span class="p">]</span> </span></span><span class="line"><span class="cl">} </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fdocker-images.tf%23L1" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/docker-images.tf</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2Fdocker-images.tf%23L1" target="_blank">Full source</a> </div> <p>Note the <code>depends_on</code> that points at Container Registry API. It ensures Terraform won&rsquo;t start building images until there&rsquo;s a registry where it can push them.</p> <h3 id="destroying">Destroying</h3> <p>You can delete the entire project with <code>make destroy</code>. If you take a look in the Makefile, there&rsquo;s something unusual before <code>terraform destroy</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">terraform state rm <span class="s2">&#34;google_project_iam_member.owner&#34;</span> </span></span><span class="line"><span class="cl">terraform state rm <span class="s2">&#34;google_project_service.container_registry&#34;</span> </span></span><span class="line"><span class="cl">terraform state rm <span class="s2">&#34;google_project_service.cloud_run&#34;</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2FMakefile%23L76" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/terraform/Makefile</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2F3d2ce607c0bcddc2a7f9cb56193167c8543d57ec%2Fterraform%2FMakefile%23L76" target="_blank">Full source</a> </div> <p>These commands are a workaround for <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fhashicorp%2Fterraform%2Fissues%2F23547" target="_blank">Terraform&rsquo;s lack of a &ldquo;skip destroy&rdquo; feature</a>. They remove some resources from Terraform&rsquo;s local state file. Terraform won&rsquo;t destroy these resources because, as far as it&rsquo;s concerned, they don&rsquo;t exist at this point.</p> <p>We don&rsquo;t want to destroy the owner IAM policy, because if something goes wrong during the destroy, you will lock yourself out of the project and won&rsquo;t be able to access it.</p> <p>The other two lines relate to enabled APIs: there&rsquo;s a possible race condition where some resources would still use these APIs during destroy. By removing them from the state, we avoid this issue.</p> <h3 id="a-note-on-magic">A note on magic</h3> <p>Our goal was to make it as easy as possible to set up the whole project. Because of that, we had to cut some corners where Terraform is missing features or an API is not yet available. There&rsquo;s also some Makefile and bash magic involved that&rsquo;s not always easy to understand.</p> <p>I want to make this clear because you will likely encounter similar dilemmas in your projects. You will need to choose between fully automated solutions glued together with alpha APIs or plain Terraform with some manual steps documented in the project.</p> <p>Both approaches have their place. For example, this project is straightforward to set up multiple times with the same configuration. If you&rsquo;d like to create an exact copy of the production environment, you can have a completely separate project working within minutes.</p> <p>If you keep just one or two environments, you won&rsquo;t need to recreate them every week. It&rsquo;s then probably fine to stick to the available APIs and document the rest.</p> <h2 id="is-serverless-the-way-to-go">Is serverless the way to go?</h2> <p>This project is our first approach to &ldquo;serverless&rdquo; deployment, and at first, we had some concerns. Is this all just hype, or is serverless the future?</p> <p>Wild Workouts is a fairly small project that might not show everything Cloud Run has to offer. Overall, we found it quite simple to set up, and it nicely hides all complexities. It&rsquo;s also more natural to work with than Cloud Functions.</p> <p>After the initial setup, there shouldn&rsquo;t be much infrastructure maintenance needed. You don&rsquo;t have to worry about keeping up a Docker registry or a Kubernetes cluster and can instead focus on creating the application.</p> <p>On the other hand, it&rsquo;s also quite limited in features, as only a few protocols are supported. It looks like a great fit for services with a REST API. The pricing model also seems reasonable, as you pay only for the resources you use.</p> <h2 id="what-about-vendor-lock-in">What about vendor lock-in?</h2> <p>The entire Terraform configuration is now tied to GCP, and there&rsquo;s no way around it. If we wanted to migrate the project to AWS, a new configuration would be needed.</p> <p>However, there are some universal concepts. Services running in Docker images can be deployed on any platform, whether it&rsquo;s a Kubernetes cluster or docker-compose. Most platforms also offer some kind of registry for images.</p> <p>Is writing a new Terraform configuration all that&rsquo;s required to migrate out of Google? Not really, as the application itself is coupled to Firebase and Authentication offered by GCP. We will show a better approach to this problem in future posts in this series.</p> <h2 id="whats-next">What&rsquo;s next?</h2> <p>This post just scratches the surface of Terraform. There are many advanced topics you should take a look at if you consider using it. For example, the <a href="proxy.php?url=https%3A%2F%2Fwww.terraform.io%2Fdocs%2Fstate%2Fremote.html" target="_blank">remote state</a> is necessary when working in a team.</p> <p>I hope this was a solid introduction. If you have any questions, please let us know in the comments below. We might even follow up with full posts to some of them, as the topic is broad enough.</p> <p>Next week, we&rsquo;re going to cover gRPC communication. 👋</p>Building a serverless application with Go, Google Cloud Run and Firebasehttps://threedots.tech/post/serverless-cloud-run-firebase-modern-go-application/Tue, 12 May 2020 00:00:00 +0200https://threedots.tech/post/serverless-cloud-run-firebase-modern-go-application/<!-- intro --> <p>Welcome to the first article from the series covering how to build business-oriented applications in Go! In this series, we want to show you how to build applications that are easy to develop, maintain, and fun to work with in the long term.</p> <p>This series doesn&rsquo;t focus too heavily on infrastructure and implementation details. But we need some foundation to build on later. In this article, we start by covering some basic tools from Google Cloud that can help us to do that.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="this-is-not-just-another-article-with-random-code-snippets">This is not just another article with random code snippets.</h4> <p>This post is part of a bigger series where we show how to build <strong>Go applications that are easy to develop, maintain, and fun to work with in the long term.</strong> We are doing it by sharing proven techniques based on many experiments we did with teams we lead and <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fddd-lite-in-go-introduction%2F%3Futm_source%3Dabout-wild-workouts%23thats-great-but-do-you-have-any-evidence-it-works">scientific research</a>.</p> <p>You can learn these patterns by building with us a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fserverless-cloud-run-firebase-modern-go-application%2F%3Futm_source%3Dabout-wild-workouts%23what-wild-workouts-can-do">fully functional</a> example Go web application &ndash; <strong>Wild Workouts</strong>.</p> <p>We did one thing differently &ndash; <strong>we included some subtle issues to the initial Wild Workouts implementation</strong>. Have we lost our minds to do that? Not yet. 😉 These issues are common for many Go projects. <strong>In the long term, these small issues become critical and stop adding new features.</strong></p> <p><strong>It&rsquo;s one of the essential skills of a senior or lead developer; you always need to keep long-term implications in mind.</strong></p> <p>We will fix them by <strong>refactoring</strong> Wild Workouts. In that way, you will quickly understand the techniques we share.</p> <p>Do you know that feeling after reading an article about some technique and trying implement it only to be blocked by some issues skipped in the guide? Cutting these details makes articles shorter and increases page views, but this is not our goal. Our goal is to create content that provides enough know-how to apply presented techniques. If you did not read <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">previous articles from the series</a> yet, we highly recommend doing that.</p> <p>We believe that in some areas, there are no shortcuts. If you want to build complex applications in a fast and efficient way, you need to spend some time learning that. If it was simple, we wouldn&rsquo;t have large amounts of scary legacy code.</p> <p>Here&rsquo;s <strong><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fseries%2Fmodern-business-software-in-go%2F%3Futm_source%3Dabout-wild-workouts">the full list of 14 articles</a></strong> released so far.</p> <p><strong>The full source code</strong> of Wild Workouts is available on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%3Futm_source%3Dabout-wild-workouts" target="_blank">GitHub</a>. Don&rsquo;t forget to leave a star for our project! ⭐</p> </p></div> </div> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="state-of-this-article-in-2026">State of this article in 2026</h4> <p>It&rsquo;s interesting to see how things have changed since we wrote this article in 2020.</p> <p>There were many changes on the frontend: Vue.js and Bootstrap are now obsolete in favor of React and Tailwind CSS.</p> <p>But as a contrast, <strong>not much has changed on the backend side!</strong> <br> This matters for long-term maintainability: you don&rsquo;t want to rewrite your backend every six years because a shiny new framework appeared. <br> With Go and solid architectural patterns, this stability is achievable.</p> <p>The only big difference is that we no longer recommend Firebase/Firestore by default. <br> Our advice now: <strong>just use PostgreSQL</strong>. <br> It&rsquo;s easier to operate and simpler to migrate between cloud providers if needed (or even host it on a VM).</p> <p>We still recommend abstracting your database behind interfaces as described in <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Frepository-pattern-in-go%2F">The Repository Pattern</a> article, which makes changing such decisions straightforward.</p> <p><strong>That&rsquo;s about it. The backend content here hasn&rsquo;t become outdated in six years, and it won&rsquo;t in the next six or more. That&rsquo;s the power of learning timeless patterns over chasing the newest tools. Your knowledge compounds over time instead of requiring you to constantly relearn everything.</strong></p> </p></div> </div> <h3 id="why-serverless">Why serverless?</h3> <p><strong>Running a Kubernetes cluster requires a lot of support from &ldquo;DevOps teams&rdquo;.</strong> Let&rsquo;s skip the fact that DevOps is not a job title for now.</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="900" height="504" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Fdevops_hu6c23818e2e2c7f5a06a24bb44ceeb1ed_71558_900x504_resize_q80_h2_lanczos.webp" alt="Devops" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Fdevops_hu6c23818e2e2c7f5a06a24bb44ceeb1ed_71558_900x504_resize_q80_lanczos.jpg"" /> <div class="code-ref"> <b>DevOps is a culture and mindset.</b> It is not a job title! <br><i>Slide from <a href="proxy.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DhNZZZyNrpiI" target='_blank'>The gordian knot</a> - Alberto Brandolini</i> </div></p> <p>Small applications that could easily run on one virtual machine now get deployed on highly complex Kubernetes clusters. All these clusters require significant maintenance.</p> <p>On the other hand, moving applications to containers has given us significant flexibility in building and deploying them. It enabled rapid <strong>deployments of hundreds of microservices</strong> with considerable <strong>autonomy</strong>. But the <strong>cost is high</strong>.</p> <p>Wouldn&rsquo;t it be great if a fully managed solution existed? 🤔</p> <p>Maybe your company already uses a managed Kubernetes cluster. If so, you probably know that even a managed cluster still requires substantial &ldquo;DevOps&rdquo; support.</p> <p>Maybe serverless? Well, splitting a big application into multiple, independent Lambdas (Cloud Functions) is a <strong>great way to create an unmaintainable cataclysm</strong>.</p> <p> <img title="" loading="lazy" decoding="async" class="img img-center" width="" height="" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Fdemon-architecture.svg" alt="Demon Architecture" onerror="this.onerror='null';this.src=''" /> <div class="code-ref"> You should probably rethink your architecture if it can be used for summoning demons. </div></p> <p>But wait, is it the only way to build serverless applications? No!</p> <h2 id="google-cloud-run">Google Cloud Run</h2> <p>The idea behind Google Cloud Run is simple: you <strong>just need to provide a Docker container, and Google Cloud runs it</strong>.<br> Inside this container, you can run an application written in any language that can expose a port with your HTTP or gRPC API.</p> <p>You&rsquo;re not limited to synchronous processing: you can also process Pub/Sub messages inside this container.</p> <p><strong>And that&rsquo;s all that you need from the infrastructure side. Google Cloud does all the magic.</strong> Based on the traffic, the container will automatically scale up and down. Sounds like a perfect solution?</p> <p>In practice, it is not so simple. Many articles show how to use Google Cloud Run, but they usually present <strong>small building blocks you can use to build an application</strong>. <strong>It&rsquo;s hard to combine all these pieces from multiple places into a fully working project</strong> (been there, done that).</p> <p>Most of these articles ignore the problem of vendor lock-in. The deployment method should be just an implementation detail. I covered this topic in the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fmicroservices-or-monolith-its-detail%2F"><em>Why using Microservices or Monolith can be just a detail?</em></a> article in 2018.</p> <p>But most importantly, using the newest and shiniest technologies doesn&rsquo;t mean your application won&rsquo;t become hated legacy in the next 3 months.</p> <p><strong>Serverless solves only infrastructure challenges. It doesn&rsquo;t stop you from building an application that is hard to maintain.</strong> I even have the impression that it&rsquo;s the opposite: these fancy applications sooner or later become the hardest to maintain.</p> <p><strong>For this series of articles, we created a fully functional, real-life application. You can deploy this application to Google Cloud with one command using Terraform. You can run a local copy with a single docker-compose command.</strong></p> <p>There is also one thing we&rsquo;re doing differently than others. <strong>We included some subtle issues that, from our observations, are common in Go projects.</strong> In the long term, these small issues become critical and prevent us from adding new features.</p> <p>Have we lost our minds? Not yet. &#x1f609; <strong>This approach will help you understand which issues you can solve and which techniques help.</strong> It&rsquo;s also a test for the practices we use. If something isn&rsquo;t a problem, why use any technique to solve it?</p> <h2 id="plan">Plan</h2> <p>In the next few articles, we cover all topics related to running the application on Google Cloud. In this part, we didn&rsquo;t add any issues or bad practices. &#x1f609; <strong>The first articles may be a bit basic if you already have some experience in Go. We want to ensure that if you&rsquo;re just starting with Go, you&rsquo;ll be able to follow the more complex topics that come next.</strong></p> <p>Next, we refactor parts of the application that handle business logic. <strong>This part will be much more complex.</strong></p> <div id="newsletter-content" class="admonition-content newsletter text-center"> <span class="h5 inline-block font-bold"> Don't miss new posts.<br>Join over 18k subscribers of our newsletter and get a <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"><b>free e-book</b></a>! </span> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-with-the-domain%2F"> <div class="book-container book-big"> <div class="book"> <div class="front"> <div class="cover img-crisp-edges"> <img loading="" decoding="async" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fimg%2Fgo-with-domain-cover-retina.jpg" alt="Cover" class="rounded-full inline img" height="424" width="300" /> </div> </div> <div class="left-side"> <h2> <span>Go With The Domain</span> <span>Three Dots Labs</span> </h2> </div> </div> </div> </a> <div id="newsletter-content" class="admonition-content newsletter row mb-10"> <div id="mlb2-217775508" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-217775508 formkit-form mx-auto"> <form class="ml-block-form md:col-10 lg:col-6 mx-auto text-center" action="proxy.php?url=https%3A%2F%2Fassets.mailerlite.com%2Fjsonp%2F1209776%2Fforms%2F139176224126666568%2Fsubscribe" data-code="" method="post" target="_blank" onsubmit="subscribedToNewsletter()"> <div class="ml-form-error notice warning" style="display: none;"> <div class="notice-body"><p>Failed to subscribe, please try again later.</p></div> </div> <div class="ml-form-formContent"> <div class="ml-form-fieldRow ml-last-item mb-6"> <div class="ml-field-group ml-field-name ml-validate-required"> <input aria-label="name" aria-required="true" type="text" class="form-control form-input" data-inputmask="" name="fields[name]" placeholder="Your name" autocomplete="given-name"> </div> </div> <div class="ml-form-fieldRow mb-6"> <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required"> <input aria-label="email" aria-required="true" type="email" class="form-control form-input" data-inputmask="" name="fields[email]" placeholder="E-mail" autocomplete="email"> </div> </div> </div> <input type="hidden" name="fields[ref_url]" value="https://threedots.tech/post/serverless-cloud-run-firebase-modern-go-application/"> <input type="hidden" name="fields[form]" value="blog-content"> <input type="hidden" name="fields[blog_tags]" value="go;golang;serverless;firebase;firestore;cloudrun;googlecloud;gcloud"> <input type="hidden" name="fields[blog_series]" value="Modern Business Software in Go"> <input type="hidden" name="fields[utm_source]" value=""> <input type="hidden" name="fields[utm_campaign]" value=""> <input type="hidden" name="fields[utm_content]" value=""> <input type="hidden" name="ml-submit" value="1"> <div class="ml-form-embedSubmit btn btn-primary rounded"> <button type="submit" class="primary"> Subscribe <svg class="arrow-right icon w-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!-- Font Awesome Free 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M190.5 66.9l22.2-22.2c9.4-9.4 24.6-9.4 33.9 0L441 239c9.4 9.4 9.4 24.6 0 33.9L246.6 467.3c-9.4 9.4-24.6 9.4-33.9 0l-22.2-22.2c-9.5-9.5-9.3-25 .4-34.3L311.4 296H24c-13.3 0-24-10.7-24-24v-32c0-13.3 10.7-24 24-24h287.4L190.9 101.2c-9.8-9.3-10-24.8-.4-34.3z"/></svg> </button> <button disabled="disabled" style="display: none;" type="button" class="loading" > <span class="ml-form-embedSubmitLoad"></span> <span class="sr-only">Loading...</span> </button> </div> <input type="hidden" name="anticsrf" value="true"> </form> </div> <span class="mt-2 text-sm font-bold text-center">🔒 We do not send spam. You can unsubscribe at any time!</span> </div> <script> function ml_webform_success_217775508() { window.top.location.href = '/mailing/confirmation-required/'; } </script> <script> window.onload = function() { fetch("https://assets.mailerlite.com/jsonp/1209776/forms/139176224126666568/takel"); }; </script> </div> <h2 id="running-the-project-locally">Running the project locally</h2> <p>The ability to run a project locally is <strong>critical for efficient development</strong>. It&rsquo;s frustrating when you can&rsquo;t check your changes quickly and easily.</p> <p>It&rsquo;s much harder to achieve it for projects built from hundreds of microservices. Fortunately, our project has only 5 services. &#x1f609; <strong>In Wild Workouts, we created a Docker Compose setup with live code reloading for both frontend and backend.</strong> For the frontend, we use a container with the <code>vue-cli-service serve</code> tool. For the backend, the situation is a bit more complex. In all containers, we run the <code>reflex</code> tool. <code>reflex</code> listens for code changes and triggers recompilation of the service. If you&rsquo;re interested in the details, you can find them in our <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fgo-docker-dev-environment-with-go-modules-and-live-code-reloading"><em>Go Docker dev environment with Go Modules and live code reloading</em></a> blog post.</p> <h3 id="requirements">Requirements</h3> <p>The only requirements needed to run the project are <a href="proxy.php?url=https%3A%2F%2Fwww.docker.com%2F" target="_blank">Docker</a> and <a href="proxy.php?url=https%3A%2F%2Fdocs.docker.com%2Fcompose%2F" target="_blank">Docker Compose</a>.</p> <h3 id="running">Running</h3> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example.git <span class="o">&amp;&amp;</span> <span class="nb">cd</span> wild-workouts-go-ddd-example </span></span></code></pre></div><p>And run Docker Compose:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker-compose up </span></span></code></pre></div><p>After downloading all JavaScript and Go dependencies, you should see a message with the frontend address:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">web_1 <span class="p">|</span> $ vue-cli-service serve </span></span><span class="line"><span class="cl">web_1 <span class="p">|</span> INFO Starting development server... </span></span><span class="line"><span class="cl">web_1 <span class="p">|</span> DONE Compiled successfully in 6315ms11:18:26 AM </span></span><span class="line"><span class="cl">web_1 <span class="p">|</span> </span></span><span class="line"><span class="cl">web_1 <span class="p">|</span> </span></span><span class="line"><span class="cl">web_1 <span class="p">|</span> App running at: </span></span><span class="line"><span class="cl">web_1 <span class="p">|</span> - Local: http://localhost:8080/ </span></span><span class="line"><span class="cl">web_1 <span class="p">|</span> </span></span><span class="line"><span class="cl">web_1 <span class="p">|</span> It seems you are running Vue CLI inside a container. </span></span><span class="line"><span class="cl">web_1 <span class="p">|</span> Access the dev server via http://localhost:&lt;your container<span class="err">&#39;</span>s external mapped port&gt;/ </span></span><span class="line"><span class="cl">web_1 <span class="p">|</span> </span></span><span class="line"><span class="cl">web_1 <span class="p">|</span> Note that the development build is not optimized. </span></span><span class="line"><span class="cl">web_1 <span class="p">|</span> To create a production build, run yarn build. </span></span></code></pre></div><p>Congratulations! Your local version of the Wild Workouts application is available at <a href="proxy.php?url=http%3A%2F%2Flocalhost%3A8080%2F" target="_blank">http://localhost:8080/</a>.</p> <p>There is also a public version available at <a href="proxy.php?url=https%3A%2F%2Fthreedotslabs-wildworkouts.web.app%2F" target="_blank">https://threedotslabs-wildworkouts.web.app</a>.</p> <h2 id="what-wild-workouts-can-do">What Wild Workouts can do?</h2> <p>How often have you seen tutorials without any real-life functionality? How often have patterns from these tutorials failed in real projects? Probably too often. &#x1f609; <strong>Real life is not as simple as tutorials suggest.</strong></p> <p>To avoid this problem, we created Wild Workouts as a fully functional project. It&rsquo;s much harder to take shortcuts and skip complexity when an application needs to be complete. This makes all articles longer, but there are no shortcuts here. <strong>If you don&rsquo;t spend enough time at the beginning, you&rsquo;ll lose much more time later during implementation. Or worse: you&rsquo;ll be fixing problems in a rush with the application already running in production.</strong></p> <h3 id="tldr">tl;dr</h3> <p><strong>Wild Workouts</strong> is an application for personal gym trainers and attendees.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="900" height="700" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Flogin_hu1844911a0cd70bc5a94f634bc7d2a753_35017_900x700_resize_q80_h2_lanczos_3.webp" alt="Login page" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Flogin_hu1844911a0cd70bc5a94f634bc7d2a753_35017_900x700_resize_lanczos_3.png"" /> <p>Trainers can set a schedule for when they are available for training.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1300" height="844" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Fschedule_huee9406ea816efa2a15566d658d7a186c_71968_1300x844_resize_q80_h2_lanczos_3.webp" alt="Schedule" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Fschedule_huee9406ea816efa2a15566d658d7a186c_71968_1300x844_resize_lanczos_3.png"" /> <p>Attendees can schedule training for available dates.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1343" height="797" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Fnew-training_huf606b9464a468d9a7f69a3e07bbab007_64039_1343x797_resize_q80_h2_lanczos_3.webp" alt="Schedule training" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Fnew-training_huf606b9464a468d9a7f69a3e07bbab007_64039_1343x797_resize_lanczos_3.png"" /> <p>Other functionalities are:</p> <ul> <li>management of &ldquo;credits&rdquo; (how many trainings the attendee can schedule)</li> <li>cancellation <ul> <li>if a training is canceled less than 24 hours before it begins, the attendee will not receive their credits back</li> </ul> </li> <li>training reschedule <ul> <li>if someone wants to reschedule a training less than 24 hours before it begins, the other participant (trainer or attendee) must approve it</li> </ul> </li> <li>calendar view</li> </ul> <p>Sounds simple. What can go wrong? 🤔</p> <h2 id="frontend">Frontend</h2> <p><em>If you are not interested in the frontend part, you can go straight to the <a href="#backend">Backend</a> section.</em></p> <p>I&rsquo;m primarily a backend engineer, and to be honest, I&rsquo;m not the greatest JavaScript or frontend specialist. &#x1f609; <strong>But we can&rsquo;t have an end-to-end application without a frontend!</strong></p> <p>In this series, we focus on the backend part. I&rsquo;ll give a high-level overview of the technologies I used on the frontend. <strong>This will be nothing new if you have any basic frontend knowledge.</strong> For more details, I recommend checking the source code in the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Ftree%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Fweb" target="_blank">web/ directory</a>.</p> If you want to know more about how the frontend is built, please let us know in the comments! <h3 id="openapi-swagger-client">OpenAPI (Swagger) client</h3> <p>Nobody likes keeping API contracts up to date manually. It&rsquo;s tedious and counterproductive to keep multiple JSON files in sync. OpenAPI solves this problem by generating a JavaScript HTTP client and Go HTTP server from the provided <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Ftree%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Fapi%2Fopenapi" target="_blank">specification</a>. We dive into the details in the <strong>Backend</strong> part.</p> <p><a href="proxy.php?url=https%3A%2F%2Fswagger.io%2Fdocs%2Fspecification%2Fabout%2F" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="900" height="272" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Fopenapi_hub2be91844206f654be98a5f0bf7a3d97_30834_900x272_resize_q80_h2_lanczos_3.webp" alt="OpenAPI" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Fopenapi_hub2be91844206f654be98a5f0bf7a3d97_30834_900x272_resize_lanczos_3.png"" /> </a></p> <h3 id="bootstrap">Bootstrap</h3> <p>You probably already know <a href="proxy.php?url=https%3A%2F%2Fgetbootstrap.com%2F" target="_blank">Bootstrap</a>, <strong>the greatest friend of every backend engineer, like me</strong>. &#x1f609; Wrestling with HTML and CSS is the part of frontend development I dislike the most. Bootstrap provided almost all the building blocks I needed for the application&rsquo;s HTML.</p> <p><a href="proxy.php?url=https%3A%2F%2Fgetbootstrap.com%2F" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="1213" height="566" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Fbootstrap_hu86f0ec03971c7fdbf12763ad2c04caa6_111327_1213x566_resize_q80_h2_lanczos_3.webp" alt="Bootstrap" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Fbootstrap_hu86f0ec03971c7fdbf12763ad2c04caa6_111327_1213x566_resize_lanczos_3.png"" /> </a></p> <h3 id="vuejs">Vue.js</h3> <p>After evaluating several popular frontend frameworks, I decided to use <a href="proxy.php?url=https%3A%2F%2Fvuejs.org%2Fv2%2Fguide%2F" target="_blank">Vue.js</a>. I really enjoyed its simplicity.</p> <p><a href="proxy.php?url=https%3A%2F%2Fvuejs.org%2F" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="906" height="297" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Fvue_hub8c95ddf84ebe97b2bb487b3d9ee256e_35533_906x297_resize_q80_h2_lanczos_3.webp" alt="Vue.js" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Fvue_hub8c95ddf84ebe97b2bb487b3d9ee256e_35533_906x297_resize_lanczos_3.png"" /> </a></p> <p>I started my journey as a full-stack developer in the pre-jQuery era. Frontend tooling has made huge progress&hellip; but I&rsquo;ll stay with the backend for now. &#x1f609;</p> <h2 id="backend">Backend</h2> <p>The backend of Wild Workouts is built from 3 services.</p> <ul> <li><code>trainer</code> &ndash; provides <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainer%2Fhttp.go" target="_blank">public HTTP</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainer%2Fgrpc.go" target="_blank">internal gRPC endpoints</a> for managing trainer schedule</li> <li><code>trainings</code> &ndash; provides <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fhttp.go" target="_blank">public HTTP</a> for managing attendee trainings</li> <li><code>users</code> &ndash; provides <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Fusers%2Fhttp.go" target="_blank">public HTTP endpoints</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Fusers%2Fgrpc.go" target="_blank">internal gRPC endpoints</a> , manages credits and user data</li> </ul> <p>If a service exposes 2 types of APIs, each of them is exposed in a separate process.</p> <h3 id="public-http-api">Public HTTP API</h3> <p>Most operations performed by applications are triggered by the public HTTP API. I&rsquo;ve heard Go newcomers ask many times what framework they should use to create an HTTP service. <strong>I always advise against using any kind of HTTP framework in Go. A simple router, like <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgo-chi%2Fchi" target="_blank">chi</a>, is more than enough.</strong> <em>chi</em> provides only the lightweight glue to define what URLs and methods our API supports. Under the hood, it uses the Go standard library <code>http</code> package, so all related tools like middlewares are 100% compatible.</p> <p>It may feel strange not to use a framework if you&rsquo;re coming from a language where Spring, Symfony, Django, or Express are the obvious choices. It felt strange to me too. Using any framework in Go adds unnecessary complexity and couples your project with that framework. <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FKISS_principle" target="_blank">KISS</a>. &#x1f609;</p> <p>All the services run the HTTP server in the same way. It makes sense not to copy this code three times.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">RunHTTPServer</span><span class="p">(</span><span class="nx">createHandler</span> <span class="kd">func</span><span class="p">(</span><span class="nx">router</span> <span class="nx">chi</span><span class="p">.</span><span class="nx">Router</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">apiRouter</span> <span class="o">:=</span> <span class="nx">chi</span><span class="p">.</span><span class="nf">NewRouter</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="nf">setMiddlewares</span><span class="p">(</span><span class="nx">apiRouter</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">rootRouter</span> <span class="o">:=</span> <span class="nx">chi</span><span class="p">.</span><span class="nf">NewRouter</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="c1">// we are mounting all APIs under /api path </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">rootRouter</span><span class="p">.</span><span class="nf">Mount</span><span class="p">(</span><span class="s">&#34;/api&#34;</span><span class="p">,</span> <span class="nf">createHandler</span><span class="p">(</span><span class="nx">apiRouter</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">logrus</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;Starting HTTP server&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">http</span><span class="p">.</span><span class="nf">ListenAndServe</span><span class="p">(</span><span class="s">&#34;:&#34;</span><span class="o">+</span><span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;PORT&#34;</span><span class="p">),</span> <span class="nx">rootRouter</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Fcommon%2Fserver%2Fhttp.go%23L20" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Fcommon%2Fserver%2Fhttp.go%23L20" target="_blank">Full source</a> </div> <p><em>chi</em> provides a set of useful built-in HTTP middlewares, but we&rsquo;re not limited to them. All middlewares compatible with the Go standard library will work.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Long story short: middlewares allow us to do anything before and after a request is executed (with access to the <code>http.Request</code>). Using HTTP middlewares gives us significant flexibility in building our custom HTTP server. We build our server from multiple decoupled components that you can customize for your needs. </p></div> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">setMiddlewares</span><span class="p">(</span><span class="nx">router</span> <span class="o">*</span><span class="nx">chi</span><span class="p">.</span><span class="nx">Mux</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">router</span><span class="p">.</span><span class="nf">Use</span><span class="p">(</span><span class="nx">middleware</span><span class="p">.</span><span class="nx">RequestID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">router</span><span class="p">.</span><span class="nf">Use</span><span class="p">(</span><span class="nx">middleware</span><span class="p">.</span><span class="nx">RealIP</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">router</span><span class="p">.</span><span class="nf">Use</span><span class="p">(</span><span class="nx">logs</span><span class="p">.</span><span class="nf">NewStructuredLogger</span><span class="p">(</span><span class="nx">logrus</span><span class="p">.</span><span class="nf">StandardLogger</span><span class="p">()))</span> </span></span><span class="line"><span class="cl"> <span class="nx">router</span><span class="p">.</span><span class="nf">Use</span><span class="p">(</span><span class="nx">middleware</span><span class="p">.</span><span class="nx">Recoverer</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nf">addCorsMiddleware</span><span class="p">(</span><span class="nx">router</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">addAuthMiddleware</span><span class="p">(</span><span class="nx">router</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">router</span><span class="p">.</span><span class="nf">Use</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">middleware</span><span class="p">.</span><span class="nf">SetHeader</span><span class="p">(</span><span class="s">&#34;X-Content-Type-Options&#34;</span><span class="p">,</span> <span class="s">&#34;nosniff&#34;</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">middleware</span><span class="p">.</span><span class="nf">SetHeader</span><span class="p">(</span><span class="s">&#34;X-Frame-Options&#34;</span><span class="p">,</span> <span class="s">&#34;deny&#34;</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">router</span><span class="p">.</span><span class="nf">Use</span><span class="p">(</span><span class="nx">middleware</span><span class="p">.</span><span class="nx">NoCache</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Fcommon%2Fserver%2Fhttp.go%23L33" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Fcommon%2Fserver%2Fhttp.go%23L33" target="_blank">Full source</a> </div> <p>We have our framework almost ready now. :) It&rsquo;s time to use it. We can call <code>server.RunHTTPServer</code> in the <code>trainings</code> service.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">main</span> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">server</span><span class="p">.</span><span class="nf">RunHTTPServer</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">router</span> <span class="nx">chi</span><span class="p">.</span><span class="nx">Router</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">HandlerFromMux</span><span class="p">(</span><span class="nx">HttpServer</span><span class="p">{</span><span class="nx">firebaseDB</span><span class="p">,</span> <span class="nx">trainerClient</span><span class="p">,</span> <span class="nx">usersClient</span><span class="p">},</span> <span class="nx">router</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fmain.go%23L38" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fmain.go%23L38" target="_blank">Full source</a> </div> <p><code>createHandler</code> needs to return <code>http.Handler</code>. In our case, it is <code>HandlerFromMux</code> generated by <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fdeepmap%2Foapi-codegen" target="_blank">oapi-codegen</a>.</p> <p>It provides us all the paths and query parameters from the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Ftree%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Fapi%2Fopenapi" target="_blank">OpenAPI specs</a>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">func</span> <span class="nf">HandlerFromMux</span><span class="p">(</span><span class="nx">si</span> <span class="nx">ServerInterface</span><span class="p">,</span> <span class="nx">r</span> <span class="nx">chi</span><span class="p">.</span><span class="nx">Router</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nf">Group</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">r</span> <span class="nx">chi</span><span class="p">.</span><span class="nx">Router</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nf">Use</span><span class="p">(</span><span class="nx">GetTrainingsCtx</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">&#34;/trainings&#34;</span><span class="p">,</span> <span class="nx">si</span><span class="p">.</span><span class="nx">GetTrainings</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nf">Group</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">r</span> <span class="nx">chi</span><span class="p">.</span><span class="nx">Router</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nf">Use</span><span class="p">(</span><span class="nx">CreateTrainingCtx</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nf">Post</span><span class="p">(</span><span class="s">&#34;/trainings&#34;</span><span class="p">,</span> <span class="nx">si</span><span class="p">.</span><span class="nx">CreateTraining</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fopenapi_api.gen.go%23L153" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/openapi_api.gen.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fopenapi_api.gen.go%23L153" target="_blank">Full source</a> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># ...</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">paths</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">/trainings</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">get</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">operationId</span><span class="p">:</span><span class="w"> </span><span class="l">getTrainings</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">responses</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">&#39;200&#39;</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">todo</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">content</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">application/json</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">schema</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/Trainings&#39;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">description</span><span class="p">:</span><span class="w"> </span><span class="l">unexpected error</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">content</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">application/json</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">schema</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/Error&#39;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c"># ...</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Fapi%2Fopenapi%2Ftrainings.yml%23L16" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/api/openapi/trainings.yml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Fapi%2Fopenapi%2Ftrainings.yml%23L16" target="_blank">Full source</a> </div> <p>If you make changes to the OpenAPI spec, you need to regenerate the Go server and JavaScript clients. Run:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">make openapi </span></span></code></pre></div><p>Part of the generated code is <code>ServerInterface</code>. It contains all methods the API must support. You implement server functionality by implementing this interface.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">ServerInterface</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// (GET /trainings) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">GetTrainings</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="c1">// (POST /trainings) </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nf">CreateTraining</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fopenapi_api.gen.go%23L14" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/openapi_api.gen.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fopenapi_api.gen.go%23L14" target="_blank">Full source</a> </div> <p>This is an example of how <code>trainings.HttpServer</code> is implemented:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">main</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;net/http&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/go-chi/render&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/pkg/internal/auth&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/pkg/internal/genproto/trainer&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/pkg/internal/genproto/users&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/pkg/internal/server/httperr&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">HttpServer</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">db</span> <span class="nx">db</span> </span></span><span class="line"><span class="cl"> <span class="nx">trainerClient</span> <span class="nx">trainer</span><span class="p">.</span><span class="nx">TrainerServiceClient</span> </span></span><span class="line"><span class="cl"> <span class="nx">usersClient</span> <span class="nx">users</span><span class="p">.</span><span class="nx">UsersServiceClient</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">HttpServer</span><span class="p">)</span> <span class="nf">GetTrainings</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">user</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">auth</span><span class="p">.</span><span class="nf">UserFromCtx</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">Unauthorised</span><span class="p">(</span><span class="s">&#34;no-user-found&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">trainings</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">h</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">GetTrainings</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nf">Context</span><span class="p">(),</span> <span class="nx">user</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">httperr</span><span class="p">.</span><span class="nf">InternalError</span><span class="p">(</span><span class="s">&#34;cannot-get-trainings&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">,</span> <span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">trainingsResp</span> <span class="o">:=</span> <span class="nx">Trainings</span><span class="p">{</span><span class="nx">trainings</span><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">render</span><span class="p">.</span><span class="nf">Respond</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="nx">trainingsResp</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fhttp.go%23L1" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/http.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fhttp.go%23L1" target="_blank">Full source</a> </div> <p>But HTTP paths are not the only thing generated from the OpenAPI spec. <strong>More importantly, it also provides the models for responses and requests.</strong> Models are, in most cases, much more complex than API paths and methods. <strong>Generating them saves time and frustration during API contract changes.</strong></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># ...</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">schemas</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">Training</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">object</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">uuid, user, userUuid, notes, time, canBeCancelled, moveRequiresAccept]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">properties</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">uuid</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">uuid</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">user</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">example</span><span class="p">:</span><span class="w"> </span><span class="l">Mariusz Pudzianowski</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">userUuid</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">uuid</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">notes</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">example</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;let&#39;s do leg day!&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">time</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">date-time</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">canBeCancelled</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">boolean</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">moveRequiresAccept</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">boolean</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">proposedTime</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">format</span><span class="p">:</span><span class="w"> </span><span class="l">date-time</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">moveProposedBy</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">string</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">Trainings</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">object</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">required</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">trainings]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">properties</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">trainings</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">array</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">items</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">$ref</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;#/components/schemas/Training&#39;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c"># ...</span><span class="w"> </span></span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Fapi%2Fopenapi%2Ftrainings.yml%23L151" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/api/openapi/trainings.yml</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Fapi%2Fopenapi%2Ftrainings.yml%23L151" target="_blank">Full source</a> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// Training defines model for Training. </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">Training</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">CanBeCancelled</span> <span class="kt">bool</span> <span class="s">`json:&#34;canBeCancelled&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">MoveProposedBy</span> <span class="o">*</span><span class="kt">string</span> <span class="s">`json:&#34;moveProposedBy,omitempty&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">MoveRequiresAccept</span> <span class="kt">bool</span> <span class="s">`json:&#34;moveRequiresAccept&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Notes</span> <span class="kt">string</span> <span class="s">`json:&#34;notes&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">ProposedTime</span> <span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> <span class="s">`json:&#34;proposedTime,omitempty&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Time</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> <span class="s">`json:&#34;time&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">User</span> <span class="kt">string</span> <span class="s">`json:&#34;user&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">UserUuid</span> <span class="kt">string</span> <span class="s">`json:&#34;userUuid&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Uuid</span> <span class="kt">string</span> <span class="s">`json:&#34;uuid&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// Trainings defines model for Trainings. </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">Trainings</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Trainings</span> <span class="p">[]</span><span class="nx">Training</span> <span class="s">`json:&#34;trainings&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fopenapi_types.gen.go%23L23" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/openapi_types.gen.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fopenapi_types.gen.go%23L23" target="_blank">Full source</a> </div> <h3 id="cloud-firestore-database">Cloud Firestore database</h3> <p>All right, we have the HTTP API. But even the best API is useless without data and the ability to persist it.</p> <p>If we want to build the application in <strong>the most modern, scalable, and truly serverless way, Firestore is a natural choice</strong>. We get all of that out of the box. What&rsquo;s the cost?</p> <p><a href="proxy.php?url=https%3A%2F%2Fcloud.google.com%2Ffirestore" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="900" height="312" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Ffirestore_hu4a628ee4373a633fe96a99860f67389c_18834_900x312_resize_q80_h2_lanczos_3.webp" alt="Firestore" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Ffirestore_hu4a628ee4373a633fe96a99860f67389c_18834_900x312_resize_lanczos_3.png"" /> </a></p> <p>For the <a href="proxy.php?url=https%3A%2F%2Ffirebase.google.com%2Fdocs%2Ffirestore%2Fpricing%23europe" target="_blank">Europe multi-region</a> option, you pay:</p> <ul> <li>$0.06 per 100,000 documents reads</li> <li>$0.18 per 100,000 documents writes</li> <li>$0.02 per 100,000 documents deletes</li> <li>$0.18/GiB of stored data/month</li> </ul> <p>Sounds pretty cheap?</p> <p>For comparison, let&rsquo;s take the cheapest <a href="proxy.php?url=https%3A%2F%2Fcloud.google.com%2Fsql%2Fpricing%232nd-gen-pricing" target="_blank">Cloud SQL MySQL</a> <code>db-f1-micro</code> instance with a shared Virtual CPU and <code>3 GB</code> of storage as a reference: it costs <em>$15.33/month</em>. The cheapest instance with high availability and with 1 non-shared Virtual CPU costs <em>$128.21/month</em>.</p> <p>Even better, in the <a href="proxy.php?url=https%3A%2F%2Ffirebase.google.com%2Fpricing" target="_blank">free plan</a>, you can store up to 1 GiB of data with 20k document writes per day.</p> <p>Firestore is a NoSQL database, so we shouldn&rsquo;t expect to build relational models the SQL way. Instead, we have a system of <a href="proxy.php?url=https%3A%2F%2Ffirebase.google.com%2Fdocs%2Ffirestore%2Fdata-model" target="_blank">hierarchical collections</a>. In our case, the data model is simple, so we have only one level of collections.</p> <p><a href="proxy.php?url=https%3A%2F%2Fconsole.firebase.google.com%2F" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="982" height="633" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fserverless-cloud-run-firebase-modern-go-app%2Ffirestore-console_hu073cf84a622e1edd694184861852ee5f_74134_982x633_resize_q80_h2_lanczos_3.webp" alt="Firestore Console" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fserverless-cloud-run-firebase-modern-go-app%5C%2Ffirestore-console_hu073cf84a622e1edd694184861852ee5f_74134_982x633_resize_lanczos_3.png"" /> </a></p> <p>Unlike many NoSQL databases, <strong>Firestore offers ACID transactions on any operation</strong>. This also works when updating multiple documents.</p> <h4 id="firestore-limitations">Firestore limitations</h4> <p>An important limitation is <em>1 update per second per document</em>. <strong>You can still update many independent documents in parallel.</strong> This is an important factor to consider when designing your database. In some cases, you should consider batching operations, different document designs, or using a different database. If data changes frequently, a key-value database might be a good choice.</p> <p><strong>In my experience, the 1 update per second per document limitation isn&rsquo;t a serious problem.</strong> In most cases when I used Firestore, we were updating many independent documents. This also applies when using Firestore for <strong>event sourcing</strong>: you only use append operations. In Wild Workouts, we shouldn&rsquo;t have a problem with this limit. &#x1f609;</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>I have also observed that Firestore needs some time to warm up. In other words, if you want to insert 10 million documents within one minute after setting up a new project, <strong>it may not work</strong>. I assume this is related to some internal mechanism that handles scalability.</p> <p>Fortunately, in the real world, traffic spikes from 0 to 10 million writes per minute are uncommon.</p> </p></div> </div> <h4 id="running-firestore-locally">Running Firestore locally</h4> <p>Unfortunately, the Firestore emulator is not perfect. &#x1f609; I found some situations where the emulator wasn&rsquo;t 100% compatible with the real version. I also encountered situations where updating and reading the same document in a transaction caused a deadlock. From my perspective, this functionality is sufficient for local development.</p> <p>The alternative is to have a separate Google Cloud project for local development. I prefer having a local environment that is truly local and doesn&rsquo;t depend on any external services. It&rsquo;s also easier to set up and can be used later in continuous integration.</p> <p><strong>Since the end of May, the Firestore emulator provides a UI.</strong> It is added to the Docker Compose and is available at <a href="proxy.php?url=http%3A%2F%2Flocalhost%3A4000%2F" target="_blank">http://localhost:4000/</a>. At the time of writing, sub-collections <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Ffirebase%2Ffirebase-tools-ui%2Fissues%2F273" target="_blank">aren&rsquo;t displayed properly</a> in the emulator UI. Don&rsquo;t worry, for Wild Workouts this isn&rsquo;t a problem. &#x1f609;</p> <h4 id="using-firestore">Using Firestore</h4> <p>Apart from the Firestore implementation, the code works the same way locally and in production. When using the emulator locally, we need to run our application with the <code>FIRESTORE_EMULATOR_HOST</code> environment variable set to the emulator hostname (in our case <code>firestore:8787</code>). This is set in the <code>.env</code> file.</p> <p>In production, Google Cloud handles everything under the hood, and no extra configuration is needed.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">firebaseClient</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">firestore</span><span class="p">.</span><span class="nf">NewClient</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;GCP_PROJECT&#34;</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">panic</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fmain.go%23L19" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainings/main.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainings%2Fmain.go%23L19" target="_blank">Full source</a> </div> <p>Here&rsquo;s an example of how I used the Firestore client to query the trainer schedule. You can see how I used the <a href="proxy.php?url=https%3A%2F%2Ffirebase.google.com%2Fdocs%2Ffirestore%2Fquery-data%2Fqueries%23simple_queries" target="_blank">queries functionality</a> to get only dates within the queried date interval.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span> <span class="nx">main</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="s">&#34;cloud.google.com/go/firestore&#34;</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">db</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">firestoreClient</span> <span class="o">*</span><span class="nx">firestore</span><span class="p">.</span><span class="nx">Client</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">d</span> <span class="nx">db</span><span class="p">)</span> <span class="nf">TrainerHoursCollection</span><span class="p">()</span> <span class="o">*</span><span class="nx">firestore</span><span class="p">.</span><span class="nx">CollectionRef</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">d</span><span class="p">.</span><span class="nx">firestoreClient</span><span class="p">.</span><span class="nf">Collection</span><span class="p">(</span><span class="s">&#34;trainer-hours&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">d</span> <span class="nx">db</span><span class="p">)</span> <span class="nf">QueryDates</span><span class="p">(</span><span class="nx">params</span> <span class="o">*</span><span class="nx">GetTrainerAvailableHoursParams</span><span class="p">,</span> <span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="p">([]</span><span class="nx">Date</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">iter</span> <span class="o">:=</span> <span class="nx">d</span><span class="p">.</span> </span></span><span class="line"><span class="cl"> <span class="nf">TrainerHoursCollection</span><span class="p">().</span> </span></span><span class="line"><span class="cl"> <span class="nf">Where</span><span class="p">(</span><span class="s">&#34;Date.Time&#34;</span><span class="p">,</span> <span class="s">&#34;&gt;=&#34;</span><span class="p">,</span> <span class="nx">params</span><span class="p">.</span><span class="nx">DateFrom</span><span class="p">).</span> </span></span><span class="line"><span class="cl"> <span class="nf">Where</span><span class="p">(</span><span class="s">&#34;Date.Time&#34;</span><span class="p">,</span> <span class="s">&#34;&lt;=&#34;</span><span class="p">,</span> <span class="nx">params</span><span class="p">.</span><span class="nx">DateTo</span><span class="p">).</span> </span></span><span class="line"><span class="cl"> <span class="nf">Documents</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nx">dates</span> <span class="p">[]</span><span class="nx">Date</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">doc</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">iter</span><span class="p">.</span><span class="nf">Next</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">==</span> <span class="nx">iterator</span><span class="p">.</span><span class="nx">Done</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">break</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">date</span> <span class="o">:=</span> <span class="nx">Date</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">doc</span><span class="p">.</span><span class="nf">DataTo</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">date</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">date</span> <span class="p">=</span> <span class="nf">setDefaultAvailability</span><span class="p">(</span><span class="nx">date</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">dates</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">dates</span><span class="p">,</span> <span class="nx">date</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">dates</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainer%2Ffirestore.go%23L69" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/firestore.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainer%2Ffirestore.go%23L69" target="_blank">Full source</a> </div> <p>No extra data mapping is needed. The Firestore library can marshal any struct with public fields or <code>map[string]interface</code>, as long as there&rsquo;s nothing unusual inside. &#x1f609; You can find the full specification of how conversion works in the <a href="proxy.php?url=https%3A%2F%2Fgodoc.org%2Fcloud.google.com%2Fgo%2Ffirestore%23DocumentSnapshot.DataTo" target="_blank">cloud.google.com/go/firestore GoDoc</a>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Date</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Date</span> <span class="nx">openapi_types</span><span class="p">.</span><span class="nx">Date</span> <span class="s">`json:&#34;date&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">HasFreeHours</span> <span class="kt">bool</span> <span class="s">`json:&#34;hasFreeHours&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Hours</span> <span class="p">[]</span><span class="nx">Hour</span> <span class="s">`json:&#34;hours&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainer%2Fopenapi_types.gen.go%23L12" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/openapi_types.gen.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainer%2Fopenapi_types.gen.go%23L12" target="_blank">Full source</a> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">date</span> <span class="o">:=</span> <span class="nx">Date</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">doc</span><span class="p">.</span><span class="nf">DataTo</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">date</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="text-center -mt-5 mb-5 fs-sm hidden mb:block"> Full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainer%2Ffirestore.go%23L88" target="_blank">github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/firestore.go</a> </div> <div class="text-center -mt-5 mb-5 fs-sm mb:hidden block"> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwild-workouts-go-ddd-example%2Fblob%2Fa0a41253db96d46d75e7ff4c7e7f95848f47dcc3%2Finternal%2Ftrainer%2Ffirestore.go%23L88" target="_blank">Full source</a> </div> <h2 id="production-deployment-tldr">Production deployment tl;dr</h2> <p>You can deploy your own version of Wild Workouts with one command:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">&gt; <span class="nb">cd</span> terraform/ </span></span><span class="line"><span class="cl">&gt; make </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">Fill all required parameters: </span></span><span class="line"><span class="cl"> project <span class="o">[</span>current: wild-workouts project<span class="o">]</span>: <span class="c1"># &lt;----- put your Wild Workouts Google Cloud project name here (it will be created) </span> </span></span><span class="line"><span class="cl"> user <span class="o">[</span>current: [email protected]<span class="o">]</span>: <span class="c1"># &lt;----- put your Google (Gmail, G-suite etc.) e-mail here</span> </span></span><span class="line"><span class="cl"> billing_account <span class="o">[</span>current: My billing account<span class="o">]</span>: <span class="c1"># &lt;----- your billing account name, can be found here https://console.cloud.google.com/billing</span> </span></span><span class="line"><span class="cl"> region <span class="o">[</span>current: europe-west1<span class="o">]</span>: </span></span><span class="line"><span class="cl"> firebase_location <span class="o">[</span>current: europe-west<span class="o">]</span>: </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># it may take a couple of minutes...</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">The setup is almost <span class="k">done</span>! </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">Now you need to <span class="nb">enable</span> Email/Password provider in the Firebase console. </span></span><span class="line"><span class="cl">To <span class="k">do</span> this, visit https://console.firebase.google.com/u/0/project/<span class="o">[</span>your-project<span class="o">]</span>/authentication/providers </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">You can also downgrade the subscription plan to Spark <span class="o">(</span>it<span class="s1">&#39;s set to Blaze by default). </span></span></span><span class="line"><span class="cl"><span class="s1">The Spark plan is completely free and has all features needed for running this project. </span></span></span><span class="line"><span class="cl"><span class="s1"> </span></span></span><span class="line"><span class="cl"><span class="s1">Congratulations! Your project should be available at: https://[your-project].web.app </span></span></span><span class="line"><span class="cl"><span class="s1"> </span></span></span><span class="line"><span class="cl"><span class="s1">If it&#39;</span>s not, check <span class="k">if</span> the build finished successfully: https://console.cloud.google.com/cloud-build/builds?project<span class="o">=[</span>your-project<span class="o">]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">If you need help, feel free to contact us at https://threedots.tech </span></span></code></pre></div><p>We describe the deployment in detail in the next articles.</p> <h2 id="whats-next">What&rsquo;s next?</h2> That&rsquo;s all for today. In the next article, I will cover <strong>internal gRPC communication between services</strong>. After that, I will cover <strong>HTTP Firebase authentication</strong>. <p>The code with gRPC communication and authentication is already on our GitHub. Feel free to read, run, and experiment with it.</p> <h3 id="deployment-and-infrastructure">Deployment and infrastructure</h3> <p>In parallel, <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Fm1_10sz" target="_blank">Miłosz</a> is working on articles describing the deployment and infrastructure part. They will cover in detail: </p> <ul> <li>Terraform</li> <li>Cloud Run</li> <li>CI/CD</li> <li>Firebase hosting</li> </ul> <h3 id="what-is-wrong-with-this-application">What is wrong with this application?!</h3> <p>After finishing all articles describing the current application, we start the part related to refactoring and adding new features to Wild Workouts.</p> <p><strong>We don&rsquo;t want to use many fancy techniques just to make our CVs look better. Our goal is to solve issues present in the application using</strong> Domain-Driven Design, Clean Architecture, CQRS, Event Storming, and Event Modeling.</p> <p>We do this through refactoring. It should then be clear <strong>what issues are solved and how the implementation becomes cleaner</strong>. Maybe during this process we&rsquo;ll even discover that some of these techniques aren&rsquo;t useful in Go. Who knows? &#x1f609;</p> <p><strong>Do you see any of the issues we included in this application? Please post your guess in the comments. :)</strong></p> <p>See you next week.</p>Using MySQL as a Pub/Subhttps://threedots.tech/post/when-sql-database-makes-great-pub-sub/Sat, 14 Dec 2019 00:00:00 +0100https://threedots.tech/post/when-sql-database-makes-great-pub-sub/<p>If you compare MySQL or PostgreSQL with Kafka or RabbitMQ, at first, it seems they are entirely different software. And usually, that&rsquo;s true, as you would use them for quite different tasks. What they have in common is processing streams of data, and they specialize in specific ways of doing it.</p> <p>While Kafka and RabbitMQ are popular examples of Pub/Subs (also known as message queues or stream processing platforms), I&rsquo;d like to share some patterns for using SQL databases as Pub/Subs as well. But shouldn&rsquo;t we pick the right tool for the job? That&rsquo;s right. However, SQL databases are readily available if you don&rsquo;t have access to the dedicated messaging infrastructure. They also have some unique features that turn out useful while working with message-driven applications.</p> <p>I&rsquo;m going to show three examples of such cases with solutions based on the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill" target="_blank">Watermill library</a> for Go, although the patterns stay the same regardless of the language. But first, let&rsquo;s ask the fundamental question.</p> <h2 id="what-is-a-pubsub">What is a Pub/Sub?</h2> <p>If you generalize it enough, you can define any Pub/Sub client with just two methods (as the name suggests): one for <strong>publishing</strong> a new message to a topic, and the other for <strong>subscribing</strong> for messages from a topic.</p> <pre tabindex="0"><code> Publish(topic, message) Subscribe(topic) stream&lt;message&gt; </code></pre><p>So how do you implement this interface for an SQL database? The simplest way is to use <code>INSERT</code> statements for publishing and <code>SELECT</code> for consuming messages. More advanced subscribing patterns can be achieved with mechanisms like MySQL&rsquo;s binlog or PostgreSQL&rsquo;s <code>LISTEN</code> and <code>NOTIFY</code> commands. But for most use cases, the simple approach should be good enough.</p> <h3 id="using-watermill">Using Watermill</h3> <p>The two-method pseudocode above is also the base idea behind Watermill&rsquo;s <code>Publisher</code> and <code>Subscriber</code> interfaces. It turns out you can hide quite a lot of complexity behind this abstraction. To implement any Pub/Sub, all you have to do is implement these two methods. One of the supported Pub/Subs is the <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fpubsubs%2Fsql%2F" target="_blank">SQL Pub/Sub</a>.</p> <p>All examples below include a short snippet of Watermill&rsquo;s <strong>handler</strong>, which is a function that receives a message and can return new messages to be published.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">msg</span> <span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="p">([]</span><span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span></code></pre></div><p>See the <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fgetting-started%2F" target="_blank">Getting started guide</a> for more details.</p> <h2 id="pattern-1-persistent-event-log">Pattern 1: Persistent Event Log</h2> <p>Message queues offer various features depending on the design. That&rsquo;s why no one fits all use cases.</p> <p>One of such features is persistent storage. For example, Apache Kafka comes out of the box with support for storing messages on topics as long as you need it. Others, like Google Cloud Pub/Sub*,* treat messages as ephemeral and delete them after they are consumed or even after several days without being acknowledged.</p> <p>You don&rsquo;t always need to store messages, but it comes in useful when working with <a href="proxy.php?url=https%3A%2F%2Fmartinfowler.com%2FeaaDev%2FDomainEvent.html" target="_blank">domain events</a>. It allows processing events from the past again, sort of like &ldquo;going back in time&rdquo; (a similar approach to Event Sourcing, although much simplified).</p> <p>If you&rsquo;d like to keep your current messaging software but need persistent storage for events, an SQL database might be the right choice.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1250" height="440" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fwhen-sql-database-makes-great-pub-sub%2FSQL_Pub_Sub_1_hu265ed3bdd3049d8df5fc65d9b1b82419_30772_1250x440_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fwhen-sql-database-makes-great-pub-sub%5C%2FSQL_Pub_Sub_1_hu265ed3bdd3049d8df5fc65d9b1b82419_30772_1250x440_resize_lanczos_3.png"" /> <h3 id="implementation">Implementation</h3> <p>The <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fmessages-router%2F%23handler" target="_blank">Watermill handler</a> for this pattern is the simplest one. It works just like a transparent proxy, moving messages from one system to the other. The <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fmessages-router" target="_blank">Router</a> component does all the work in the background. All you have to do is define the subscriber, publisher, and corresponding topics.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"> <span class="nx">router</span><span class="p">.</span><span class="nf">AddHandler</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;googlecloud-to-mysql&#34;</span><span class="p">,</span> <span class="c1">// unique handler name </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">googleCloudTopic</span><span class="p">,</span> <span class="c1">// subscriber&#39;s topic </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">googleCloudSubscriber</span><span class="p">,</span> <span class="c1">// subscriber </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">mysqlTable</span><span class="p">,</span> <span class="c1">// publisher&#39;s topic </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">mysqlPublisher</span><span class="p">,</span> <span class="c1">// publisher </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kd">func</span><span class="p">(</span><span class="nx">msg</span> <span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="p">([]</span><span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">[]</span><span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">{</span><span class="nx">msg</span><span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span></code></pre></div><p>See full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Fpersistent-event-log" target="_blank">persistent-event-log example</a>.</p> <h2 id="pattern-2-transactional-events">Pattern 2: Transactional Events</h2> <p>Consider user signup, a classic use case in every web application. The requirements are straightforward. You need to store user details in an SQL table and publish <code>UserSignedUp</code> event, so other services in your system consume it and take some action (e.g., send a newsletter, update statistics report, fill personal details in a CRM).</p> <p>The issue is, you can&rsquo;t execute SQL statements and publish events both at the same time. You have two options:</p> <ul> <li><strong>Save the user first, then publish the event</strong> — if publishing fails, the SQL table is up to date, but the rest of the system is inconsistent. The user can log in, but doesn&rsquo;t receive a newsletter, the monthly statistics report is incorrect, and your sales team doesn&rsquo;t know whom to call. Other teams that depend on the event will never receive it.</li> </ul> <img title="" loading="lazy" decoding="async" class="img img-center" width="1190" height="470" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fwhen-sql-database-makes-great-pub-sub%2FSQL_Pub_Sub_2a_hu92d6597ce5e5171159c2bf593859b24c_37129_1190x470_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fwhen-sql-database-makes-great-pub-sub%5C%2FSQL_Pub_Sub_2a_hu92d6597ce5e5171159c2bf593859b24c_37129_1190x470_resize_lanczos_3.png"" /> <ul> <li><strong>Publish the event first, then save the user</strong> — if saving the user fails, the system is consistent, but the SQL table is outdated. The user can&rsquo;t log in but receives a newsletter. Your reports show more users than there are using your product, and your sales team nags the frustrated user with calls. Let&rsquo;s hope your &ldquo;unsubscribe&rdquo; button still works.</li> </ul> <img title="" loading="lazy" decoding="async" class="img img-center" width="1190" height="530" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fwhen-sql-database-makes-great-pub-sub%2FSQL_Pub_Sub_2b_hu3c646b3ba394326e0032df1ce3592edb_37619_1190x530_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fwhen-sql-database-makes-great-pub-sub%5C%2FSQL_Pub_Sub_2b_hu3c646b3ba394326e0032df1ce3592edb_37619_1190x530_resize_lanczos_3.png"" /> <p>You can come up with complicated solutions for this problem, like two-phase commits or periodically synchronizing all systems. There&rsquo;s also a more straightforward pattern using the SQL database.</p> <p>The idea is to insert the event into a separate table instead of publishing it directly. The critical part is doing it within the same transaction as saving the user. It guarantees both or none are stored. In the background, a separate process is <em>subscribed</em> to the table with events and <em>publishes</em> all incoming rows on some message queue.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1931" height="440" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fwhen-sql-database-makes-great-pub-sub%2FSQL_Pub_Sub_2_hu0074fc86bfebb92f81dbca96dfc07859_41494_1931x440_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fwhen-sql-database-makes-great-pub-sub%5C%2FSQL_Pub_Sub_2_hu0074fc86bfebb92f81dbca96dfc07859_41494_1931x440_resize_lanczos_3.png"" /> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> One thing to keep in mind is that <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FEventual_consistency" target="_blank">eventual consistency</a> must be acceptable in your system. It&rsquo;s possible that the background process stops working for some reason and publishes events sometime in the future. But if you&rsquo;re already working with events, I hope you did consider that. </p></div> </div> <h3 id="implementation-1">Implementation</h3> <p>When using Watermill, the handler function is very similar to the previous example. After all, it&rsquo;s the same kind of event proxy, but this time the database is the entry point.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"> <span class="nx">router</span><span class="p">.</span><span class="nf">AddHandler</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;mysql-to-kafka&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">mysqlTable</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">mysqlSubscriber</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">kafkaTopic</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">kafkaPublisher</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">msg</span> <span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="p">([]</span><span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">[]</span><span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">{</span><span class="nx">msg</span><span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span></code></pre></div><p>See full source: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Ftransactional-events" target="_blank">transactional-events example</a>.</p> <h2 id="pattern-3-synchronizing-databases">Pattern 3: Synchronizing Databases</h2> <p>Synchronizing two databases isn&rsquo;t a new problem. That&rsquo;s how replication already worked for decades. It operates on low-level mechanisms (like the binary log), but the idea is very close to a Pub/Sub: one database is <em>publishing</em> changes, and the other <em>subscribes</em> to them.</p> <p>A bit harder task is moving data between two different database engines, each with distinct table schema. If replication is using an interface similar to a Pub/Sub, can we use the same principles for migration as well?</p> <p>Let&rsquo;s consider migrating a MySQL table to PostgreSQL. It would look roughly like this:</p> <ol> <li>The application inserts a new row into the MySQL table.</li> <li>Synchronization service receives this as an update (because it <strong>subscribed</strong> to that table).</li> <li>The service <strong>translates</strong> the row into the destination format (only if schemas differ, so this step is optional).</li> <li>The service inserts a new row into the PostgreSQL table (<strong>publish</strong>).</li> </ol> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> The hard part about implementing this handler is making sure we keep proper order of events, don&rsquo;t miss any rows, and don&rsquo;t duplicate them. The first two are solved for you by Watermill out of the box, but you need to take care of de-duplicating the events yourself. A simple approach is to make sure your event handlers are idempotent. </p></div> </div> <img title="" loading="lazy" decoding="async" class="img img-center" width="1235" height="425" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fwhen-sql-database-makes-great-pub-sub%2FSQL_Pub_Sub_hu90c3c5deb3dc9ad25774b7c86879fe12_32147_1235x425_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fwhen-sql-database-makes-great-pub-sub%5C%2FSQL_Pub_Sub_hu90c3c5deb3dc9ad25774b7c86879fe12_32147_1235x425_resize_lanczos_3.png"" /> <h3 id="implementation-2">Implementation</h3> <p>If table schemas in both databases are the same, the handler can be as short, as in the first two patterns. In this example, we deal with distinct tables, so we need to implement the <strong>translation</strong> part as well.</p> <p>First of all, we need to rename the <code>user</code> column to <code>username</code>. Second, merge <code>first_name</code> and <code>last_name</code> columns into <code>full_name</code>.</p> <p>You can see that the <a href="proxy.php?url=https%3A%2F%2Fgolang.org%2Fpkg%2Fencoding%2Fgob%2F" target="_blank">gob</a> library is used for encoding and decoding the payloads, but you can use anything you like, as long as it serializes to a slice of bytes.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> By default, the SQL subscriber consumes all existing records in the table and listens for any new incoming rows. So you can start with migrating the current data and then keep the service running to keep both databases up to date. The synchronization will work as long as your tables are append-only - see Limitations below for details. </p></div> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"> <span class="kd">type</span> <span class="nx">mysqlUser</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="nx">User</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">FirstName</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">LastName</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">CreatedAt</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">type</span> <span class="nx">postgresUser</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">int</span> </span></span><span class="line"><span class="cl"> <span class="nx">Username</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">FullName</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">CreatedAt</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">router</span><span class="p">.</span><span class="nf">AddHandler</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;mysql-to-postgres&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">mysqlTable</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">mysqlSubscriber</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">postgresTable</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">postgresPublisher</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span><span class="p">(</span><span class="nx">msg</span> <span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="p">([]</span><span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Decode the row coming from the MySQL table </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">decoder</span> <span class="o">:=</span> <span class="nx">gob</span><span class="p">.</span><span class="nf">NewDecoder</span><span class="p">(</span><span class="nx">bytes</span><span class="p">.</span><span class="nf">NewBuffer</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">Payload</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="nx">originUser</span> <span class="o">:=</span> <span class="nx">mysqlUser</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">decoder</span><span class="p">.</span><span class="nf">Decode</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">originUser</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Translate from the MySQL schema to the PostgreSQL schema </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">newUser</span> <span class="o">:=</span> <span class="nx">postgresUser</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span><span class="p">:</span> <span class="nx">originUser</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Username</span><span class="p">:</span> <span class="nx">originUser</span><span class="p">.</span><span class="nx">User</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">FullName</span><span class="p">:</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;%s %s&#34;</span><span class="p">,</span> <span class="nx">originUser</span><span class="p">.</span><span class="nx">FirstName</span><span class="p">,</span> <span class="nx">originUser</span><span class="p">.</span><span class="nx">LastName</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">CreatedAt</span><span class="p">:</span> <span class="nx">originUser</span><span class="p">.</span><span class="nx">CreatedAt</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Encode the row to be saved in the PstgreSQL table </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kd">var</span> <span class="nx">payload</span> <span class="nx">bytes</span><span class="p">.</span><span class="nx">Buffer</span> </span></span><span class="line"><span class="cl"> <span class="nx">encoder</span> <span class="o">:=</span> <span class="nx">gob</span><span class="p">.</span><span class="nf">NewEncoder</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">payload</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">encoder</span><span class="p">.</span><span class="nf">Encode</span><span class="p">(</span><span class="nx">newUser</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">newMessage</span> <span class="o">:=</span> <span class="nx">message</span><span class="p">.</span><span class="nf">NewMessage</span><span class="p">(</span><span class="nx">watermill</span><span class="p">.</span><span class="nf">NewULID</span><span class="p">(),</span> <span class="nx">payload</span><span class="p">.</span><span class="nf">Bytes</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">[]</span><span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">{</span><span class="nx">newMessage</span><span class="p">},</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span></code></pre></div><p>This example is complex enough to introduce a new <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-sql%2Fblob%2Fmaster%2Fpkg%2Fsql%2Fschema_adapter_mysql.go%23L11" target="_blank">Schema Adapter</a>. Refer to the full source to see the implementation: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Freal-world-examples%2Fsynchronizing-databases" target="_blank">synchronizing-databases example</a>.</p> <h2 id="limitations">Limitations</h2> <p>While you can implement all patterns above using Watermill&rsquo;s SQL Pub/Sub, it has some limitations.</p> <p>Currently, only the <code>INSERT</code> statement triggers new messages sent to the subscriber. The good news is there&rsquo;s planned support for <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F5" target="_blank">MySQL binlog</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F126" target="_blank">PostgreSQL LISTEN/NOTIFY</a> subscribers, and they allow listening for updates and deletes as well.</p> <p>The SQL Pub/Sub is also a low performer <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill-benchmark" target="_blank">compared to other Pub/Subs</a>. The current MySQL subscriber can process about 150 messages per second. That&rsquo;s almost 13 million messages daily, so there&rsquo;s a chance that&rsquo;s more than enough for your needs.</p> <p><strong>Update:</strong> There&rsquo;s an interesting <a href="proxy.php?url=https%3A%2F%2Fnews.ycombinator.com%2Fitem%3Fid%3D21834152" target="_blank">discussion on Hacker News</a> on using databases as Pub/Subs and related topics.</p>Golang CQRS, Metrics and AMQP - Watermill v0.3.0 releasedhttps://threedots.tech/post/watermill-0-3/Wed, 13 Feb 2019 00:00:00 +0100https://threedots.tech/post/watermill-0-3/<p>54 days of work, 12,909 lines of code, 47 Monsters and 42 KFC Twisters later finally it is Watermill <code>v0.3.0</code>! To keep it short, let&rsquo;s go through the changes.</p> <p>One important thing: at the end of this post there is a 3 question survey. Please take a moment to fill it out, it will help us make Watermill even better.</p> <h3 id="cqrs-component">CQRS component</h3> <p>One of the most important parts of the <code>v0.3.0</code> release is built-in <strong>CQRS</strong> support. The implementation itself is pretty simple and based on the existing <code>message.Router</code> and <code>message.PubSub</code> examples.</p> <p>We built CQRS in a way that allows you to use this component with your pre-existing (even not Golang-based) infrastructure. All you need to do is implement your own Marshaler.</p> <p>If you are interested in how to use CQRS, please see <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fcqrs%2F" target="_blank">watermill.io documentation</a>.</p> <p><a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fcqrs%2F%23usage" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="508" height="300" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fwatermill-0-3%2Fcqrs-event-storming_hue8a0f6dd589391d46e816eecd35fa288_21079_508x300_resize_q80_h2_lanczos_3.webp" alt="CQRS Event Storming" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fwatermill-0-3%5C%2Fcqrs-event-storming_hue8a0f6dd589391d46e816eecd35fa288_21079_508x300_resize_lanczos_3.png"" /> </a></p> <p>Thanks to <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Fadymitruk" target="_blank">@adymitruk</a> for a bit of consulting and inspirations!</p> <h3 id="prometheus-metrics-component">Prometheus metrics component</h3> <p>Second, not less important feature is the <strong>metrics component</strong>. Our goal was to allow you to add metrics to your Watermill application in one minute.</p> <p>All you need to do, is add a few lines:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">prometheusRegistry</span><span class="p">,</span> <span class="nx">closeMetricsServer</span> <span class="o">:=</span> <span class="nx">metrics</span><span class="p">.</span><span class="nf">CreateRegistryAndServeHTTP</span><span class="p">(</span><span class="s">&#34;:8081&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">defer</span> <span class="nf">closeMetricsServer</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="nx">metrics</span><span class="p">.</span><span class="nf">NewPrometheusMetricsBuilder</span><span class="p">(</span><span class="nx">prometheusRegistry</span><span class="p">,</span> <span class="s">&#34;&#34;</span><span class="p">,</span> <span class="s">&#34;&#34;</span><span class="p">).</span><span class="nf">AddPrometheusRouterMetrics</span><span class="p">(</span><span class="nx">router</span><span class="p">)</span> </span></span></code></pre></div><p>and <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fmetrics%2F%23importing-the-dashboard" target="_blank">load our dashboard</a> in your Grafana instance.</p> <p><a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fmetrics%2F%23importing-the-dashboard" target="_blank"> <img title="" loading="lazy" decoding="async" class="img img-center" width="720" height="540" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fwatermill-0-3%2Fgrafana-dashboard_hu1b191495316c78bb04119af7067e3b94_120986_720x540_resize_q80_h2_lanczos_3.webp" alt="Watermill Grafana Dashboard" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fwatermill-0-3%5C%2Fgrafana-dashboard_hu1b191495316c78bb04119af7067e3b94_120986_720x540_resize_lanczos_3.png"" /> </a></p> <p>If you are not using Prometheus or Grafana in your system, you may use this <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2Fcomponents%2Fmetrics" target="_blank">component</a> as the reference implementation. It is not using any internals, but only publicly available handler middlewares and pub/sub decorators.</p> <p>For the detailed documentation, please check <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fmetrics%2F" target="_blank">watermill.io website</a>.</p> <p>All kudos for the implementation goes to <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmaclav3" target="_blank">@maclav3</a>!</p> <h3 id="amqp-rabbitmq-pubsub-implementation">AMQP (RabbitMQ) Pub/Sub implementation</h3> <p>We are happy to announce that AMQP Pub/Sub has joined the list of <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fpub-sub-implementations%2F" target="_blank">supported Pub/Sub implementations</a>. You can now use it in addition to <strong>Kafka</strong>, <strong>Google Cloud Pub/Sub</strong>, and <strong>NATS</strong>.</p> <p>The title may be a bit confusing, because <strong>RabbitMQ</strong> can work both as a Pub/Sub (fanout) and a queue - and we support both ways. There are available <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fmessage%2Finfrastructure%2Famqp%2Fconfig.go" target="_blank">pre-created configs</a> which allow you to create a Pub/Sub or a queue.</p> <p>The implementation is based on <code>github.com/streadway/amqp</code>, but offers <strong>higher-level</strong> API and <strong>reconnect</strong> support. You can use it as a standalone Pub/Sub, but I kindly recommend to use <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fgetting-started%2F%23using-messages-router" target="_blank"><code>message.Router</code></a>.</p> <p>As always, I encourage you to check <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fgetting-started%2F" target="_blank">getting started with Watermill</a> and <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fpub-sub-implementations%2F%23rabbitmq-amqp" target="_blank">watermill.io documentation</a>. Please keep in mind, that there are some glossary incompatibilities between Watermill and AMQP (for example, <code>topic</code> has a different meaning). They are all explained in the documentation.</p> <h3 id="http-publisher">HTTP Publisher</h3> <p>Can HTTP work as a Pub/Sub? Of course! At least when it implements Watermill&rsquo;s <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fmessage%2Fpubsub.go" target="_blank"><code>message.PubSub</code></a> interface. :)</p> <p>But why? For example, you can create deadly easy <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples%2Fkafka-to-http" target="_blank">Kafka-based webhooks</a>.</p> <p>See more details on this publisher in the <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fpub-sub-implementations%2F%23http" target="_blank">documentation</a>.</p> <p>Thanks <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmaclav3" target="_blank">@maclav3</a> for the implementation!</p> <h3 id="added-contextcontext-to-subscribe">Added <code>context.Context</code> to <code>Subscribe</code></h3> <p><code>Subscribe</code> now accepts a context as the first parameter. When the context is canceled, the subscription will be closed. This context is also added to the received message.</p> <h3 id="gochannel-pubsub-update">GoChannel Pub/Sub update</h3> <ul> <li>added <code>Persistent</code> option, which stores all produced message in memory and sends them to all new subscribers.</li> <li>added <code>BlockPublishUntilSubscriberAck</code>, which will block <code>Publish</code> until the message is Acked by all consumers.</li> </ul> <h3 id="uuid-and-ulid-functions">UUID and ULID functions</h3> <p>We were using <code>satori/go.uuid</code> in many places. Unfortunately, this library is no longer maintained. We replaced all UUID functions in the project with just added <code>watermill.NewUUID()</code>, <code>watermill.NewULID()</code> and <code>watermill.NewShortUUID()</code>.</p> <p>You may use our functions or your own implementation - UUID is still a <code>string</code>.</p> <h3 id="misc">Misc</h3> <ul> <li>Travis CI replaced with GitLab CI. Thanks <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Fm1_10sz" target="_blank">@m1_10sz</a>!</li> <li>Rewritten and optimized <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fmessage%2Finfrastructure%2Ftest_pubsub.go" target="_blank">pub/sub tests</a>.</li> <li>Updated and added new <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Ftree%2Fmaster%2F_examples" target="_blank">examples</a> and docs.</li> <li>Made some <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2FUPGRADE-0.3.md" target="_blank">breaking changes</a>.</li> <li>Added <code>SubscribeInitialize</code> method for <strong>some</strong> subscribers, which require initialization before publish (for example: create AMQP exchange, queue, etc.). If you are using <code>Subscribe</code> before <code>Publish</code>, it is not required!</li> <li>Better stack traces for <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2Fmaster%2Fmessage%2Frouter%2Fmiddleware%2Frecoverer.go" target="_blank"><code>middleware.Recoverer</code></a>.</li> <li>Added <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fblob%2F402ef6745e88c3fff283006c39bdfa67f3deab6d%2Fmessage%2Fsubscriber%2Fread.go" target="_blank"><code>subscriber.BulkReadWithDeduplication</code></a>.</li> <li>Added Publishers and Subscribers decorators support to the router (used in the metrics component, for example).</li> <li>Added <code>watermill.LogFields.Copy</code> method.</li> <li>Added <code>watermill.LoggerAdapter.With</code> method to the interface.</li> </ul> <h3 id="survey">Survey</h3> <p>In the end, we would like to ask for your feedback. We created a short <a href="proxy.php?url=https%3A%2F%2Fwww.surveymonkey.com%2Fr%2F2KX8WMX" target="_blank">1-minute survey</a>.</p> <p>We will be grateful for your answers, as it will help us to keep improving Watermill.</p> <h3 id="support">Support</h3> <p>If you didn&rsquo;t find an answer to your question <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2F" target="_blank">in the documentation</a>, you can hit me on Twitter: <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Froblaszczak" target="_blank">@roblaszczak</a>.</p> <p>Also, check out the <strong>#watermill</strong> channel on <a href="proxy.php?url=https%3A%2F%2Fgophers.slack.com%2F" target="_blank">gophers.slack.com</a> (<a href="proxy.php?url=https%3A%2F%2Fgophersinvite.herokuapp.com%2F" target="_blank">invite here</a>).</p>GitLab CI tips for building custom workflowshttps://threedots.tech/post/gitlab-ci-tips-for-building-custom-workflows/Mon, 14 Jan 2019 00:00:00 +0100https://threedots.tech/post/gitlab-ci-tips-for-building-custom-workflows/<p>This time I&rsquo;d like to touch on a few more advanced topics related to GitLab CI. The common theme here is implementing custom features within your pipeline. Again, most of the tips are specific to GitLab, but some could be easily applied in other CI systems as well.</p> <h2 id="running-integration-tests">Running integration tests</h2> <p>Checking your code with <strong>unit tests</strong> is usually easy to plug into any CI system. It usually is as simple as running one command built in your language&rsquo;s toolset. In these tests, you will most likely use various mocks and stubs to hide implementation details and focus on testing particular logic. For example, you can use in-memory database as storage or write stub HTTP clients that always return some prepared payloads.</p> <p>However, eventually, you will need to run <strong>integration tests</strong> to cover more unusual test cases. I don&rsquo;t want to start a discussion on all possible test types here, so let me say this: by <em>integration tests</em> I mean tests that use some sort of external resource. This can be a real database server, an HTTP service, attachable storage, and so on.</p> <p>GitLab makes it trivial to run attached resources as Docker containers, linked with the container running your scripts. You can use the <a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fci%2Fdocker%2Fusing_docker_images.html%23what-is-a-service" target="_blank">services</a> keyword to define these dependencies. They will be accessible by their image name or your chosen name if you specify it in the <code>alias</code> field.</p> <p>This is a simple example of using an attached MySQL container:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">integration_tests</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">tests</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">services</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysql:8</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">alias</span><span class="p">:</span><span class="w"> </span><span class="l">db</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./run_tests.sh db:3306</span><span class="w"> </span></span></span></code></pre></div><p>In that case, you have to connect to the <code>db</code> hostname in your test scripts. Using an alias is usually a good idea, as you can switch the images without changing the test code. For example, you could change <code>mysql</code> image with <code>mariadb</code> and the script would still run correctly.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="waiting-for-containers">Waiting for containers</h4> <p>Because attached containers can take a while to warm up, you will probably need to wait before sending any requests. A simple way to do this is by using <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fvishnubob%2Fwait-for-it" target="_blank">wait-for-it.sh</a> script with a specified timeout.</p> </p></div> </div> <h3 id="using-docker-compose">Using Docker Compose</h3> <p>Using <code>services</code> should be good enough for most of your use cases. However, sometimes you need the external services to communicate with each other. One example may be running Kafka and ZooKeeper in two separate containers (the official images are built this way). Another is running tests with a dynamic number of nodes, like selenium. For running services like this, a better solution is to use <a href="proxy.php?url=https%3A%2F%2Fdocs.docker.com%2Fcompose%2F" target="_blank">Docker Compose</a>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3&#39;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">zookeeper</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">confluentinc/cp-zookeeper</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ZOOKEEPER_CLIENT_PORT</span><span class="p">:</span><span class="w"> </span><span class="m">2181</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">kafka</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">confluentinc/cp-kafka</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">KAFKA_ZOOKEEPER_CONNECT</span><span class="p">:</span><span class="w"> </span><span class="l">zookeeper:2181</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">KAFKA_ADVERTISED_LISTENERS</span><span class="p">:</span><span class="w"> </span><span class="l">PLAINTEXT://kafka:9092</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="m">9092</span><span class="p">:</span><span class="m">9092</span><span class="w"> </span></span></span></code></pre></div><p>If you&rsquo;re running your own GitLab runners on a trusted server, you can use <a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Frunner%2Fexecutors%2Fshell.html" target="_blank">Shell executor</a> to run Docker Compose.<br> Another option may be using <a href="proxy.php?url=https%3A%2F%2Fhub.docker.com%2F_%2Fdocker" target="_blank">Docker in Docker</a> (<code>dind</code>) container. <a href="proxy.php?url=https%3A%2F%2Fjpetazzo.github.io%2F2015%2F09%2F03%2Fdo-not-use-docker-in-docker-for-ci%2F" target="_blank">Read this first</a>, though.</p> <p>One way of using Compose is to set up the environment, run tests and tear it down. A simple bash script would look like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker-compose up -d </span></span><span class="line"><span class="cl">./run_tests.sh localhost:9092 </span></span><span class="line"><span class="cl">docker-compose down </span></span></code></pre></div><p>This solution is fine as long as your tests can run in a minimal environment. It may happen though, that you will need some dependencies installed. There is another way of running tests in Docker Compose that lets you built your own docker image with the test environment. You make one of the containers run the tests and exit with proper code.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3&#39;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">zookeeper</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">confluentinc/cp-zookeeper</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ZOOKEEPER_CLIENT_PORT</span><span class="p">:</span><span class="w"> </span><span class="m">2181</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">kafka</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">confluentinc/cp-kafka</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">KAFKA_ZOOKEEPER_CONNECT</span><span class="p">:</span><span class="w"> </span><span class="l">zookeeper:2181</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">KAFKA_ADVERTISED_LISTENERS</span><span class="p">:</span><span class="w"> </span><span class="l">PLAINTEXT://kafka:9092</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">tests</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">registry.example.com/some-image</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">./run_tests.sh kafka:9092</span><span class="w"> </span></span></span></code></pre></div><p>Notice that we got rid of the <code>ports</code> mapping. In this example, tests can communicate directly with all services.</p> <p>Tests can now be executed with a single command:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker-compose up --exit-code-from tests </span></span></code></pre></div><p>The <code>--exit-code-from</code> option implies <code>--abort-on-container-exit</code>, which means the whole environment started by <code>docker-compose up</code> will be stopped after one of the containers exits. The exit code of that command will be equal to the exit code of the chosen service (<code>tests</code> in the example above). So if the command running your tests exits with a non-zero code, the whole <code>docker-compose up</code> command will exit with this code.</p> <h2 id="using-labels-as-ci-tags">Using labels as CI tags</h2> <p>A word of warning: this is rather an unusual idea, but one I&rsquo;ve found very useful and flexible. As you probably know, GitLab has <a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fuser%2Fproject%2Flabels.html" target="_blank">Labels</a> feature on project and group levels. Labels can be attached to issues and merge requests. They have no relation to the pipelines, though.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="274" height="444" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fgitlab-ci-tips%2Flabels_hufdc3dd5ea1635381701bb6e9f0123315_22406_274x444_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fgitlab-ci-tips%5C%2Flabels_hufdc3dd5ea1635381701bb6e9f0123315_22406_274x444_resize_lanczos_3.png"" /> <p>With a bit of hacking, you can access the merge request labels in the job scripts. As of GitLab 11.6, this is now even easier, as there is a <code>CI_MERGE_REQUEST_IID</code> environment variable (yes, it&rsquo;s <code>IID</code>, not <code>ID</code>) available if your pipeline is using <code>only: merge_requests</code>.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>If you&rsquo;re not using <code>only: merge_requests</code> or running an older version of GitLab, you can still get the MR with an API call.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl <span class="s2">&#34;</span><span class="nv">$CI_API_V4_URL</span><span class="s2">/projects/</span><span class="nv">$CI_PROJECT_ID</span><span class="s2">/repository/commits/</span><span class="nv">$CI_COMMIT_SHA</span><span class="s2">/merge_requests?private_token=</span><span class="nv">$GITLAB_TOKEN</span><span class="s2">&#34;</span> </span></span></code></pre></div><p><code>iid</code> is the field you&rsquo;re after. Just be aware that this could return multiple MRs for a given commit.</p> </p></div> </div> <p>When you get the MR IID, all that&rsquo;s left is to call the <a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fapi%2Fmerge_requests.html" target="_blank">Merge Requests API</a> and use the <code>labels</code> field from the response.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl <span class="s2">&#34;</span><span class="nv">$CI_API_V4_URL</span><span class="s2">/projects/</span><span class="nv">$CI_PROJECT_ID</span><span class="s2">/merge_requests/</span><span class="nv">$CI_MERGE_REQUEST_IID</span><span class="s2">?private_token=</span><span class="nv">$GITLAB_TOKEN</span><span class="s2">&#34;</span> </span></span></code></pre></div><h3 id="authorization">Authorization</h3> <p>Sadly, using <code>$CI_JOB_TOKEN</code> to access project&rsquo;s API is <a href="proxy.php?url=https%3A%2F%2Fgitlab.com%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29566" target="_blank">not possible at the moment</a> (at least unless the project is public). If your project has restricted access (internal or private), you will need to generate a personal API token to authorize with the GitLab API.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="651" height="578" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fgitlab-ci-tips%2Fprivate-token_hud558a8fdc43332bd9f00e980175236c3_43550_651x578_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fgitlab-ci-tips%5C%2Fprivate-token_hud558a8fdc43332bd9f00e980175236c3_43550_651x578_resize_lanczos_3.png"" /> <p>This is not the most secure solution, though, so be careful. If this token would be leaked, someone could gain write access to all of your projects. One way of reducing the risk is to create a separate account with read-only access to the repository and generate a personal token for this account.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <h4 id="how-safe-are-your-variables">How safe are your variables?</h4> <p>A few versions ago, the <em>Variables</em> section has been called <em>Secret Variables</em>, which could sound like they were designed to store credentials and sensitive data safe. Frankly, the variables are just hidden from users who don&rsquo;t have Maintainer permissions. They are not encrypted on disk and can be easily leaked as environment variables in your scripts.</p> <p>Keep it in mind when adding any variables and consider storing your secrets in safer solutions (e.g. <a href="proxy.php?url=https%3A%2F%2Fwww.vaultproject.io%2F" target="_blank">HashiCorp&rsquo;s Vault</a>).</p> </p></div> </div> <h3 id="use-cases">Use cases</h3> <p>It&rsquo;s now up to you to decide what to do with the list of labels. Some ideas:</p> <ul> <li>Use it for tests segmentation.</li> <li>Introduce key-value semantics with a colon (e.g. labels like <code>tests:auth</code>, <code>tests:user</code>)</li> <li>Enable some specific features for jobs.</li> <li>Allow debugging of specific jobs when a label is present.</li> </ul> <h2 id="calling-external-apis">Calling external APIs</h2> <p>While GitLab comes with a suite of included features, it&rsquo;s very likely you still use other tools that could be integrated with your pipelines. The simplest way to do it is of course by good old <code>curl</code> calls.</p> <p>If you&rsquo;re writing your own tools, you could also have it listen to <a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fuser%2Fproject%2Fintegrations%2Fwebhooks.html" target="_blank">GitLab&rsquo;s Webhooks</a> (look up the <strong>Integrations</strong> tab in the project&rsquo;s settings). If you&rsquo;d use it with some critical systems though, make sure they are highly-available.</p> <h3 id="example-grafana-annotations">Example: Grafana annotations</h3> <p>If you&rsquo;re using <a href="proxy.php?url=https%3A%2F%2Fgrafana.com%2F" target="_blank">Grafana</a>, <a href="proxy.php?url=http%3A%2F%2Fdocs.grafana.org%2Freference%2Fannotations%2F" target="_blank">annotations</a> are a nice way of marking on graphs an event that happened in time. While it&rsquo;s possible to add them by clicking in the GUI, they can also be added by the <a href="proxy.php?url=http%3A%2F%2Fdocs.grafana.org%2Fhttp_api%2Fannotations%2F" target="_blank">Grafana REST API</a>.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1155" height="459" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fgitlab-ci-tips%2Fgrafana-annotations_hud97ebb71356def1399bd8f88718f1b14_44937_1155x459_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fgitlab-ci-tips%5C%2Fgrafana-annotations_hud97ebb71356def1399bd8f88718f1b14_44937_1155x459_resize_lanczos_3.png"" /> <p>You will need to generate an API Key to access the API. Consider creating a dedicated user with restricted access.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="958" height="177" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fgitlab-ci-tips%2Fgrafana-api-key_huf0a5860a540e70a38d6c618160353e7e_509772_958x177_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fgitlab-ci-tips%5C%2Fgrafana-api-key_huf0a5860a540e70a38d6c618160353e7e_509772_958x177_resize_lanczos_3.png"" /> <p>Set two variables in your project settings:</p> <ul> <li><code>GRAFANA_URL</code> - the URL of your Grafana instance (e.g. <code>https://grafana.example.com</code>)</li> <li><code>GRAFANA_APIKEY</code> - the generated API Key</li> </ul> <p>To keep it re-usable, you can put the script in the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fkeeping-common-scripts-in-gitlab-ci%2F">common scripts repository</a>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash </span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="nb">set</span> -e </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="nv">$#</span> -lt <span class="m">2</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> </span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">&#34;Usage: </span><span class="nv">$0</span><span class="s2"> &lt;text&gt; &lt;tag&gt;&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nb">exit</span> <span class="m">1</span> </span></span><span class="line"><span class="cl"><span class="k">fi</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">readonly</span> <span class="nv">text</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"><span class="nb">readonly</span> <span class="nv">tag</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"><span class="nb">readonly</span> <span class="nv">time</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>date +%s<span class="k">)</span><span class="s2">000&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">cat &gt;./payload.json <span class="s">&lt;&lt;EOF </span></span></span><span class="line"><span class="cl"><span class="s">{ </span></span></span><span class="line"><span class="cl"><span class="s"> &#34;text&#34;: &#34;$text&#34;, </span></span></span><span class="line"><span class="cl"><span class="s"> &#34;tags&#34;: [&#34;$tag&#34;], </span></span></span><span class="line"><span class="cl"><span class="s"> &#34;time&#34;: $time, </span></span></span><span class="line"><span class="cl"><span class="s"> &#34;timeEnd&#34;: $time </span></span></span><span class="line"><span class="cl"><span class="s">} </span></span></span><span class="line"><span class="cl"><span class="s">EOF</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">curl -X POST <span class="s2">&#34;</span><span class="nv">$GRAFANA_URL</span><span class="s2">/api/annotations&#34;</span> <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> -H <span class="s2">&#34;Authorization: Bearer </span><span class="nv">$GRAFANA_APIKEY</span><span class="s2">&#34;</span> <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> -H <span class="s2">&#34;content-type: application/json&#34;</span> <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> -d @./payload.json </span></span></code></pre></div><p>Now call it with proper parameters in the CI definition:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">deploy</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">deploy</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">$SCRIPTS_DIR/deploy.sh production</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">$SCRIPTS_DIR/grafana-annotation.sh &#34;$VERSION deployed to production&#34; deploy-production</span><span class="w"> </span></span></span></code></pre></div><p>You could also put it in the <code>deploy.sh</code> script to keep the CI definition even simpler.</p> <h2 id="bonus-quick-tips">Bonus quick tips</h2> <p>GitLab has a <a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fci%2Fyaml" target="_blank">great documentation</a> on all possible keywords in the CI definition. I don&rsquo;t want to duplicate the content here, but I&rsquo;d like to point out a few useful use cases. Click on the headers to view the docs.</p> <h3 id="advanced-onlyexcept-usagehttpsdocsgitlabcomeeciyamlonly-and-except-simplified"><a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fci%2Fyaml%2F%23only-and-except-simplified" target="_blank">Advanced only/except usage</a></h3> <p>Use pattern matching on variables to enable custom builds for some of the branches. You don&rsquo;t want to overuse it, but if you quickly need to push a hotfix, this might help.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">only</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">refs</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">branches</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">variables</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">$CI_COMMIT_REF_NAME =~ /^hotfix/</span><span class="w"> </span></span></span></code></pre></div><p>GitLab includes a lot of <a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fci%2Fvariables%2F%23predefined-variables-environment-variables" target="_blank">predefined variables</a> in each CI job, make use of them.</p> <h3 id="yaml-anchorshttpsdocsgitlabcomeeciyamlanchors"><a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fci%2Fyaml%2F%23anchors" target="_blank">YAML anchors</a></h3> <p>Use them to avoid duplication.</p> <p>Since 11.3 you can also use the <a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fci%2Fyaml%2F%23extends" target="_blank">extends</a> keyword.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">.common_before_script</span><span class="p">:</span><span class="w"> </span><span class="cp">&amp;common_before_script</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">before_script</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">...</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">...</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">deploy</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">&lt;&lt;</span><span class="p">:</span><span class="w"> </span><span class="cp">*common_before_script</span><span class="w"> </span></span></span></code></pre></div><h3 id="skipping-dependencieshttpsdocsgitlabcomeeciyamldependencies"><a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fci%2Fyaml%2F%23dependencies" target="_blank">Skipping dependencies</a></h3> <p>By default, all artifacts built in the pipeline will be passed to all following jobs. You can save some time and disk space by explicitly listing artifacts the jobs depends on:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">dependencies</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">build</span><span class="w"> </span></span></span></code></pre></div><p>Alternatively, skip them entirely if none are required:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">dependencies</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w"> </span></span></span></code></pre></div><h3 id="git-strategyhttpsdocsgitlabcomeeciyamlgit-strategy"><a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fci%2Fyaml%2F%23git-strategy" target="_blank">Git strategy</a></h3> <p>Skip cloning the repository if the job won&rsquo;t use its files.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">variables</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">GIT_STRATEGY</span><span class="p">:</span><span class="w"> </span><span class="l">none</span><span class="w"> </span></span></span></code></pre></div><h2 id="thats-it">That&rsquo;s it!</h2> <p>Thanks for reading! Hit me up with feedback or questions on <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Fm1_10sz" target="_blank">Twitter</a> or <a href="proxy.php?url=https%3A%2F%2Fwww.reddit.com%2Fuser%2Fmi_losz" target="_blank">Reddit</a>.</p> <p>For more GitLab tips, check out my previous posts:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fkeeping-common-scripts-in-gitlab-ci%2F">Keeping common scripts in GitLab CI</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fautomatic-semantic-versioning-in-gitlab-ci%2F">Automatic Semantic Versioning in GitLab CI</a></li> </ul>Creating local Go dev environment with Docker and live code reloadinghttps://threedots.tech/post/go-docker-dev-environment-with-go-modules-and-live-code-reloading/Thu, 03 Jan 2019 00:00:00 +0100https://threedots.tech/post/go-docker-dev-environment-with-go-modules-and-live-code-reloading/<p>This post is a quick how-to for starting a new project in Go. It features:</p> <ul> <li>Hot code reloading</li> <li>Running multiple Docker containers with Docker Compose</li> <li>Using Go Modules for managing dependencies</li> </ul> <p>It&rsquo;s best to show the above working together with an example project. We&rsquo;re going to set up two separate services communicating with messages over <a href="proxy.php?url=https%3A%2F%2Fnats.io%2F" target="_blank">NATS</a>. The first one will receive messages on an HTTP endpoint and then publish them to a NATS topic. The other will subscribe to this topic and print incoming messages on the standard output.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="820" height="140" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fgo-development-environment%2Fschema_hu5b8eef11c30f53ff5fdf8c42fae24faa_10185_820x140_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fgo-development-environment%5C%2Fschema_hu5b8eef11c30f53ff5fdf8c42fae24faa_10185_820x140_resize_lanczos_3.png"" /> <p>It&rsquo;s pretty simple architecture, right? To keep the examples short and make handling messages easier, we&rsquo;re going to use the <a href="proxy.php?url=https%3A%2F%2Fwatermill.io" target="_blank">Watermill library</a>.</p> <p>I won&rsquo;t go through all Go code here, but you can clone the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fnats-example" target="_blank">example repository</a>.</p> <h2 id="requirements">Requirements</h2> <p>You&rsquo;re going to need Go 1.11+, <a href="proxy.php?url=https%3A%2F%2Fwww.docker.com%2F" target="_blank">Docker</a> and <a href="proxy.php?url=https%3A%2F%2Fdocs.docker.com%2Fcompose%2F" target="_blank">Docker Compose</a> installed on your local machine.</p> <h2 id="go-modules">Go Modules</h2> <p><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgolang%2Fgo%2Fwiki%2FModules" target="_blank">Go Modules</a> were introduced in Go 1.11 and are (hopefully) the final solution for managing dependencies.</p> <p>I recommend keeping the repository outside of <code>GOPATH</code> if you&rsquo;re going to use them. If you&rsquo;d prefer to use <code>GOPATH</code> anyway, make sure you&rsquo;ve set <code>GO111MODULE=on</code> variable.</p> <p>Let&rsquo;s initialize modules for each of our packages (inside <code>subscriber</code> and <code>publisher</code> directories).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go mod init github.com/ThreeDotsLabs/nats-example/subscriber </span></span><span class="line"><span class="cl">go mod init github.com/ThreeDotsLabs/nats-example/publisher </span></span></code></pre></div><p>Download dependencies:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go get github.com/ThreeDotsLabs/watermill </span></span><span class="line"><span class="cl">go get github.com/nats-io/go-nats-streaming </span></span></code></pre></div><p>Add an extra ULID library only for the <code>publisher</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go get github.com/oklog/ulid </span></span></code></pre></div><h2 id="dockerfile">Dockerfile</h2> <p>The next step is creating a Docker image for containers that will run both applications. The <code>Dockerfile</code> is fairly simple:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-docker" data-lang="docker"><span class="line"><span class="cl"><span class="k">FROM</span><span class="s"> golang:1.11.2-stretch</span><span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">RUN</span> go get github.com/cespare/reflex<span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">COPY</span> reflex.conf /<span class="err"> </span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;reflex&#34;</span><span class="p">,</span> <span class="s2">&#34;-c&#34;</span><span class="p">,</span> <span class="s2">&#34;/reflex.conf&#34;</span><span class="p">]</span><span class="err"> </span></span></span></code></pre></div><p>The image is based on the official Go image. Usually, you&rsquo;d want to stick with the smaller <code>alpine</code> version, but since it is missing some dependencies for compiling <code>reflex</code>, I&rsquo;m using the <code>stretch</code> version here.</p> <p>It installs and runs <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fcespare%2Freflex" target="_blank">reflex</a> which will be used for hot recompiling the code. It can be very useful for quickly testing your changes.</p> <p>Reflex is configured by <code>reflex.conf</code>, which in our case is just one line:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">-r <span class="s1">&#39;(\.go$|go\.mod)&#39;</span> -s go run . </span></span></code></pre></div><p>What the command means is: &ldquo;watch for changes to <code>go.mod</code> and all files ending in <code>.go</code> and execute <code>go run .</code> when it happens. The <code>-s</code> flag stands for <code>service</code> and will make reflex kill previously run command before starting it again, which is exactly what we want.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Reflex is not a regular dependency. It is installed and used only in the docker image and won&rsquo;t be deployed with your application. Thanks to <a href="proxy.php?url=https%3A%2F%2Fwww.reddit.com%2Fuser%2Fhabarnam" target="_blank">/u/habarnam</a> for pointing that out. </p></div> </div> <h2 id="docker-compose">Docker Compose</h2> <p>We&rsquo;ll use Docker Compose for running multiple services within a shared network. It allows you to spin up containers with services or databases without cluttering your local machine. Also, other developers can set up the project by cloning the repository and running one command, instead of manually installing dependencies and compiling the application.</p> <p>Put the <code>docker-compose.yml</code> file in the repository:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3&#39;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">publisher</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span><span class="l">.</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./publisher:/app</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">$GOPATH/pkg/mod/cache:/go/pkg/mod/cache</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">working_dir</span><span class="p">:</span><span class="w"> </span><span class="l">/app</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">env_file</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">.env</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">ports</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="m">5000</span><span class="p">:</span><span class="m">5000</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">subscriber</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span><span class="l">.</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">volumes</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">./subscriber:/app</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">$GOPATH/pkg/mod/cache:/go/pkg/mod/cache</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">working_dir</span><span class="p">:</span><span class="w"> </span><span class="l">/app</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">env_file</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">.env</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">nats</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nats-streaming:0.11.2</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="kc">on</span>-<span class="l">failure</span><span class="w"> </span></span></span></code></pre></div><p>It defines three services. Both <code>publisher</code> and <code>subscriber</code> are based on the <code>Dockerfile</code> we&rsquo;ve created before (this is done by the <code>build: .</code> option). Each application&rsquo;s code is mounted in the working directory, so <code>go run .</code> will run the proper package. The <code>ports</code> part in the <code>publisher</code> service will bind port 5000 on your local system and map it to the port inside container.</p> <p>I&rsquo;ve also mounted <code>$GOPATH/pkg/mod/cache</code> directory, to use existing cache for dependencies, speeding up downloads.</p> <p>The services are configured by environment variables, kept in a separate file:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">PORT</span><span class="o">=</span><span class="m">5000</span> </span></span><span class="line"><span class="cl"><span class="nv">NATS_TOPIC</span><span class="o">=</span>example </span></span><span class="line"><span class="cl"><span class="nv">NATS_CLUSTER_ID</span><span class="o">=</span>test-cluster </span></span><span class="line"><span class="cl"><span class="nv">NATS_URL</span><span class="o">=</span>nats://nats:4222 </span></span></code></pre></div><p>Notice the <code>nats</code> hostname in the <code>NATS_URL</code> variable - this is the name of the service defined in the <code>docker-compose.yml</code> file.</p> <h2 id="running">Running</h2> <p>To start the environment, run:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker-compose up </span></span></code></pre></div><p>You&rsquo;ll see the output of all three services. In another terminal, try sending an example request to the publisher service with <code>curl</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ curl -X POST http://localhost:5000 --data <span class="s2">&#34;this is my message&#34;</span> </span></span><span class="line"><span class="cl">Sent message: this is my message with ID 01D09P02SBW5D0QPWP14QQZJWH </span></span></code></pre></div><p>You should see the message delivered in the <code>subscriber</code> log output:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">subscriber_1 <span class="p">|</span> <span class="o">[</span>00<span class="o">]</span> 2019/01/03 11:01:27 received message: 01D09P02SBW5D0QPWP14QQZJWH, payload: this is my message </span></span></code></pre></div><p>Now, try editing either of the <code>main.go</code> files in your editor. After you save the file, appropriate service should be recompiled and restarted.</p> <script id="asciicast-kas6dYKpMzyubpCmW9aOjnBIu" src="proxy.php?url=https%3A%2F%2Fasciinema.org%2Fa%2Fkas6dYKpMzyubpCmW9aOjnBIu.js" async></script> <p>When finished, you can kill the environment with <code>Ctrl-C</code> in the terminal it is running. You can also delete all containers by running <code>docker-compose down</code>.</p> <h2 id="whats-next">What&rsquo;s next?</h2> <p>Now that you&rsquo;ve built Docker images of the services it&rsquo;s the first step towards deploying it. We&rsquo;ll look into it in the next post.</p> <p>If you&rsquo;d like to explore more docker-compose definitions, check <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fgetting-started%2F%3Futm_source%3Dwhats_next" target="_blank">Watermill Getting started</a> for Kafka and Google Cloud Pub/Sub examples.</p>Watermill v0.2.0 releasedhttps://threedots.tech/post/watermill-0-2/Wed, 19 Dec 2018 00:00:00 +0100https://threedots.tech/post/watermill-0-2/<p>Let me start by thanking all contributors for feedback on <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill" target="_blank">Watermill</a> - it drives us to add new features. Thanks!</p> <p>It&rsquo;s been almost a month since the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fintroducing-watermill%2F">initial release of Watermill</a>. However, it&rsquo;s just the beginning and we are still working hard to ship new features.</p> <p>What is new in Watermill 0.2?</p> <h3 id="documentation---watermilliohttpswatermillio">Documentation - <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2F" target="_blank">watermill.io</a></h3> <p>Godoc is great. However, it&rsquo;s functionality is sometimes too limited to express more complicated documentation.</p> <p>For that reason, we&rsquo;ve created <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2F" target="_blank">watermill.io</a>.</p> <p>The most important parts of the documentation are:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fgetting-started%2F" target="_blank">Getting Started guide</a> which will explain you the most basic concepts of the Watermill by the examples.</li> <li><a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fpub-sub-implementations%2F" target="_blank">Pub/Sub&rsquo;s implementations</a> which contains detailed documentation of all built-in Pub/Sub&rsquo;s implementations.</li> <li><a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fmessages-router%2F" target="_blank">Router documentation</a> which explains how Messages Router is working.</li> <li><a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2F" target="_blank">All documentation topics</a></li> </ul> <p>Of course, we are not abandoning godoc. You can still find all important information in the comments (in fact, a large part of <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2F" target="_blank">watermill.io</a> is generated directly from godocs).`</p> <h3 id="added-contextcontext-to-the-message">Added <code>context.Context</code> to the message.</h3> <p>From now on, the <code>Message</code> holds a <code>Context</code>, which carries deadlines, cancelation signals, and other message-scoped values.</p> <p>Unlike metadata, it is not serialized nor sent to the Pub/Sub.</p> <h3 id="replaced-kafka-pubsub-implementation-with-sarama">Replaced Kafka Pub/Sub implementation with Sarama</h3> <p>Watermill development started when <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FShopify%2Fsarama" target="_blank">Sarama</a> didn&rsquo;t have support for consumer groups yet. Fortunately, it is now available. Thanks to this fact we could replace <code>librdkafka</code> implementation with Sarama.</p> <p>Using Sarama allowed us to get entirely rid of the <code>cgo</code> dependency in Watermill. Another great bonus is a great improvement of messages publishing performance (quick benchmarks showed upgrade from 10k messages/s to 75k messages/s).</p> <p>Because the client implementation has been replaced, <strong>this change is not backward compatible</strong>.</p> <p>Kafka Breaking changes and API cleanups:</p> <ul> <li><code>kafka.NewCustomPublisher</code> has been removed, please use <code>NewPublisher</code> instead.</li> <li><code>kafka.ConfluentConsumerConstructor</code> has been removed and is no longer needed.</li> <li><code>kafka.SubscriberConfig.NoConsumerGroup</code> is no longer needed. You can now just pass empty <code>kafka.SubscriberConfig.ConsumerGroup</code>.</li> <li><code>kafka.AutoOffsetReset</code> has been removed. Please use <code>github.com/Shopify/sarama.Config.Consumer.Offsets.Initial</code> passed to <code>NewSubscriber</code> <code>overwriteSaramaConfig</code> argument instead.</li> <li><code>kafka.ConsumersCount</code> has been removed and is no longer needed.</li> <li><code>kafka.KafkaConfigOverwrite</code> has been removed and is no longer needed. You can now pass <code>sarama.Config</code> to <code>NewSubscriber</code> and <code>NewPublisher</code>.</li> <li><code>kafka.NewConfluentSubscriber</code> has been removed. Please use <code>kafka.NewSubscriber</code> instead.</li> <li><code>kafka.NewCustomConfluentSubscriber</code> has been removed. Please use <code>kafka.NewSubscriber</code> instead.</li> <li><code>kafka.DefaultConfluentConsumerConstructor</code> has been removed and is no longer needed.</li> <li><code>kafka.Marshaler</code> and <code>kafka.Unmarshaler</code> interfaces have been changed to be compatible with the Sarama API.</li> </ul> <h3 id="google-cloud-pubsub">Google Cloud Pub/Sub</h3> <blockquote> <p>Cloud Pub/Sub brings the flexibility and reliability of enterprise message-oriented middleware to the cloud. At the same time, Cloud Pub/Sub is scalable, durable event ingestion and delivery system that serves as a foundation for modern stream analytics pipelines. By providing many-to-many, asynchronous messaging that decouples senders and receivers, it allows for secure and highly available communication among independently written applications. Cloud Pub/Sub delivers low-latency, durable messaging that helps developers quickly integrate systems hosted on the Google Cloud Platform and externally.</p> </blockquote> <p>Detailed documentation can be found at <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fpub-sub-implementations%2F%23google-cloud-pub-sub" target="_blank">watermill.io</a> and in the <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fgetting-started%2F%23subscribing_gcloud" target="_blank">Getting Started Google Cloud section</a>.</p> <p>Credits goes to <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmaclav3" target="_blank">@maclav3</a>. Thanks for the contribution!</p> <h3 id="nats-streaming-pubsub">NATS Streaming Pub/Sub</h3> <blockquote> <p>NATS Streaming is a data streaming system powered by NATS, and written in the Go programming language. The executable name for the NATS Streaming server is nats-streaming-server. NATS Streaming embeds, extends, and interoperates seamlessly with the core NATS platform.</p> </blockquote> <p>Detailed documentation can be found at <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fpub-sub-implementations%2F%23nats-streaming" target="_blank">watermill.io</a> and in <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fgetting-started%2F%23subscribing_nats-streaming" target="_blank">Getting Started NATS section</a>.</p> <p>Built in cooperation with <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FNykakin" target="_blank">@Nykakin</a>, <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fmaclav3" target="_blank">@maclav3</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fm110" target="_blank">@m110</a>.</p> <h3 id="whats-next">What&rsquo;s next?</h3> <p>If there&rsquo;s something missing in Watermill, feel free to post a <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues" target="_blank">new feature request</a> or give thumbs up for the existing issue.</p> <p>For now, <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F8" target="_blank">Event sourcing</a>, <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F7" target="_blank">Sagas support</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F5" target="_blank">MySQL Binlog subscriber</a> seem to be the most popular.</p> <p>Also, the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F17" target="_blank">HTTP Publisher</a> sounds like not too much of work and may provide some interesting functionalities.</p> <h3 id="support">Support</h3> <p>If you didn&rsquo;t find an answer to your question <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2F" target="_blank">in the documentation</a>, you can hit me at Twitter: <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Froblaszczak" target="_blank">@roblaszczak</a>.</p> <p>Also, check out the <strong>#watermill</strong> channel on <a href="proxy.php?url=https%3A%2F%2Fgophers.slack.com%2F" target="_blank">gophers.slack.com</a> (<a href="proxy.php?url=https%3A%2F%2Fgophersinvite.herokuapp.com%2F" target="_blank">invite here</a>).</p>Automatic Semantic Versioning in GitLab CIhttps://threedots.tech/post/automatic-semantic-versioning-in-gitlab-ci/Thu, 13 Dec 2018 00:00:00 +0100https://threedots.tech/post/automatic-semantic-versioning-in-gitlab-ci/<p>In the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fkeeping-common-scripts-in-gitlab-ci%2F">previous post</a> I showed how to keep all the scripts used in the CI in one repository. Let&rsquo;s see what more advanced scripts you could put in there.</p> <p>This time I&rsquo;d like to show how to add automatic versioning to your pipeline. You will also see how to push commits to your repository within the CI jobs. But first, let&rsquo;s start with some background.</p> <h2 id="picking-your-flow">Picking your flow</h2> <p>One of the things I love about GitLab is its flexibility for setting up your own CI workflow. Whether you&rsquo;re doing one release each week, every other month, following <a href="proxy.php?url=https%3A%2F%2Fnvie.com%2Fposts%2Fa-successful-git-branching-model%2F" target="_blank">classic gitflow</a> or practicing continuous deployment, it&rsquo;s just a matter of the config.</p> <p>As a proponent of continuous delivery principles, I usually stick to a protected <code>master</code> branch and short-lived feature branches that are merged after code review. Each commit on the <code>master</code> is automatically deployed to some sort of staging environment (preferably identical with prod) and then can be manually promoted to production. If you have a solid suite of multi-layer tests, this workflow will let you deploy multiple times a day.</p> <h2 id="versioning">Versioning</h2> <p>Whatever flow you pick, versioning of your application becomes essential in many steps of the pipeline. For example, it is crucial to know precisely to which version you should rollback if something goes wrong. You should always be able to find the commit for a given version number quickly.</p> <p>The versioning system is up to you and will depend on your workflow. I usually stick with <a href="proxy.php?url=https%3A%2F%2Fsemver.org%2F" target="_blank">Semantic Versioning</a>, as it feels natural and it is already a standard for many projects, especially for libraries. So the first commit on the <code>master</code> branch would get version <code>1.0.0</code>, the next one <code>1.0.1</code>, then <code>1.0.2</code>, and so on (alternatively, you can start with <code>0.0.1</code> and treat <code>1.0.0</code> as first public release).</p> <p>Where to keep the versions? Some good practices from my experience:</p> <ol> <li>Don&rsquo;t keep it in the code or a file committed to the repository.</li> <li><strong>Git tags</strong> are a perfect fit for keeping versions tied with commits.</li> <li><strong>Automate it.</strong> Don&rsquo;t waste developers&rsquo; time on wondering which version number to pick.</li> </ol> <p>So basically, you want to have some automated version tagging wired into your CI system.</p> <h2 id="implementation">Implementation</h2> <p>The essential piece handling versioning is a script that runs on every job on the <code>master</code> branch. This script should take the most recent version, bump it, add a tag and push it to the remote repository.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> You probably don&rsquo;t need to generate versions on other branches. There are some valid use cases for this, but that&rsquo;s a story for another post. </p></div> </div> <p>This time I&rsquo;m going to use Python and the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fk-bx%2Fpython-semver" target="_blank">python-semver library</a>. If you&rsquo;d prefer to stick with bash, take a look at the <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Ffsaintjacques%2Fsemver-tool" target="_blank">semver-tool</a>.</p> <p>Let&rsquo;s see what the script should do.</p> <h3 id="1-pick-the-most-recent-version-and-bump-it">1. Pick the most recent version and bump it.</h3> <p>This is simple enough. You can extract the most recent tag by running <code>git describe --tags</code>. The semver library can then be used to bump the version.</p> <p>Our <code>main</code> function might look something like this (<a href="proxy.php?url=https%3A%2F%2Fgitlab.com%2Fthreedotslabs%2Fci-scripts%2Fblob%2Fmaster%2Fcommon%2Fgen-semver" target="_blank">full source</a>):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="n">latest</span> <span class="o">=</span> <span class="n">git</span><span class="p">(</span><span class="s2">&#34;describe&#34;</span><span class="p">,</span> <span class="s2">&#34;--tags&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">decode</span><span class="p">()</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">except</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">CalledProcessError</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="c1"># No tags in the repository</span> </span></span><span class="line"><span class="cl"> <span class="n">version</span> <span class="o">=</span> <span class="s2">&#34;1.0.0&#34;</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="c1"># Skip already tagged commits</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="s1">&#39;-&#39;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">latest</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="nb">print</span><span class="p">(</span><span class="n">latest</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="mi">0</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">version</span> <span class="o">=</span> <span class="n">bump</span><span class="p">(</span><span class="n">latest</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">tag_repo</span><span class="p">(</span><span class="n">version</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nb">print</span><span class="p">(</span><span class="n">version</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="mi">0</span> </span></span></code></pre></div><p>There is one catch, though - how will you decide whether to bump patch, minor or major version?</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">bump</span><span class="p">(</span><span class="n">latest</span><span class="p">):</span> </span></span><span class="line"><span class="cl"> <span class="c1"># TODO decide what to bump</span> </span></span><span class="line"><span class="cl"> <span class="c1"># Then use bump_patch, bump_minor or bump_major</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">semver</span><span class="o">.</span><span class="n">bump_patch</span><span class="p">(</span><span class="n">latest</span><span class="p">)</span> </span></span></code></pre></div><p>You will have to mark it in the commit somehow. I won&rsquo;t implement this in the example to keep it simple, but here are some ideas that could work:</p> <ul> <li>Make bumping patch the default behavior, as it is the most common operation.</li> <li>An intention to bump minor or major version can be marked in the commit message, e.g., by using some phrase like <code>#minor</code> or <code>bump-minor</code>.</li> <li>Similarly, you could attach labels to the Merge Requests called <code>bump-minor</code> and <code>bump-major</code>.</li> <li>You could come up with a script that detects whether any breaking changes were introduced or new functionalities added.</li> </ul> <h3 id="2-add-a-new-tag-and-push-it-to-the-remote-repository">2. Add a new tag and push it to the remote repository.</h3> <h4 id="authentication">Authentication</h4> <p>This step requires the CI job to have write access to the repository. Sadly, as of now, GitLab still doesn&rsquo;t support <a href="proxy.php?url=https%3A%2F%2Fgitlab.com%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F18106" target="_blank">pushing changes back to the repository</a> out of the box. Deploy tokens shown in the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fkeeping-common-scripts-in-gitlab-ci%2F">previous post</a> can&rsquo;t be used here, since they allow only read-only access. So we&rsquo;re left with <a href="proxy.php?url=https%3A%2F%2Fgitlab.com%2Fhelp%2Fssh%2FREADME" target="_blank">Deploy Keys</a>.</p> <p>First, generate a new key on your local machine (no passphrase):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-keygen -t rsa -b <span class="m">4096</span> </span></span></code></pre></div><p>Add the <em>public</em> part as a new <strong>Deploy Key</strong> in the <code>Settings -&gt; Repository</code> section. Make sure to check the &ldquo;Write access allowed&rdquo; option.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="1032" height="578" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fautomatic-semver%2Fdeploy-keys_hu2882159e378151d2687f0226839e7e91_66554_1032x578_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fautomatic-semver%5C%2Fdeploy-keys_hu2882159e378151d2687f0226839e7e91_66554_1032x578_resize_lanczos_3.png"" /> <p>Add the <em>private</em> part as a new Variable in the CI/CD section. The name is up to you; I&rsquo;ll stick with <code>SSH_PRIVATE_KEY</code>.</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="975" height="444" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fautomatic-semver%2Fvariables_hu562ec0bd7d84bba0a0b661e39c5260fd_48382_975x444_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fautomatic-semver%5C%2Fvariables_hu562ec0bd7d84bba0a0b661e39c5260fd_48382_975x444_resize_lanczos_3.png"" /> <p>After you&rsquo;ve saved the keys in GitLab, it is a good idea to delete the private key file (or better yet, <code>shred</code> it).</p> <p>All that&rsquo;s left is to add the SSH key to the CI definition. There are several ways to do it, one of them looks like this (change <code>gitlab.com</code> with your hostname if you&rsquo;re using self-hosted GitLab):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">script</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">mkdir -p ~/.ssh &amp;&amp; chmod 700 ~/.ssh</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">ssh-keyscan gitlab.com &gt;&gt; ~/.ssh/known_hosts &amp;&amp; chmod 644 ~/.ssh/known_hosts</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">eval $(ssh-agent -s)</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">ssh-add &lt;(echo &#34;$SSH_PRIVATE_KEY&#34;)</span><span class="w"> </span></span></span></code></pre></div><h4 id="pushing-the-tag">Pushing the tag</h4> <p>A simple <code>git push</code> won&rsquo;t work right away, as the repository is cloned using HTTPS, not SSH. The simplest solution is to transform the remote push URL by using a regex.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">tag_repo</span><span class="p">(</span><span class="n">tag</span><span class="p">):</span> </span></span><span class="line"><span class="cl"> <span class="n">url</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">&#34;CI_REPOSITORY_URL&#34;</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1"># Transforms the repository URL to the SSH URL</span> </span></span><span class="line"><span class="cl"> <span class="c1"># Example input: https://gitlab-ci-token:[email protected]/threedotslabs/ci-examples.git</span> </span></span><span class="line"><span class="cl"> <span class="c1"># Example output: [email protected]:threedotslabs/ci-examples.git</span> </span></span><span class="line"><span class="cl"> <span class="n">push_url</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s1">&#39;.+@([^/]+)/&#39;</span><span class="p">,</span> <span class="sa">r</span><span class="s1">&#39;git@\1:&#39;</span><span class="p">,</span> <span class="n">url</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">git</span><span class="p">(</span><span class="s2">&#34;remote&#34;</span><span class="p">,</span> <span class="s2">&#34;set-url&#34;</span><span class="p">,</span> <span class="s2">&#34;--push&#34;</span><span class="p">,</span> <span class="s2">&#34;origin&#34;</span><span class="p">,</span> <span class="n">push_url</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">git</span><span class="p">(</span><span class="s2">&#34;tag&#34;</span><span class="p">,</span> <span class="n">tag</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">git</span><span class="p">(</span><span class="s2">&#34;push&#34;</span><span class="p">,</span> <span class="s2">&#34;origin&#34;</span><span class="p">,</span> <span class="n">tag</span><span class="p">)</span> </span></span></code></pre></div><h3 id="3-pass-information-about-the-version-to-the-next-build-steps">3. Pass information about the version to the next build steps.</h3> <p>It&rsquo;s very likely that more than one job in your pipeline will need to know the generated version. The idiomatic way to achieve this is to pass a file with the version as an artifact.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">python:3.7-stretch</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">version</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">pip install semver</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">$SCRIPTS_DIR/common/gen-semver &gt; version</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">artifacts</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">paths</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">version</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">only</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">branches</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">build</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">golang:1.11</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">build</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">export VERSION=&#34;unknown&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="s2">&#34;[ -f ./version ] &amp;&amp; export VERSION=$(cat ./version)&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">$SCRIPTS_DIR/golang/build-semver . example-server main.Version &#34;$VERSION&#34;</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">artifacts</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">paths</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">bin/</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">only</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">branches</span><span class="w"> </span></span></span></code></pre></div><div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> If you missed the <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fpost%2Fkeeping-common-scripts-in-gitlab-ci%2F">common scripts post</a> or don&rsquo;t want to use it, you can also commit the script in your application repository. </p></div> </div> <p>The next steps can now read the <code>./version</code> file. You can also make it more convenient with a one-liner placed in <code>before_script</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">before_script</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="p">[</span><span class="w"> </span>-<span class="l">f ./version ] &amp;&amp; export VERSION=$(cat ./version)</span><span class="w"> </span></span></span></code></pre></div><h3 id="dont-run-on-tags">Don&rsquo;t run on tags</h3> <p>Remember to put proper <code>only</code> setting in your step definition or your automatic tag pushes will trigger new pipelines. Limiting it to <code>branches</code> should be easy enough:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">only</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">branches</span><span class="w"> </span></span></span></code></pre></div><h2 id="what-about-changelogs">What about changelogs?</h2> <p>Some workflows generate the version based on a <code>CHANGELOG</code> file in the repository. I don&rsquo;t recommend this approach, as this forces developers to come up with versions, duplicates the commit messages and is prone to merge conflicts.</p> <p>Instead, you can treat the git log itself as a changelog (focus on <a href="proxy.php?url=https%3A%2F%2Fchris.beams.io%2Fposts%2Fgit-commit%2F" target="_blank">great commit messages</a>). With the automated versioning set up, you have everything you need in the commit - author, date, version, and message. Have the CI generate a changelog file for you and upload it somewhere with each new version.</p> <h2 id="summary">Summary</h2> <p>This is just a basic setup that can be tweaked for more specific cases. Hit me on Twitter if you have any questions or if you&rsquo;d like to share your own versioning process.</p> <p>See the full examples here:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgitlab.com%2Fthreedotslabs%2Fci-scripts%2Fblob%2Fmaster%2Fcommon%2Fgen-semver" target="_blank">gen-semver script</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgitlab.com%2Fthreedotslabs%2Fci-scripts%2Fblob%2Fmaster%2Fgolang%2Fbuild-semver" target="_blank">build-semver script</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgitlab.com%2Fthreedotslabs%2Fci-examples%2Fblob%2Fmaster%2F.gitlab-ci.yml" target="_blank">.gitlab-ci.yaml definition</a></li> </ul> <p>External links:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fsemver.org%2F" target="_blank">Semantic versioning</a></li> <li>Take a look at the <a href="proxy.php?url=https%3A%2F%2Fsemantic-release.gitbook.io%2F" target="_blank">semantic-release</a> tool to compare some ideas.</li> </ul>Introducing Watermill - Go event-driven applications libraryhttps://threedots.tech/post/introducing-watermill/Wed, 21 Nov 2018 00:00:00 +0100https://threedots.tech/post/introducing-watermill/<p>Watermill is a Go library for working efficiently with message streams. It is intended as a library for building event-driven applications, enabling event sourcing, CQRS, RPC over messages, sagas.</p> <h3 id="why">Why?</h3> <h4 id="lack-of-standard-messaging-library">Lack of standard messaging library</h4> <p>There are many third party and standard library tools which help to implement standardized RPC or HTTP communication in Golang. There are also multiple third party HTTP routers and frameworks.</p> <p>But when you want to build a message-driven application, there are no libraries which are infrastructure-agnostic and not opinionated.</p> <p>Watermill is a library which will work with any implementation of <em>Publisher</em> and <em>Subscriber</em> interfaces:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Publisher</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">Publish</span><span class="p">(</span><span class="nx">topic</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">messages</span> <span class="o">...*</span><span class="nx">Message</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"> <span class="nf">Close</span><span class="p">()</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Subscriber</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">Subscribe</span><span class="p">(</span><span class="nx">topic</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kd">chan</span> <span class="o">*</span><span class="nx">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nf">Close</span><span class="p">()</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>For now, Watermill comes with the following implementations:</p> <ul> <li><strong>Kafka Pub/Sub</strong></li> <li><strong>Go channel (intraprocess) Pub/Sub</strong></li> <li><strong>HTTP Subscriber</strong></li> </ul> <p>In close future, there are plans for <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F3" target="_blank">RabbitMQ (AMQP) Pub/Sub</a>, <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F4" target="_blank">Google Cloud PubSub</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F5" target="_blank">MySQL Binlog subscriber</a>.</p> <h4 id="high-entry-level">High entry level</h4> <p>When implementing a HTTP REST API nowadays, how do you go about it? Do you send it via TCP like this:</p> <p><strong><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fgolang%2Fgo%2Fblob%2Fbfd9b94069e74b0c6516a045cbb83bf1024a1269%2Fsrc%2Fnet%2Fhttp%2Frequest.go%23L516" target="_blank">Source: Golang code</a></strong></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">_</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">w</span><span class="p">.(</span><span class="nx">io</span><span class="p">.</span><span class="nx">ByteWriter</span><span class="p">);</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">bw</span> <span class="p">=</span> <span class="nx">bufio</span><span class="p">.</span><span class="nf">NewWriter</span><span class="p">(</span><span class="nx">w</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">w</span> <span class="p">=</span> <span class="nx">bw</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprintf</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;%s %s HTTP/1.1\r\n&#34;</span><span class="p">,</span> <span class="nf">valueOrDefault</span><span class="p">(</span><span class="nx">r</span><span class="p">.</span><span class="nx">Method</span><span class="p">,</span> <span class="s">&#34;GET&#34;</span><span class="p">),</span> <span class="nx">ruri</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Header lines </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Fprintf</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;Host: %s\r\n&#34;</span><span class="p">,</span> <span class="nx">host</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// [74 lines later] </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tw</span><span class="p">.</span><span class="nf">writeBody</span><span class="p">(</span><span class="nx">w</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// [a lot of crazy stuff in writeBody] </span></span></span></code></pre></div><p>or just by calling:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">helloHandler</span> <span class="o">:=</span> <span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">req</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">io</span><span class="p">.</span><span class="nf">WriteString</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;Hello, world!\n&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>For the same reason, you probably shouldn&rsquo;t use your PubSub&rsquo;s raw driver (for example for Kafka). Instead of this, you should use something that will deal with stuff like acknowledgment, errors handling and publishing created messages in a universal and consistent manner. It is also a lot easier for less experienced developers.</p> <p>Watermill works with high level handler functions, with abstraction level similar to Golang HTTP handlers:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">HandlerFunc</span> <span class="kd">func</span><span class="p">(</span><span class="nx">msg</span> <span class="o">*</span><span class="nx">Message</span><span class="p">)</span> <span class="p">([]</span><span class="o">*</span><span class="nx">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span></code></pre></div><p>All you need to do to process a message is to create a function with the <code>HandlerFunc</code> signature:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">process</span><span class="p">(</span><span class="nx">msg</span> <span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="p">([]</span><span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Println</span><span class="p">(</span><span class="s">&#34;received message: &#34;</span><span class="p">,</span> <span class="nb">string</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">Payload</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">producedMessages</span> <span class="o">:=</span> <span class="p">[]</span><span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">message</span><span class="p">.</span><span class="nf">NewMessage</span><span class="p">(</span><span class="nx">uuid</span><span class="p">.</span><span class="nf">NewV4</span><span class="p">().</span><span class="nf">String</span><span class="p">(),</span> <span class="p">[]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nf">Now</span><span class="p">().</span><span class="nf">String</span><span class="p">()),</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">producedMessages</span><span class="p">,</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h4 id="extensibility-and-elasticity">Extensibility and elasticity</h4> <p>Every organization and project is different. This is why Watermill is built so that every part that may differ between organizations can be configured.</p> <p>As said before, you can use any Pub/Sub or implement your own when needed. If you are using different message format, you can also inject your own message marshaller.</p> <p>Nowadays, any modern service must support <strong>metrics</strong> and <strong>distributed tracing</strong>. When building a message-driven application, often we need a <strong>posion queue</strong> or <strong>retrying</strong> support. Also, sometimes we need to limit the rate of message processing with <strong>throttling</strong>.</p> <p>Every one of these functionalities can be implemented by a <strong>middleware</strong>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">HandlerMiddleware</span> <span class="kd">func</span><span class="p">(</span><span class="nx">h</span> <span class="nx">HandlerFunc</span><span class="p">)</span> <span class="nx">HandlerFunc</span> </span></span></code></pre></div><p>Watermill is shipped with ready-to-use middlewares for these functionalities.</p> <p>Also, error handling can be different for some services. By default, we want to send an Ack when the processing was successful. But sometimes, we want to send an Ack just after receiving the message. It can be also done by using a middleware:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">InstantAck</span><span class="p">(</span><span class="nx">h</span> <span class="nx">message</span><span class="p">.</span><span class="nx">HandlerFunc</span><span class="p">)</span> <span class="nx">message</span><span class="p">.</span><span class="nx">HandlerFunc</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kd">func</span><span class="p">(</span><span class="nx">message</span> <span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">)</span> <span class="p">([]</span><span class="o">*</span><span class="nx">message</span><span class="p">.</span><span class="nx">Message</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">message</span><span class="p">.</span><span class="nf">Ack</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">h</span><span class="p">(</span><span class="nx">message</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h4 id="fast">Fast</h4> <p>Use of message-driven architecture and Golang is often dictated by high performance requirements. For that reason, Watermill can process hundreds of thousands of messages per second with low latency. But there&rsquo;s still a lot of work to be done in this field. Performance updates and benchmarks are on the way!</p> <h3 id="how-to-start">How to start?</h3> <p>In <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2F" target="_blank">https://watermill.io/</a> you can find detailed documentation of Watermill. I recommend to start with the <a href="proxy.php?url=https%3A%2F%2Fwatermill.io%2Fdocs%2Fgetting-started%2F" target="_blank">Getting Started</a> guide.</p> <h3 id="future-plans">Future plans</h3> <p>For now, Watermill is in beta phase. Currently, our number one priority is to create a good documentation.</p> <p>We have also plans to implement some more components basing on our Messages Router and Pub/Sub:</p> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F7" target="_blank">Sagas</a>,</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F6" target="_blank">CQRS</a> ,</li> <li><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F8" target="_blank">Event Sourcing</a> .</li> </ul> <p>As mentioned before, we are planning to implement <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F3" target="_blank">RabbitMQ (AMQP) Pub/Sub</a>, <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F4" target="_blank">Google Cloud PubSub</a> and <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues%2F5" target="_blank">MySQL Binlog subscriber</a>.</p> <p>If you need something that Watermill is missing, feel free to add <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fwatermill%2Fissues" target="_blank">a new issue</a>. If one of the previously mentioned features is important for you, please add a thumbs up in GitHub issue.</p>Keeping common scripts in GitLab CIhttps://threedots.tech/post/keeping-common-scripts-in-gitlab-ci/Sat, 03 Nov 2018 00:00:00 +0100https://threedots.tech/post/keeping-common-scripts-in-gitlab-ci/<p>With this post, I&rsquo;d like to start a series of CI-related tips, targeted mostly at GitLab, since that&rsquo;s my go-to tool for things CI/CD-related. I&rsquo;m sure most of them could be easily applied to other CI systems, though.</p> <p>While GitLab does a great job at many things (repository hosting and related stuff, like MRs, pipelines, issue boards, etc.), it sometimes lacks more specialized features in some areas. With heavy usage of pipelines in my projects, I often had to look for workarounds and smart tricks. So I&rsquo;d like to share some of them now, based on my experiences.</p> <h2 id="the-issue">The issue</h2> <p>An example CI definition could look something like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">stages</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">test</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">build</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">deploy</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">test</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">golang:1.11</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go test ./...</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go vet ./...</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">build</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">golang:1.11</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">build</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go build -o ./bin/my-app ./cmd/my-app</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">artifacts</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">paths</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">bin/</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">deploy</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">deploy</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">...</span><span class="w"> </span><span class="c"># upload bin/ somewhere</span><span class="w"> </span></span></span></code></pre></div><p>I have some issues with using bare bash commands like this. See what happens when you need to change something in one of your jobs:</p> <ol> <li>You need to push a new commit in the application repository, <strong>triggering full pipeline</strong> for the project.</li> <li>Your changes <strong>won&rsquo;t propagate to other branches</strong>, unless your commit from <code>master</code> is merged by other developers.</li> <li>Your changes will affect <strong>only this one repository</strong>. If you maintain more similar projects, you need to push the changes to all of them.</li> </ol> <h2 id="common-scripts-repository">Common scripts repository</h2> <p>One of the things I missed when starting out with GitLab pipelines was a place for common scripts, which could be used by multiple repositories. At that time <a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fci%2Fyaml%2F%23include" target="_blank">include</a> was not yet available, but even now it hardly solves the issue for me.</p> <p>Reusing parts of the YAML definition is one thing, but the other is reusing the standard scripts across all of your projects. I&rsquo;d prefer things like &ldquo;build app&rdquo;, &ldquo;package app&rdquo;, &ldquo;deploy app&rdquo; to be one-liners in the pipeline.</p> <p>I also feel that this fits nicely into the DevOps mindset: ops prepare the scripts, which are easy to use by developers. The scripts are developed using the same good practices as the application code itself: with code reviews and their own pipeline including unit tests and static checks.</p> <p>I usually end up with a mix of bash and Python scripts. Bash works well for simple scripts, but for more complex stuff, going with Python (or other scripting language you like) should save you some future headaches. Python is usually easier to read by other people too, but keep in mind it may not be included in every docker image you&rsquo;ll be using in your jobs.</p> <h3 id="implementation">Implementation</h3> <p>The idea is pretty simple: fetch the scripts repository every time a job is run. This should be as simple as adding global settings in your <code>.gitlab-ci.yml</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">variables</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">SCRIPTS_REPO</span><span class="p">:</span><span class="w"> </span><span class="l">https://gitlab.com/threedotslabs/ci-scripts</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">before_script</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">export SCRIPTS_DIR=$(mktemp -d)</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">git clone -q --depth 1 &#34;$SCRIPTS_REPO&#34; &#34;$SCRIPTS_DIR&#34;</span><span class="w"> </span></span></span></code></pre></div><p>You could also move the variables to the project&rsquo;s CI/CD variables. Or <code>include</code> this config from another location. Or use your own docker images with the scripts repository baked in and run <code>git pull</code> in it. The basic idea stays the same.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Passing <code>--depth 1</code> option results in a <em>shallow clone</em> of the repository, i.e. truncated to the most recent commit from the <code>master</code> branch. Thanks to this, the overhead for each job is minimal. <a href="proxy.php?url=https%3A%2F%2Fgit-scm.com%2Fdocs%2Fgit-clone%23git-clone---depthltdepthgt" target="_blank">Learn more in docs.</a> </p></div> </div> <h3 id="example-script">Example script</h3> <p>Let&rsquo;s say you have a bunch of Golang applications and you&rsquo;d prefer to have a consistent build process for all of them.</p> <p>Why would you want it, when a simple <code>go build</code> should be enough? For example, you might want to bake the version number into every one of your services: it&rsquo;s usually useful to know which version you&rsquo;re running! Putting that into a common script ensures that none of your applications will skip this step.</p> <p>In the next post, I&rsquo;ll show you how to generate <a href="proxy.php?url=https%3A%2F%2Fsemver.org%2F" target="_blank">semver</a> versions for commits. For now, we&rsquo;ll stick with git commit hash as our version number.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash </span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="c1"># Build generic golang application.</span> </span></span><span class="line"><span class="cl"><span class="c1">#</span> </span></span><span class="line"><span class="cl"><span class="c1"># Example:</span> </span></span><span class="line"><span class="cl"><span class="c1"># build-go cmd/server example-server pkg.version.Version</span> </span></span><span class="line"><span class="cl"><span class="nb">set</span> -e </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$#</span><span class="s2">&#34;</span> -ne <span class="m">3</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> </span></span><span class="line"><span class="cl"> <span class="nb">echo</span> <span class="s2">&#34;Usage: </span><span class="nv">$0</span><span class="s2"> &lt;package&gt; &lt;target_binary&gt; &lt;version_var&gt;&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nb">exit</span> <span class="m">1</span> </span></span><span class="line"><span class="cl"><span class="k">fi</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">readonly</span> <span class="nv">package</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"><span class="nb">readonly</span> <span class="nv">target_binary</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"><span class="nb">readonly</span> <span class="nv">version_var</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$3</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">readonly</span> <span class="nv">bin_dir</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$CI_PROJECT_DIR</span><span class="s2">/bin/&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="nv">$bin_dir</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl">go build -ldflags<span class="o">=</span><span class="s2">&#34;-X </span><span class="nv">$version_var</span><span class="s2">=</span><span class="nv">$CI_COMMIT_SHA</span><span class="s2">&#34;</span> -o <span class="s2">&#34;</span><span class="nv">$bin_dir</span><span class="s2">/</span><span class="nv">$target_binary</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$package</span><span class="s2">&#34;</span> </span></span></code></pre></div><div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Don&rsquo;t forget the <code>set -e</code> part or your bash scripts may silently fail and cause some hard to find bugs. </p></div> </div> <p>This script expects that the resulting binary will be kept in the <code>bin</code> directory. It&rsquo;s just another thing you might want to have consistent across repositories.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Note that I used <code>CI_</code> variables <a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fci%2Fvariables%2F" target="_blank">provided by GitLab</a>. While they make the scripts really simple to run, it comes with the cost of more complicated testing, since you need to set up some environment variables before running scripts locally. If you prefer more explicit approach, just pass those variables as arguments to your script. </p></div> </div> <p>This script should be put in your <a href="proxy.php?url=https%3A%2F%2Fgitlab.com%2Fthreedotslabs%2Fci-scripts" target="_blank">scripts repository</a>. Don&rsquo;t forget to set the execute permissions (<code>chmod +x</code>).</p> <p>The last thing you need to do is use the script in your <a href="proxy.php?url=https%3A%2F%2Fgitlab.com%2Fthreedotslabs%2Fci-examples" target="_blank">application repository</a>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">build</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">golang:1.11</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">build</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">script</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">$SCRIPTS_DIR/golang/build . example-server main.Version</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">artifacts</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">paths</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">bin/</span><span class="w"> </span></span></span></code></pre></div><p>And this is it! Your scripts should now always be up-to-date across your repositories. Of course this setup won&rsquo;t make much sense if you&rsquo;re using just one repository, so keep that in mind. Don&rsquo;t overcomplicate it, if you can.</p> <h2 id="authentication">Authentication</h2> <p>If you&rsquo;re using a private repository for your scripts, you&rsquo;ll need to add some form of authentication to clone it. You can do this with either <a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fuser%2Fproject%2Fdeploy_tokens%2F" target="_blank">Deploy tokens</a> or <a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fci%2Fssh_keys%2F" target="_blank">SSH deploy keys</a>.</p> <p>Using deploy tokens is a bit easier, so let&rsquo;s see it in action.</p> <ol> <li>In the scripts repository: generate new deploy token. You&rsquo;ll find it in the <code>Settings -&gt; Repository -&gt; Deploy tokens</code> section.</li> <li>In the application repository: add the generated user and token as new variables called <code>SCRIPTS_USER</code> and <code>SCRIPTS_TOKEN</code>. You can do this in the <code>Settings -&gt; CI / CD -&gt; Variables</code> section.</li> <li>Modify the <code>SCRIPTS_REPO</code> variable in your application CI definition:</li> </ol> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">variables</span><span class="p">:</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">SCRIPTS_REPO</span><span class="p">:</span><span class="w"> </span><span class="l">https://$SCRIPTS_USER:[email protected]/threedotslabs/ci-scripts</span><span class="w"> </span></span></span></code></pre></div><div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Managing secrets across multiple projects is easier if you add variables at the group level. </p></div> </div> <h2 id="testing-the-changes">Testing the changes</h2> <p>If you&rsquo;ve worked with multiple repositories before, you should already know that it adds some synchronization overhead. For example, pushing a broken script to the <code>master</code> branch could break builds in all your projects. That&rsquo;s why code reviews and unit tests are very much welcome.</p> <p>If unit testing the scripts is hard or just not possible, you could try testing them on branches first, before merging to <code>master</code>. You would just need to checkout to the chosen branch after cloning. What&rsquo;s cool, you can retry the failed job this way just after pushing changes to the script, without running the complete pipeline again.</p> <h2 id="what-about-makefiles">What about Makefiles?</h2> <p>Moving all bash commands to <code>Makefile</code> can tidy up your CI definition and allows developers to run the same checks and tests that will be run in the pipeline. For example, you could introduce <code>make test</code> target that will run unit tests, <code>make lint</code> for static checks and so on.</p> <p>Sadly, this will mean duplicating the <code>Makefile</code> across all your repositories and it doesn&rsquo;t really solve the issues mentioned above. Makefiles also have their own quirks, so better make sure you know what you&rsquo;re doing or you might run into unexpected issues (e.g. silently passing on errors).</p> <h2 id="wrapping-up">Wrapping up</h2> <p>While the script we&rsquo;ve used is really trivial, this shows how this integration can be used for keeping all CI-related scripts in one place. I&rsquo;ll show more advanced examples in the next posts, so stay tuned!</p> <p>If you have any questions or you&rsquo;d like to share other ideas for similar workflow, hit me on Twitter.</p> <h3 id="helpful-resources">Helpful resources</h3> <ul> <li><a href="proxy.php?url=https%3A%2F%2Fgitlab.com%2Fthreedotslabs%2Fci-scripts" target="_blank">example scripts repository</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fgitlab.com%2Fthreedotslabs%2Fci-examples" target="_blank">example application repository</a></li> <li><a href="proxy.php?url=https%3A%2F%2Fabout.gitlab.com%2F2017%2F11%2F27%2Fgo-tools-and-gitlab-how-to-do-continuous-integration-like-a-boss%2F" target="_blank">Go tools and GitLab: How to do continuous integration like a boss </a></li> <li><a href="proxy.php?url=https%3A%2F%2Fdocs.gitlab.com%2Fee%2Fci%2Fyaml" target="_blank">Configuration of your jobs with .gitlab-ci.yml</a></li> </ul>When using Microservices or Modular Monolith in Go can be just a detail?https://threedots.tech/post/microservices-or-monolith-its-detail/Wed, 28 Feb 2018 00:00:00 +0100https://threedots.tech/post/microservices-or-monolith-its-detail/<p>Nowadays we can often hear that monolithic architecture is obsolete and responsible for all evil in IT. We often hear that microservices architecture is a silver bullet which helps to kill all this monolithic evil. <strong>But you probably know that there are almost no silver bullets in IT and every decision entails trade-offs.</strong></p> <p>One of the most favored advantages of microservices architecture is good modules separation. You can deploy every service independently and services are easier to scale. Also, every team can have their own repository and use the technology of their choice. We can easily rewrite the entire service very fast. In return, we get possible network problems, latency, limited bandwidth. You must deal with potential bugs in the communication layer. It is much harder to debug and maintain from the developer and ops perspective. We must keep services API’s contracts up to date and fight with compatibility issues. We also can’t forget about potential performance drawbacks. The overall complexity is greater in many areas, starting with developing and ending in administration.</p> <p>In summary, regardless of all advantages, it is hard to build <strong>good</strong> microservices architecture. If you don’t trust my opinion, you can trust <a href="proxy.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FMartin_Fowler" target="_blank">this guy</a>:</p> <blockquote> <p>Almost all the cases where I&rsquo;ve heard of a system that was built as a microservice system from scratch, it has ended up in serious trouble.</p> <footer> <strong>Martin Fowler</strong> <cite> <a href="proxy.php?url=https%3A%2F%2Fmartinfowler.com%2Fbliki%2FMonolithFirst.html" title="https://martinfowler.com/bliki/MonolithFirst.html" target="_blank">martinfowler.com</a> </cite> </footer> </blockquote> <blockquote> <p>Almost all the successful microservice stories have started with a monolith that got too big and was broken up.</p> <footer> <strong>Martin Fowler</strong> <cite> <a href="proxy.php?url=https%3A%2F%2Fmartinfowler.com%2Fbliki%2FMonolithFirst.html" title="https://martinfowler.com/bliki/MonolithFirst.html" target="_blank">martinfowler.com</a> </cite> </footer> </blockquote> <p>Let’s assume a crazy idea, which will not follow actual trends - we are developing the new application and we want to start with a monolith. Does it mean that we must live with all monolith disadvantages?</p> <p>Not at all. The truth is that a lot of these problems are usually not because of the monolith architecture, but because of lack of good programming practices and good design.</p> <p>Let’s look how an example good designed monolith application will look, compared to the microservices architecture version. In this article, we will name it <strong>Clean Monolith</strong>. Both are built with Clean Architecture rules.</p> <h2 id="clean-architecture">Clean architecture</h2> <p>In a nutshell, Clean architecture assumes that your service should be separated into 4 layers:</p> <ul> <li>Domain - our domain logic and entities</li> <li>Application - some kind of glue for domain layer. For example, get the entity from the repository, call some method on this entity, save the entity in the repository. Also, it should take responsibility for all cross-cutting concerns like logging, transactions, instrumentation etc. It is also responsible for providing views (read models).</li> <li>Interfaces - thin layer which allows us to use the application, for example REST API, CLI Interface, Queue, etc.</li> <li>Infrastructure - database adapters, rest clients, in general implementation of interfaces from Domain/Application layer.</li> </ul> <img title="" loading="lazy" decoding="async" class="img img-center" width="694" height="510" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-or-monolith%2Fcleanarch_hucdab799abf1ee5a850f8f3789a6d7d64_79597_694x510_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-or-monolith%5C%2Fcleanarch_hucdab799abf1ee5a850f8f3789a6d7d64_79597_694x510_resize_lanczos_3.png"" /> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> Clean Architecture is a very similar concept to Hexagonal/Ports and adapters architecture/Onion Architecture of which you could already hear about. </p></div> </div> <p>The key concept of clean architecture is that any layer cannot know anything about outside layer. For example, the domain should not be aware of application or infrastructure layer (so it doesn’t know that domain entities are persisted in MySQL for example). Also, the application layer should not be aware of how it is called (REST API, CLI, Queue listener, whatever).</p> <p>But how to achieve it? The answer is Inversion of Control. In short: hide implementation behind interfaces.</p> <p>Uncle Bob has already described it pretty good. At the bottom of the article I will provide a link with a full explanation of Clean Architecture.</p> <h2 id="monolith-vs-microservices">Monolith vs Microservices</h2> <p>For the purposes of the article let&rsquo;s use an example of a simple shop. This shop will allow us to: list products, place order, initialize payment in remote payments provider, receive notifications about payments and mark order as paid. Let&rsquo;s take a look at the architecture diagram.</p> <p>I&rsquo;ve created diagrams for both microservices and monolith example. Both are made in the spirit of Clean Architecture.</p> <p><em>(click to see in full resolution)</em></p> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-or-monolith%2Fmicroservices.png" style="display: block;" class="glightbox"> <img title="" loading="lazy" decoding="async" class="img img-center" width="1608" height="1803" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-or-monolith%2Fmicroservices_huc05df840d52449a7a4665a04cc794b13_373395_1608x1803_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-or-monolith%5C%2Fmicroservices_huc05df840d52449a7a4665a04cc794b13_373395_1608x1803_resize_lanczos_3.png"" /> </a> <p><em>Big picture microservices architecture diagram</em>.</p> <p>Typically monolith architecture will look like <strong>Big Ball of Mud</strong>:</p> <img title="" loading="lazy" decoding="async" class="img img-center" width="509" height="486" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-or-monolith%2Fbig-ball-of-mud_hu35e4e5d60072e5d23de388eb818f1466_134120_509x486_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-or-monolith%5C%2Fbig-ball-of-mud_hu35e4e5d60072e5d23de388eb818f1466_134120_509x486_resize_lanczos_3.png"" /> <p><em>Big Ball of Mud</em>.</p> <p>But when we design architecture with the spirit of Clean Architecture it will look like this:</p> <p><em>(click to see in full resolution)</em></p> <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-or-monolith%2Fmonolith.png" style="display: block;" class="glightbox"> <img title="" loading="lazy" decoding="async" class="img img-center" width="1608" height="1803" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-or-monolith%2Fmonolith_huc05df840d52449a7a4665a04cc794b13_377979_1608x1803_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-or-monolith%5C%2Fmonolith_huc05df840d52449a7a4665a04cc794b13_377979_1608x1803_resize_lanczos_3.png"" /> </a> <p><em>Big picture monolithic architecture diagram</em>.</p> <p>Do you see differences in this example? If you look closely, you can see that there are only in interfaces/infrastructure layers. The domain and application layers are basically the same (which is expected when we use DDD). I marked differences with orange arrows to make it more visible ;)</p> <p>Microservices can avoid it because we have a natural barrier between services (for example because of separated repositories), so we have enforced modules separation by design. But we can achieve some separation with monolith application and Clean Architecture helps here a lot, because we are limited to communicate only between infrastructure and interfaces layer. For example, we are not allowed to use anything from other bounded context&rsquo;s domain/application/infrastructure in our bounded context.</p> <p><strong>It is extremely important to not void this rule. The best way to enforce it is to have a tool which can validate it and plug it into the CI.</strong></p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>I’ve developed a tool, which can do it for Golang: <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Froblaszczak%2Fgo-cleanarch" target="_blank">go-cleanarch</a>.</p> <p>I’ve found a similar tool for PHP some time ago, but I cannot find it. There is a chance that tools like it exist for other languages. If you know any tool like this, please send it to me on Twitter: <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Froblaszczak" target="_blank">@roblaszczak</a> and I&rsquo;ll put it here :)</p> </p></div> </div> <p>Unfortunately, the fact that we have modules doesn’t mean that our architecture is good. Good modules separation is critical for proper working monolith and microservices. To make it good we should check the concept of <strong>Bounded Contexts (at the bottom of the article). Another great tool which will show (physically!) where your Bounded Contexts are and show you how your domain is working is Event Storming.</strong></p> <img title="" loading="lazy" decoding="async" class="img img-center" width="900" height="310" src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fmedia%2Fmicroservices-or-monolith%2Fstorming_hue472c784f896f464215c02a04dfcf03e_397383_900x310_resize_q80_h2_lanczos_3.webp" alt="" onerror="this.onerror='null';this.src="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F%5C%2Fmedia%5C%2Fmicroservices-or-monolith%5C%2Fstorming_hue472c784f896f464215c02a04dfcf03e_397383_900x310_resize_lanczos_3.png"" /> <p>There are a lot of people who say that persistence is just an <a href="proxy.php?url=https%3A%2F%2F8thlight.com%2Fblog%2Funcle-bob%2F2012%2F08%2F13%2Fthe-clean-architecture.html" target="_blank">implementation detail</a> (<em>Frameworks and Drivers</em> part). They also say that persistence layer should be implemented so as to replace driver without any impact on any other layer than infrastructure.</p> <p><strong>Let’s look at the big picture diagram and go one step further: let’s say that whether the application is a microservice or a monolith is an implementation detail.</strong></p> <p>If it is true, we can start developing our application as Clean Monolith and when good moment comes, migrate it to microservices without much work and without touching other layers than interfaces/infrastructure. It is a similar approach to starting implementation of database driver in filesystem or memory, to defer the decision about choice of the database as long as we can.</p> <p>Of course, it may still require some optimizations while migrating to microservices (or in the opposite direction, because why not?) it will be pretty easy operation, compared to classical monolith refactoring (or even worse, reimplementing).</p> <p>I want not to be groundless. Here are some snippets from the source code of the application presented on the schema.</p> <h2 id="show-me-the-code">Show me the code</h2> <p>In this example we will follow simple shop flow which includes: placing the order, initializing payment and simulated asynchronous payment receiving.</p> <p>Placing the order should be made synchronous, initializing and receiving payments is async.</p> <h3 id="placing-the-order">Placing the order</h3> <p>In interfaces it&rsquo;s nothing special. Just parsing HTTP request and executing a command in the application layer.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>In the top of every snippet you can check where it is located in the repository.</p> <p>You can also read bounded context and layer from it. For example <code>pkg/orders/interfaces/public/http/something.go</code> is <code>interfaces</code> layer of <code>orders</code> bounded context.</p> </p></div> </div> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/orders/interfaces/public/http/orders.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">o</span> <span class="nx">ordersResource</span><span class="p">)</span> <span class="nf">Post</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">req</span> <span class="o">:=</span> <span class="nx">PostOrderRequest</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">render</span><span class="p">.</span><span class="nf">Decode</span><span class="p">(</span><span class="nx">r</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">req</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">_</span> <span class="p">=</span> <span class="nx">render</span><span class="p">.</span><span class="nf">Render</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="nx">common_http</span><span class="p">.</span><span class="nf">ErrBadRequest</span><span class="p">(</span><span class="nx">err</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span> <span class="o">:=</span> <span class="nx">application</span><span class="p">.</span><span class="nx">PlaceOrderCommand</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">OrderID</span><span class="p">:</span> <span class="nx">orders</span><span class="p">.</span><span class="nf">ID</span><span class="p">(</span><span class="nx">uuid</span><span class="p">.</span><span class="nf">NewV1</span><span class="p">().</span><span class="nf">String</span><span class="p">()),</span> </span></span><span class="line"><span class="cl"> <span class="nx">ProductID</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">ProductID</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Address</span><span class="p">:</span> <span class="nx">application</span><span class="p">.</span><span class="nf">PlaceOrderCommandAddress</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">Address</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">o</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nf">PlaceOrder</span><span class="p">(</span><span class="nx">cmd</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">_</span> <span class="p">=</span> <span class="nx">render</span><span class="p">.</span><span class="nf">Render</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="nx">common_http</span><span class="p">.</span><span class="nf">ErrInternal</span><span class="p">(</span><span class="nx">err</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">w</span><span class="p">.</span><span class="nf">WriteHeader</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">StatusOK</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nx">render</span><span class="p">.</span><span class="nf">JSON</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="nx">PostOrdersResponse</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">OrderID</span><span class="p">:</span> <span class="nb">string</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">OrderID</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>For people not familiar with Golang: in Go you don&rsquo;t need to explicitly say that you are implementing an interface. You just need to implement interface&rsquo;s methods.</p> <p>A good example is <code>io.Reader</code> interface.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Reader</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">Read</span><span class="p">(</span><span class="nx">p</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="nx">n</span> <span class="kt">int</span><span class="p">,</span> <span class="nx">err</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>for example:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// no Foo implements io.Reader needed </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kd">type</span> <span class="nx">Foo</span> <span class="kd">struct</span> <span class="p">{}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">f</span> <span class="nx">Foo</span><span class="p">)</span> <span class="nf">Read</span><span class="p">(</span><span class="nx">b</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">)</span> <span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">SomeFunc</span><span class="p">(</span><span class="nx">r</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Reader</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">buf</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="nx">r</span><span class="p">.</span><span class="nf">Read</span><span class="p">(</span><span class="nx">buf</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="nf">SomeFunc</span><span class="p">(</span><span class="nx">Foo</span><span class="p">{})</span> </span></span></code></pre></div> </p></div> </div> <p>It&rsquo;s more interesting in application service in <code>pkg/orders/application/orders.go</code>:</p> <p>First of all, interfaces of Products Service (to get product data from <code>Shop bounded context</code>) and Payments Service (to initialize payment in <code>Payments bounded context</code>):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/orders/application/orders.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">productsService</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">ProductByID</span><span class="p">(</span><span class="nx">id</span> <span class="nx">orders</span><span class="p">.</span><span class="nx">ProductID</span><span class="p">)</span> <span class="p">(</span><span class="nx">orders</span><span class="p">.</span><span class="nx">Product</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">paymentsService</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">InitializeOrderPayment</span><span class="p">(</span><span class="nx">id</span> <span class="nx">orders</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> <span class="nx">price</span> <span class="nx">price</span><span class="p">.</span><span class="nx">Price</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>And finally the Application service. This is exactly the same for monolith and microservices. We just inject different <code>productsService</code> and <code>paymentsService</code> implementations.</p> <p>We also use domain objects here and repository to persist <code>Order</code> in the database (in our case we use memory implementation, but it&rsquo;s also a detail and can be changed to any storage).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/orders/application/orders.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">OrdersService</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">productsService</span> <span class="nx">productsService</span> </span></span><span class="line"><span class="cl"> <span class="nx">paymentsService</span> <span class="nx">paymentsService</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">ordersRepository</span> <span class="nx">orders</span><span class="p">.</span><span class="nx">Repository</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="nx">OrdersService</span><span class="p">)</span> <span class="nf">PlaceOrder</span><span class="p">(</span><span class="nx">cmd</span> <span class="nx">PlaceOrderCommand</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">address</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">orders</span><span class="p">.</span><span class="nf">NewAddress</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Address</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Address</span><span class="p">.</span><span class="nx">Street</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Address</span><span class="p">.</span><span class="nx">City</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Address</span><span class="p">.</span><span class="nx">PostCode</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">cmd</span><span class="p">.</span><span class="nx">Address</span><span class="p">.</span><span class="nx">Country</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;invalid address&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">product</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">s</span><span class="p">.</span><span class="nx">productsService</span><span class="p">.</span><span class="nf">ProductByID</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">ProductID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;cannot get product&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">newOrder</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">orders</span><span class="p">.</span><span class="nf">NewOrder</span><span class="p">(</span><span class="nx">cmd</span><span class="p">.</span><span class="nx">OrderID</span><span class="p">,</span> <span class="nx">product</span><span class="p">,</span> <span class="nx">address</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;cannot create order&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">s</span><span class="p">.</span><span class="nx">ordersRepository</span><span class="p">.</span><span class="nf">Save</span><span class="p">(</span><span class="nx">newOrder</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;cannot save order&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">s</span><span class="p">.</span><span class="nx">paymentsService</span><span class="p">.</span><span class="nf">InitializeOrderPayment</span><span class="p">(</span><span class="nx">newOrder</span><span class="p">.</span><span class="nf">ID</span><span class="p">(),</span> <span class="nx">newOrder</span><span class="p">.</span><span class="nf">Product</span><span class="p">().</span><span class="nf">Price</span><span class="p">());</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;cannot initialize payment&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;order %s placed&#34;</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nx">OrderID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h3 id="productsservice-implementation"><code>productsService</code> implementation</h3> <h4 id="microservice">Microservice</h4> <p>In microservices version we use HTTP (REST) interface to get product info. I&rsquo;ve separated REST API&rsquo;s to private (internal) and public (accessed by frontend, for example).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/orders/infrastructure/shop/http.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">http_interface</span> <span class="s">&#34;github.com/ThreeDotsLabs/monolith-shop/pkg/shop/interfaces/private/http&#34;</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">h</span> <span class="nx">HTTPClient</span><span class="p">)</span> <span class="nf">ProductByID</span><span class="p">(</span><span class="nx">id</span> <span class="nx">orders</span><span class="p">.</span><span class="nx">ProductID</span><span class="p">)</span> <span class="p">(</span><span class="nx">orders</span><span class="p">.</span><span class="nx">Product</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">resp</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">http</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="nx">fmt</span><span class="p">.</span><span class="nf">Sprintf</span><span class="p">(</span><span class="s">&#34;%s/products/%s&#34;</span><span class="p">,</span> <span class="nx">h</span><span class="p">.</span><span class="nx">address</span><span class="p">,</span> <span class="nx">id</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">orders</span><span class="p">.</span><span class="nx">Product</span><span class="p">{},</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;request to shop failed&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">productView</span> <span class="o">:=</span> <span class="nx">http_interface</span><span class="p">.</span><span class="nx">ProductView</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Unmarshal</span><span class="p">(</span><span class="nx">b</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">productView</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">orders</span><span class="p">.</span><span class="nx">Product</span><span class="p">{},</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrapf</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;cannot decode response: %s&#34;</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">OrderProductFromHTTP</span><span class="p">(</span><span class="nx">productView</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>The REST endpoint in Shop bounded context looks like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/shop/interfaces/private/http/products.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">productsResource</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">repo</span> <span class="nx">products_domain</span><span class="p">.</span><span class="nx">Repository</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="nx">productsResource</span><span class="p">)</span> <span class="nf">Get</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">product</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">p</span><span class="p">.</span><span class="nx">repo</span><span class="p">.</span><span class="nf">ByID</span><span class="p">(</span><span class="nx">products_domain</span><span class="p">.</span><span class="nf">ID</span><span class="p">(</span><span class="nx">chi</span><span class="p">.</span><span class="nf">URLParam</span><span class="p">(</span><span class="nx">r</span><span class="p">,</span> <span class="s">&#34;id&#34;</span><span class="p">)))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">_</span> <span class="p">=</span> <span class="nx">render</span><span class="p">.</span><span class="nf">Render</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="nx">common_http</span><span class="p">.</span><span class="nf">ErrInternal</span><span class="p">(</span><span class="nx">err</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">render</span><span class="p">.</span><span class="nf">Respond</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="nx">ProductView</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nb">string</span><span class="p">(</span><span class="nx">product</span><span class="p">.</span><span class="nf">ID</span><span class="p">()),</span> </span></span><span class="line"><span class="cl"> <span class="nx">product</span><span class="p">.</span><span class="nf">Name</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="nx">product</span><span class="p">.</span><span class="nf">Description</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="nf">priceViewFromPrice</span><span class="p">(</span><span class="nx">product</span><span class="p">.</span><span class="nf">Price</span><span class="p">()),</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>We also have a simple type which is used in HTTP response. In theory we can make Domain type serializable to JSON, but if we will do it every domain change will change our API contract, and every request for API contract change will change the domain. Doesn&rsquo;t sound good and doesn&rsquo;t have much in common with DDD and Clean Architecture.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/shop/interfaces/private/http/products.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">ProductView</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">string</span> <span class="s">`json:&#34;id&#34;`</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> <span class="s">`json:&#34;name&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Description</span> <span class="kt">string</span> <span class="s">`json:&#34;description&#34;`</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">Price</span> <span class="nx">PriceView</span> <span class="s">`json:&#34;price&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">PriceView</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Cents</span> <span class="kt">uint</span> <span class="s">`json:&#34;cents&#34;`</span> </span></span><span class="line"><span class="cl"> <span class="nx">Currency</span> <span class="kt">string</span> <span class="s">`json:&#34;currency&#34;`</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>You can notice that <code>ProductView</code> is imported in <code>pkg/orders/infrastructure/shop/http.go</code> (example above), because, as I said before, <strong>imports from interfaces to infrastructure between bounded contexts are totally fine</strong>.</p> <h4 id="monolith">Monolith</h4> <p>In monolith version it&rsquo;s pretty simple: in Orders bounded context we just call the function from Shop bounded context (<code>intraprocess.ProductInterface:ProductByID</code>) instead of calling REST API.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/orders/infrastructure/shop/intraprocess.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/ThreeDotsLabs/monolith-shop/pkg/orders/domain/orders&#34;</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;github.com/ThreeDotsLabs/monolith-shop/pkg/shop/interfaces/private/intraprocess&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">IntraprocessService</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">intraprocessInterface</span> <span class="nx">intraprocess</span><span class="p">.</span><span class="nx">ProductInterface</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewIntraprocessService</span><span class="p">(</span><span class="nx">intraprocessInterface</span> <span class="nx">intraprocess</span><span class="p">.</span><span class="nx">ProductInterface</span><span class="p">)</span> <span class="nx">IntraprocessService</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">IntraprocessService</span><span class="p">{</span><span class="nx">intraprocessInterface</span><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">i</span> <span class="nx">IntraprocessService</span><span class="p">)</span> <span class="nf">ProductByID</span><span class="p">(</span><span class="nx">id</span> <span class="nx">orders</span><span class="p">.</span><span class="nx">ProductID</span><span class="p">)</span> <span class="p">(</span><span class="nx">orders</span><span class="p">.</span><span class="nx">Product</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">shopProduct</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">i</span><span class="p">.</span><span class="nx">intraprocessInterface</span><span class="p">.</span><span class="nf">ProductByID</span><span class="p">(</span><span class="nb">string</span><span class="p">(</span><span class="nx">id</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">orders</span><span class="p">.</span><span class="nx">Product</span><span class="p">{},</span> <span class="nx">err</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">OrderProductFromIntraprocess</span><span class="p">(</span><span class="nx">shopProduct</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>And in Shop bounded context:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/shop/interfaces/private/intraprocess/products.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">ProductInterface</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">repo</span> <span class="nx">products</span><span class="p">.</span><span class="nx">Repository</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">i</span> <span class="nx">ProductInterface</span><span class="p">)</span> <span class="nf">ProductByID</span><span class="p">(</span><span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">Product</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">domainProduct</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">i</span><span class="p">.</span><span class="nx">repo</span><span class="p">.</span><span class="nf">ByID</span><span class="p">(</span><span class="nx">products</span><span class="p">.</span><span class="nf">ID</span><span class="p">(</span><span class="nx">id</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">Product</span><span class="p">{},</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;cannot get product&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nf">ProductFromDomainProduct</span><span class="p">(</span><span class="o">*</span><span class="nx">domainProduct</span><span class="p">),</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>You can notice that in Orders bounded context we don&rsquo;t import anything outside of Shops bounded context (as Clean Architecture assumes). So, we need some kind of <em>transport type</em> which can be imported in Shops BC.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">Product</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">Description</span> <span class="kt">string</span> </span></span><span class="line"><span class="cl"> <span class="nx">Price</span> <span class="nx">price</span><span class="p">.</span><span class="nx">Price</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>It can look redundant and duplicated, but in practice it helps with keeping constant contract between bounded contexts. For example we can totally replace application and domain layer and don&rsquo;t touch this type. You need to keep in mind that <strong>cost of avoiding duplication increases with scale</strong>. Also, duplication of data is not the same as duplication of behaviour.</p> <p>Do you see some analogy to <code>ProductView</code> in microservices version?</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>Looking back, &ldquo;ipc&rdquo; wasn&rsquo;t the best name for this pattern. It&rsquo;s too easy to confuse with Unix IPC (pipes, shared memory, message queues).</p> <p>Today we&rsquo;d call this pattern &ldquo;module contracts.&rdquo; The <code>intraprocess</code> package would become something like <code>shop/api/module/client</code>, where the shop module defines its contract.</p> <p><strong>If you want to practice this pattern hands-on, check out our <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2Fgo-backend%2F" target="_blank">Go Backend Masterclass</a>.</strong> You build a real Go backend from scratch, including module contracts, HTTP/gRPC APIs, repositories, and testing.</p> </p></div> </div> <h3 id="initializing-payment">Initializing payment</h3> <p>In the previous example we replaced HTTP Call with a Function call, which was synchronous. But how to deal with asynchronous operations? It depends. In Go it&rsquo;s easy because of concurrency primitives. If it&rsquo;s hard to achieve in your language you can just use Rabbit in the monolith.</p> <p>Like in the previous example, both versions look the same in application and domain layer.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/orders/application/orders.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">paymentsService</span> <span class="kd">interface</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nf">InitializeOrderPayment</span><span class="p">(</span><span class="nx">id</span> <span class="nx">orders</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> <span class="nx">price</span> <span class="nx">price</span><span class="p">.</span><span class="nx">Price</span><span class="p">)</span> <span class="kt">error</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="nx">OrdersService</span><span class="p">)</span> <span class="nf">PlaceOrder</span><span class="p">(</span><span class="nx">cmd</span> <span class="nx">PlaceOrderCommand</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// .. </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">s</span><span class="p">.</span><span class="nx">paymentsService</span><span class="p">.</span><span class="nf">InitializeOrderPayment</span><span class="p">(</span><span class="nx">newOrder</span><span class="p">.</span><span class="nf">ID</span><span class="p">(),</span> <span class="nx">newOrder</span><span class="p">.</span><span class="nf">Product</span><span class="p">().</span><span class="nf">Price</span><span class="p">());</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;cannot initialize payment&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// .. </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> </span></span></code></pre></div><h4 id="microservices">Microservices</h4> <p>In microservices we are using RabbitMQ to send message:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/orders/infrastructure/payments/amqp.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">i</span> <span class="nx">AMQPService</span><span class="p">)</span> <span class="nf">InitializeOrderPayment</span><span class="p">(</span><span class="nx">id</span> <span class="nx">orders</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> <span class="nx">price</span> <span class="nx">price</span><span class="p">.</span><span class="nx">Price</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">order</span> <span class="o">:=</span> <span class="nx">payments_amqp_interface</span><span class="p">.</span><span class="nx">OrderToProcessView</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ID</span><span class="p">:</span> <span class="nb">string</span><span class="p">(</span><span class="nx">id</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="nx">Price</span><span class="p">:</span> <span class="nx">payments_amqp_interface</span><span class="p">.</span><span class="nx">PriceView</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Cents</span><span class="p">:</span> <span class="nx">price</span><span class="p">.</span><span class="nf">Cents</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="nx">Currency</span><span class="p">:</span> <span class="nx">price</span><span class="p">.</span><span class="nf">Currency</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">b</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Marshal</span><span class="p">(</span><span class="nx">order</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;cannot marshal order for amqp&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="p">=</span> <span class="nx">i</span><span class="p">.</span><span class="nx">channel</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">i</span><span class="p">.</span><span class="nx">queue</span><span class="p">.</span><span class="nx">Name</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kc">false</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kc">false</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">amqp</span><span class="p">.</span><span class="nx">Publishing</span><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">ContentType</span><span class="p">:</span> <span class="s">&#34;application/json&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="nx">Body</span><span class="p">:</span> <span class="nx">b</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">Wrap</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="s">&#34;cannot send order to amqp&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;sent order %s to amqp&#34;</span><span class="p">,</span> <span class="nx">id</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>and to receive messages</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/orders/interfaces/public/http/orders.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">PaymentsInterface</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">conn</span> <span class="o">*</span><span class="nx">amqp</span><span class="p">.</span><span class="nx">Connection</span> </span></span><span class="line"><span class="cl"> <span class="nx">queue</span> <span class="nx">amqp</span><span class="p">.</span><span class="nx">Queue</span> </span></span><span class="line"><span class="cl"> <span class="nx">channel</span> <span class="o">*</span><span class="nx">amqp</span><span class="p">.</span><span class="nx">Channel</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">service</span> <span class="nx">application</span><span class="p">.</span><span class="nx">PaymentsService</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">o</span> <span class="nx">PaymentsInterface</span><span class="p">)</span> <span class="nf">Run</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">select</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="nx">msg</span> <span class="o">:=</span> <span class="o">&lt;-</span><span class="nx">msgs</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">o</span><span class="p">.</span><span class="nf">processMsg</span><span class="p">(</span><span class="nx">msg</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;cannot process msg: %s, err: %s&#34;</span><span class="p">,</span> <span class="nx">msg</span><span class="p">.</span><span class="nx">Body</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="o">&lt;-</span><span class="nx">done</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">o</span> <span class="nx">PaymentsInterface</span><span class="p">)</span> <span class="nf">processMsg</span><span class="p">(</span><span class="nx">msg</span> <span class="nx">amqp</span><span class="p">.</span><span class="nx">Delivery</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">orderView</span> <span class="o">:=</span> <span class="nx">OrderToProcessView</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Unmarshal</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">Body</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">orderView</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;cannot decode msg: %s, error: %s&#34;</span><span class="p">,</span> <span class="nb">string</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">Body</span><span class="p">),</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">orderPrice</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">price</span><span class="p">.</span><span class="nf">NewPrice</span><span class="p">(</span><span class="nx">orderView</span><span class="p">.</span><span class="nx">Price</span><span class="p">.</span><span class="nx">Cents</span><span class="p">,</span> <span class="nx">orderView</span><span class="p">.</span><span class="nx">Price</span><span class="p">.</span><span class="nx">Currency</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Printf</span><span class="p">(</span><span class="s">&#34;cannot decode price for msg %s: %s&#34;</span><span class="p">,</span> <span class="nb">string</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">Body</span><span class="p">),</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">o</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nf">InitializeOrderPayment</span><span class="p">(</span><span class="nx">orderView</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> <span class="nx">orderPrice</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h4 id="monolith-1">Monolith</h4> <p>In monolith version sending to channel is stupid easy</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/orders/infrastructure/payments/intraprocess.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">IntraprocessService</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">orders</span> <span class="kd">chan</span> <span class="o">&lt;-</span> <span class="nx">intraprocess</span><span class="p">.</span><span class="nx">OrderToProcess</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">NewIntraprocessService</span><span class="p">(</span><span class="nx">ordersChannel</span> <span class="kd">chan</span> <span class="o">&lt;-</span> <span class="nx">intraprocess</span><span class="p">.</span><span class="nx">OrderToProcess</span><span class="p">)</span> <span class="nx">IntraprocessService</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="nx">IntraprocessService</span><span class="p">{</span><span class="nx">ordersChannel</span><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">i</span> <span class="nx">IntraprocessService</span><span class="p">)</span> <span class="nf">InitializeOrderPayment</span><span class="p">(</span><span class="nx">id</span> <span class="nx">orders</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> <span class="nx">price</span> <span class="nx">price</span><span class="p">.</span><span class="nx">Price</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">i</span><span class="p">.</span><span class="nx">orders</span> <span class="o">&lt;-</span> <span class="nx">intraprocess</span><span class="p">.</span><span class="nx">OrderToProcess</span><span class="p">{</span><span class="nb">string</span><span class="p">(</span><span class="nx">id</span><span class="p">),</span> <span class="nx">price</span><span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>and receiving <em>(I only removed shutdown (close) support, to not complicate the code)</em>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="c1">// pkg/payments/interfaces/intraprocess/orders.go </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">PaymentsInterface</span> <span class="kd">struct</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">orders</span> <span class="o">&lt;-</span><span class="kd">chan</span> <span class="nx">OrderToProcess</span> </span></span><span class="line"><span class="cl"> <span class="nx">service</span> <span class="nx">application</span><span class="p">.</span><span class="nx">PaymentsService</span> </span></span><span class="line"><span class="cl"> <span class="nx">orderProcessingWg</span> <span class="o">*</span><span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</span> </span></span><span class="line"><span class="cl"> <span class="nx">runEnded</span> <span class="kd">chan</span> <span class="kd">struct</span><span class="p">{}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// .. </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">o</span> <span class="nx">PaymentsInterface</span><span class="p">)</span> <span class="nf">Run</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="nx">order</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">o</span><span class="p">.</span><span class="nx">orders</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">go</span> <span class="kd">func</span><span class="p">(</span><span class="nx">orderToPay</span> <span class="nx">OrderToProcess</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... </span></span></span><span class="line"><span class="cl"><span class="c1"></span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">o</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nf">InitializeOrderPayment</span><span class="p">(</span><span class="nx">orderToPay</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> <span class="nx">orderToPay</span><span class="p">.</span><span class="nx">Price</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">log</span><span class="p">.</span><span class="nf">Print</span><span class="p">(</span><span class="s">&#34;Cannot initialize payment:&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}(</span><span class="nx">order</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h3 id="and-more">And more&hellip;</h3> <p>Marking order as paid works almost the same as placing the order (REST API/Function call). If you&rsquo;re curious how it works, please check the full source code.</p> <p><strong>Full source can be found here:</strong> <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2FThreeDotsLabs%2Fmonolith-microservice-shop" target="_blank">https://github.com/ThreeDotsLabs/monolith-microservice-shop</a></p> <p>I&rsquo;ve implemented some acceptance tests which will check that all flow works exactly the same for both monolith and microservices. Tests can be found in <code>tests/acceptance_test.go</code>.</p> <p>You can find more info on how to run the project and tests in <code>README.md</code>.</p> <p>There is still some code, which is not covered here. If you want to get a deeper understanding of this code, please follow me on the <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Froblaszczak" target="_blank">Twitter (@roblaszczak)</a> or subscribe to our newsletter - you will be notified when the article is ready. You will learn some basic concepts of the Golang, if you don’t know it already. I will also make this code more production grade.</p> <p>We are also planning to write some DevOps articles (Packer, Terraform, Ansible).</p> <h2 id="summary">Summary</h2> <p>What about another microservices advantage? It will be less flexible, but you can still deploy monolith independently, although you must use different flow. For example, you can use feature branches where the master branch will be production and commits for not released changes should be merged into this feature branches. These branches should be used in the staging environment. This is true that monolith is harder to scale because you must scale entire application, not a single module. But in many cases, it is good enough. In this case we cannot assign one team per repository, but fortunately there won&rsquo;t be many conflicts if modules are separated well. The only conflicts will occur in layers responsible for cross modules communication, but the same conflicts will occur in REST/Queue API’s. In monolith though it will have compile check unlike in microservices, where you must validate contracts, what requires extra work. Also, with monolith you will receive compile-check of shared types. The fast rewrite of the module (microservice in microservices architecture) is a matter of good modules separation - if the module is properly separated and designed you can replace it without touching anything outside.</p> <div class="notice note"> <div class="notice-head"><svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10 9V14M10 19C5.02944 19 1 14.9706 1 10C1 5.02944 5.02944 1 10 1C14.9706 1 19 5.02944 19 10C19 14.9706 14.9706 19 10 19ZM10.0498 6V6.1L9.9502 6.1002V6H10.0498Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </svg><p>Note</p> </div> <div class="notice-body"><p> <p>And I will repeat it again: there is no silver bullet. <strong>It always depends</strong>. But probably in most cases <em>Clean Monolith</em> will be enough for the beginning. If it will be well designed, the decision to move to microservices architecture will be not a problem.</p> <p>In some cases (for example a small projects) it can be an overkill to write this all extra code for interfaces/infrastructure layer. But what is a small project? Well, it depends&hellip;</p> </p></div> </div> <p>Plan before you start implementing. There is no “lack of design”, it is only good design and bad design. Event storming is a good idea to kick-off the project.</p> <p>If you want to receive notification about the next article, you can subscribe to our newsletter or follow me on Twitter: @roblaszczak</p> <p>I’m aware that I’ve introduced a lot of techniques, of which a major part might be new for you. The good news is that to have good architecture you don’t need to learn all of these techniques at the same time. It’s also hard to properly understand these techniques simultaneously. I would recommend starting with Clean Architecture, then check some basics of CQRS and then you can drive into DDD. At the end of the article, I will provide some useful resources, which I had used when I was learning these techniques.</p> <p>If you have any questions, please write to me on Twitter.</p> <p>Thanks for the inspiration for this article: <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Funclebobmartin" target="_blank">@unclebobmartin</a>, <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Fvaughnvernon" target="_blank">@vaughnvernon</a>, <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Fmartinfowler" target="_blank">@martinfowler</a> and <a href="proxy.php?url=https%3A%2F%2Ftwitter.com%2Fmariuszgil" target="_blank">@mariuszgil</a>.</p>Why learn with us?Mon, 01 Jan 0001 00:00:00 +0000The Domain EngineerMon, 01 Jan 0001 00:00:00 +0000<p>Learn the essentials of Domain-Driven Design by writing real-life Go code. Master reliable backend systems.</p>Real-World Go AI engineeringMon, 01 Jan 0001 00:00:00 +0000<p>Tired of overhyped AI tutorials from people with no clue about backend engineering?</p> <p>Learn how to build production-grade LLM services. Designed for experienced Go backend engineers.</p>Ready to build your next project with Hugo?Mon, 01 Jan 0001 00:00:00 +0000Privacyhttps://threedots.tech/privacy-policy/Mon, 01 Jan 0001 00:00:00 +0000https://threedots.tech/privacy-policy/ <div> <h1 id="privacy-policy">Privacy Policy</h1> <p>Your privacy is important to us. It is Three Dots Labs R. Laszczak M. Smolka s.c.'s policy to respect your privacy and comply with any applicable law and regulation regarding any personal information we may collect about you, including across our website, <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F">https://threedots.tech/</a>, and other sites we own and operate. </p> <p>Personal information is any information about you which can be used to identify you. This includes information about you as a person (such as name, address, and date of birth), your devices, payment details, and even information about how you use a website or online service. </p> <p>In the event our site contains links to third-party sites and services, please be aware that those sites and services have their own privacy policies. After following a link to any third-party content, you should read their posted privacy policy information about how they collect and use personal information. This Privacy Policy does not apply to any of your activities after you leave our site. </p> <p>This policy is effective as of 27 May 2022. </p> <p>Last updated: 27 May 2022 </p> <h3>Information We Collect</h3> <p>Information we collect falls into one of two categories: “voluntarily provided” information and “automatically collected” information. </p> <p>“Voluntarily provided” information refers to any information you knowingly and actively provide us when using or participating in any of our services and promotions. </p> <p>“Automatically collected” information refers to any information automatically sent by your devices in the course of accessing our products and services. </p> <h4>Log Data</h4> <p>When you visit our website, our servers may automatically log the standard data provided by your web browser. It may include your device’s Internet Protocol (IP) address, your browser type and version, the pages you visit, the time and date of your visit, the time spent on each page, and other details about your visit. </p> <p>Additionally, if you encounter certain errors while using the site, we may automatically collect data about the error and the circumstances surrounding its occurrence. This data may include technical details about your device, what you were trying to do when the error happened, and other technical information relating to the problem. You may or may not receive notice of such errors, even in the moment they occur, that they have occurred, or what the nature of the error is. </p> <p>Please be aware that while this information may not be personally identifying by itself, it may be possible to combine it with other data to personally identify individual persons. </p> <h4>Personal Information</h4> <p>We may ask for personal information — for example, when you register an account or when you contact us — which may include one or more of the following: </p> <ul> <li>Name</li> <li>Email</li> </ul> <h4>Legitimate Reasons for Processing Your Personal Information</h4> <p>We only collect and use your personal information when we have a legitimate reason for doing so. In which instance, we only collect personal information that is reasonably necessary to provide our services to you. </p> <h4>Collection and Use of Information</h4> <p>We may collect personal information from you when you do any of the following on our website: </p> <ul> <li>Register for an account</li> <li>Use a mobile device or web browser to access our content</li> <li>Contact us via email, social media, or on any similar technologies</li> <li>When you mention us on social media</li> </ul> <p>We may collect, hold, use, and disclose information for the following purposes, and personal information will not be further processed in a manner that is incompatible with these purposes: </p> <ul> <li>to provide you with our platform's core features and services</li> <li>to contact and communicate with you</li> <li>to enable you to access and use our website, associated applications, and associated social media platforms</li> </ul> <p>We may combine voluntarily provided and automatically collected personal information with general information or research data we receive from other trusted sources. For example, Our marketing and market research activities may uncover data and insights, which we may combine with information about how visitors use our site to improve our site and your experience on it. </p> <h4>Security of Your Personal Information</h4> <p>When we collect and process personal information, and while we retain this information, we will protect it within commercially acceptable means to prevent loss and theft, as well as unauthorized access, disclosure, copying, use, or modification. </p> <p>Although we will do our best to protect the personal information you provide to us, we advise that no method of electronic transmission or storage is 100% secure, and no one can guarantee absolute data security. </p> <p>You are responsible for selecting any password and its overall security strength, ensuring the security of your own information within the bounds of our services. For example, ensuring any passwords associated with accessing your personal information and accounts are secure and confidential. </p> <h4>How Long We Keep Your Personal Information</h4> <p>We keep your personal information only for as long as we need to. This time period may depend on what we are using your information for, in accordance with this privacy policy. For example, if you have provided us with personal information as part of creating an account with us, we may retain this information for the duration your account exists on our system. If your personal information is no longer required for this purpose, we will delete it or make it anonymous by removing all details that identify you. </p> <p>However, if necessary, we may retain your personal information for our compliance with a legal, accounting, or reporting obligation or for archiving purposes in the public interest, scientific, or historical research purposes or statistical purposes. </p> <h3>Children’s Privacy</h3> <p>We do not aim any of our products or services directly at children under the age of 13, and we do not knowingly collect personal information about children under 13. </p> <h3>Disclosure of Personal Information to Third Parties</h3> <p>We may disclose personal information to: </p> <ul> <li>a parent, subsidiary, or affiliate of our company</li> <li>third-party service providers for the purpose of enabling them to provide their services, including (without limitation) IT service providers, data storage, hosting and server providers, ad networks, error loggers, debt collectors, maintenance or problem-solving providers, marketing or advertising providers, professional advisors, and payment systems operators</li> <li>our employees, contractors, and/or related entities</li> <li>our existing or potential agents or business partners</li> <li>credit reporting agencies, courts, tribunals, and regulatory authorities, in the event you fail to pay for goods or services we have provided to you</li> <li>courts, tribunals, regulatory authorities, and law enforcement officers, as required by law, in connection with any actual or prospective legal proceedings, or in order to establish, exercise, or defend our legal rights</li> <li>third parties, including agents or sub-contractors, who assist us in providing information, products, services, or direct marketing to you</li> <li>third parties to collect and process data</li> <li>an entity that buys, or to which we transfer all or substantially all of our assets and business</li> </ul> <p>Third parties we currently use include: </p> <ul> <li>ConvertKit</li> <li>Google Adsense</li> <li>Facebook Ads</li> <li>Paddle</li> </ul> <h3>International Transfers of Personal Information</h3> <p>The personal information we collect is stored and/or processed in United States, Belgium, and Poland, or where we or our partners, affiliates, and third-party providers maintain facilities. </p> <p>The countries to which we store, process, or transfer your personal information may not have the same data protection laws as the country in which you initially provided the information. If we transfer your personal information to third parties in other countries: (i) we will perform those transfers in accordance with the requirements of applicable law; and (ii) we will protect the transferred personal information in accordance with this privacy policy. </p> <h3>Your Rights and Controlling Your Personal Information</h3> <p><strong>Your choice:</strong> By providing personal information to us, you understand we will collect, hold, use, and disclose your personal information in accordance with this privacy policy. You do not have to provide personal information to us, however, if you do not, it may affect your use of our website or the products and/or services offered on or through it. </p> <p><strong>Information from third parties:</strong> If we receive personal information about you from a third party, we will protect it as set out in this privacy policy. If you are a third party providing personal information about somebody else, you represent and warrant that you have such person’s consent to provide the personal information to us. </p> <p><strong>Marketing permission:</strong> If you have previously agreed to us using your personal information for direct marketing purposes, you may change your mind at any time by contacting us using the details below. </p> <p><strong>Access:</strong> You may request details of the personal information that we hold about you. </p> <p><strong>Correction:</strong> If you believe that any information we hold about you is inaccurate, out of date, incomplete, irrelevant, or misleading, please contact us using the details provided in this privacy policy. We will take reasonable steps to correct any information found to be inaccurate, incomplete, misleading, or out of date. </p> <p><strong>Non-discrimination:</strong> We will not discriminate against you for exercising any of your rights over your personal information. Unless your personal information is required to provide you with a particular service or offer (for example providing user support), we will not deny you goods or services and/or charge you different prices or rates for goods or services, including through granting discounts or other benefits, or imposing penalties, or provide you with a different level or quality of goods or services. </p> <p><strong>Notification of data breaches:</strong> We will comply with laws applicable to us in respect of any data breach. </p> <p><strong>Complaints:</strong> If you believe that we have breached a relevant data protection law and wish to make a complaint, please contact us using the details below and provide us with full details of the alleged breach. We will promptly investigate your complaint and respond to you, in writing, setting out the outcome of our investigation and the steps we will take to deal with your complaint. You also have the right to contact a regulatory body or data protection authority in relation to your complaint. </p> <p><strong>Unsubscribe:</strong> To unsubscribe from our email database or opt-out of communications (including marketing communications), please contact us using the details provided in this privacy policy, or opt-out using the opt-out facilities provided in the communication. We may need to request specific information from you to help us confirm your identity. </p> <h3>Use of Cookies</h3> <p>We use “cookies” to collect information about you and your activity across our site. A cookie is a small piece of data that our website stores on your computer, and accesses each time you visit, so we can understand how you use our site. This helps us serve you content based on preferences you have specified. </p> <p>Please refer to our Cookie Policy for more information. </p> <h3>Business Transfers</h3> <p>If we or our assets are acquired, or in the unlikely event that we go out of business or enter bankruptcy, we would include data, including your personal information, among the assets transferred to any parties who acquire us. You acknowledge that such transfers may occur, and that any parties who acquire us may, to the extent permitted by applicable law, continue to use your personal information according to this policy, which they will be required to assume as it is the basis for any ownership or use rights we have over such information. </p> <h3>Limits of Our Policy</h3> <p>Our website may link to external sites that are not operated by us. Please be aware that we have no control over the content and policies of those sites, and cannot accept responsibility or liability for their respective privacy practices. </p> <h3>Changes to This Policy</h3> <p>At our discretion, we may change our privacy policy to reflect updates to our business processes, current acceptable practices, or legislative or regulatory changes. If we decide to change this privacy policy, we will post the changes here at the same link by which you are accessing this privacy policy. </p> <p>If the changes are significant, or if required by applicable law, we will contact you (based on your selected preferences for communications from us) and all our registered users with the new details and links to the updated or changed policy. </p> <p>If required by law, we will get your permission or give you the opportunity to opt in to or opt out of, as applicable, any new uses of your personal information. </p> <h3>Additional Disclosures for Australian Privacy Act Compliance (AU)</h3> <h4>International Transfers of Personal Information</h4> <p>Where the disclosure of your personal information is solely subject to Australian privacy laws, you acknowledge that some third parties may not be regulated by the Privacy Act and the Australian Privacy Principles in the Privacy Act. You acknowledge that if any such third party engages in any act or practice that contravenes the Australian Privacy Principles, it would not be accountable under the Privacy Act, and you will not be able to seek redress under the Privacy Act. </p> <h3>Additional Disclosures for General Data Protection Regulation (GDPR) Compliance (EU)</h3> <h4>Data Controller / Data Processor</h4> <p>The GDPR distinguishes between organisations that process personal information for their own purposes (known as “data controllers”) and organizations that process personal information on behalf of other organizations (known as “data processors”). We, Three Dots Labs R. Laszczak M. Smolka s.c., located at the address provided in our Contact Us section, are a Data Controller with respect to the personal information you provide to us. </p> <h4>Legal Bases for Processing Your Personal Information</h4> <p>We will only collect and use your personal information when we have a legal right to do so. In which case, we will collect and use your personal information lawfully, fairly, and in a transparent manner. If we seek your consent to process your personal information, and you are under 16 years of age, we will seek your parent or legal guardian’s consent to process your personal information for that specific purpose. </p> <p>Our lawful bases depend on the services you use and how you use them. This means we only collect and use your information on the following grounds: </p> <h5>Consent From You</h5> <p>Where you give us consent to collect and use your personal information for a specific purpose. You may withdraw your consent at any time using the facilities we provide; however this will not affect any use of your information that has already taken place. When you contact us, you may consent to your name and email address being used so we can respond to your enquiry. While you may request that we delete your contact details at any time, we cannot recall any email we have already sent. If you have any further enquiries about how to withdraw your consent, please feel free to enquire using the details provided in the Contact Us section of this privacy policy. </p> <h5>Performance of a Contract or Transaction</h5> <p>Where you have entered into a contract or transaction with us, or in order to take preparatory steps prior to our entering into a contract or transaction with you. For example, if you contact us with an enquiry, we may require personal information such as your name and contact details in order to respond. </p> <h5>Our Legitimate Interests</h5> <p>Where we assess it is necessary for our legitimate interests, such as for us to provide, operate, improve and communicate our services. We consider our legitimate interests to include research and development, understanding our audience, marketing and promoting our services, measures taken to operate our services efficiently, marketing analysis, and measures taken to protect our legal rights and interests. </p> <h5>Compliance with Law</h5> <p>In some cases, we may have a legal obligation to use or keep your personal information. Such cases may include (but are not limited to) court orders, criminal investigations, government requests, and regulatory obligations. If you have any further enquiries about how we retain personal information in order to comply with the law, please feel free to enquire using the details provided in the Contact Us section of this privacy policy. </p> <h4>International Transfers Outside of the European Economic Area (EEA)</h4> <p>We will ensure that any transfer of personal information from countries in the European Economic Area (EEA) to countries outside the EEA will be protected by appropriate safeguards, for example by using standard data protection clauses approved by the European Commission, or the use of binding corporate rules or other legally accepted means. </p> <h4>Your Rights and Controlling Your Personal Information</h4> <p><strong>Restrict:</strong> You have the right to request that we restrict the processing of your personal information if (i) you are concerned about the accuracy of your personal information; (ii) you believe your personal information has been unlawfully processed; (iii) you need us to maintain the personal information solely for the purpose of a legal claim; or (iv) we are in the process of considering your objection in relation to processing on the basis of legitimate interests. </p> <p><strong>Objecting to processing:</strong> You have the right to object to processing of your personal information that is based on our legitimate interests or public interest. If this is done, we must provide compelling legitimate grounds for the processing which overrides your interests, rights, and freedoms, in order to proceed with the processing of your personal information. </p> <p><strong>Data portability:</strong> You may have the right to request a copy of the personal information we hold about you. Where possible, we will provide this information in CSV format or other easily readable machine format. You may also have the right to request that we transfer this personal information to a third party. </p> <p><strong>Deletion:</strong> You may have a right to request that we delete the personal information we hold about you at any time, and we will take reasonable steps to delete your personal information from our current records. If you ask us to delete your personal information, we will let you know how the deletion affects your use of our website or products and services. There may be exceptions to this right for specific legal reasons which, if applicable, we will set out for you in response to your request. If you terminate or delete your account, we will delete your personal information within 28 days of the deletion of your account. Please be aware that search engines and similar third parties may still retain copies of your personal information that has been made public at least once, like certain profile information and public comments, even after you have deleted the information from our services or deactivated your account. </p> <h3>Additional Disclosures for California Compliance (US)</h3> <p>Under California Civil Code Section 1798.83, if you live in California and your business relationship with us is mainly for personal, family, or household purposes, you may ask us about the information we release to other organizations for their marketing purposes. </p> <p>To make such a request, please contact us using the details provided in this privacy policy with “Request for California privacy information” in the subject line. You may make this type of request once every calendar year. We will email you a list of categories of personal information we revealed to other organisations for their marketing purposes in the last calendar year, along with their names and addresses. Not all personal information shared in this way is covered by Section 1798.83 of the California Civil Code. </p> <h4>Do Not Track</h4> <p>Some browsers have a “Do Not Track” feature that lets you tell websites that you do not want to have your online activities tracked. At this time, we do not respond to browser “Do Not Track” signals. </p> <p>We adhere to the standards outlined in this privacy policy, ensuring we collect and process personal information lawfully, fairly, transparently, and with legitimate, legal reasons for doing so. </p> <h4>Cookies and Pixels</h4> <p>At all times, you may decline cookies from our site if your browser permits. Most browsers allow you to activate settings on your browser to refuse the setting of all or some cookies. Accordingly, your ability to limit cookies is based only on your browser’s capabilities. Please refer to the Cookies section of this privacy policy for more information. </p> <h4>CCPA-permitted financial incentives</h4> <p>In accordance with your right to non-discrimination, we may offer you certain financial incentives permitted by the CCPA that can result in different prices, rates, or quality levels for the goods or services we provide. </p> <p>Any CCPA-permitted financial incentive we offer will reasonably relate to the value of your personal information, and we will provide written terms that describe clearly the nature of such an offer. Participation in a financial incentive program requires your prior opt-in consent, which you may revoke at any time. </p> <h4>California Notice of Collection</h4> <p>In the past 12 months, we have collected the following categories of personal information enumerated in the California Consumer Privacy Act: </p> <ul> <li>Identifiers, such as name, email address, phone number account name, IP address, and an ID or number assigned to your account.</li> <li>Commercial information, such as products or services history and purchases.</li> </ul> <p>For more information on information we collect, including the sources we receive information from, review the “Information We Collect” section. We collect and use these categories of personal information for the business purposes described in the “Collection and Use of Information” section, including to provide and manage our Service. </p> <h4>Right to Know and Delete</h4> <p>If you are a California resident, you have rights to delete your personal information we collected and know certain information about our data practices in the preceding 12 months. In particular, you have the right to request the following from us: </p> <ul> <li>The categories of personal information we have collected about you;</li> <li>The categories of sources from which the personal information was collected;</li> <li>The categories of personal information about you we disclosed for a business purpose or sold;</li> <li>The categories of third parties to whom the personal information was disclosed for a business purpose or sold;</li> <li>The business or commercial purpose for collecting or selling the personal information; and</li> <li>The specific pieces of personal information we have collected about you.</li> </ul> <p>To exercise any of these rights, please contact us using the details provided in this privacy policy. </p> <h4>Shine the Light</h4> <p>If you are a California resident, in addition to the rights discussed above, you have the right to request information from us regarding the manner in which we share certain personal information as defined by California’s “Shine the Light” with third parties and affiliates for their own direct marketing purposes. </p> <p>To receive this information, send us a request using the contact details provided in this privacy policy. Requests must include “California Privacy Rights Request” in the first line of the description and include your name, street address, city, state, and ZIP code. </p> <h3>Contact Us</h3> <p>For any questions or concerns regarding your privacy, you may contact us using the following details: </p> <p>Robert Laszczak<br> [email protected] </p> <h1 id="terms">Terms of Service</h1> <p>These Terms of Service govern your use of the website located at <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F">https://threedots.tech/</a> and any related services provided by Three Dots Labs R. Laszczak M. Smolka s.c.. </p> <p>By accessing <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F">https://threedots.tech/</a>, you agree to abide by these Terms of Service and to comply with all applicable laws and regulations. If you do not agree with these Terms of Service, you are prohibited from using or accessing this website or using any other services provided by Three Dots Labs R. Laszczak M. Smolka s.c.. </p> <p>We, Three Dots Labs R. Laszczak M. Smolka s.c., reserve the right to review and amend any of these Terms of Service at our sole discretion. Upon doing so, we will update this page. Any changes to these Terms of Service will take effect immediately from the date of publication. </p> <p>These Terms of Service were last updated on 27 May 2022. </p> <h3>Limitations of Use</h3> <p>By using this website, you warrant on behalf of yourself, your users, and other parties you represent that you will not: </p> <ol> <li>modify, copy, prepare derivative works of, decompile, or reverse engineer any materials and software contained on this website;</li> <li>remove any copyright or other proprietary notations from any materials and software on this website;</li> <li>transfer the materials to another person or “mirror” the materials on any other server;</li> <li>knowingly or negligently use this website or any of its associated services in a way that abuses or disrupts our networks or any other service Three Dots Labs R. Laszczak M. Smolka s.c. provides;</li> <li>use this website or its associated services to transmit or publish any harassing, indecent, obscene, fraudulent, or unlawful material;</li> <li>use this website or its associated services in violation of any applicable laws or regulations;</li> <li>use this website in conjunction with sending unauthorized advertising or spam;</li> <li>harvest, collect, or gather user data without the user’s consent; or</li> <li>use this website or its associated services in such a way that may infringe the privacy, intellectual property rights, or other rights of third parties.</li> </ol> <h3>Intellectual Property</h3> <p>The intellectual property in the materials contained in this website are owned by or licensed to Three Dots Labs R. Laszczak M. Smolka s.c. and are protected by applicable copyright and trademark law. We grant our users permission to download one copy of the materials for personal, non-commercial transitory use. </p> <p>This constitutes the grant of a license, not a transfer of title. This license shall automatically terminate if you violate any of these restrictions or the Terms of Service, and may be terminated by Three Dots Labs R. Laszczak M. Smolka s.c. at any time. </p> <h3>Liability</h3> <p>Our website and the materials on our website are provided on an 'as is' basis. To the extent permitted by law, Three Dots Labs R. Laszczak M. Smolka s.c. makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties including, without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property, or other violation of rights. </p> <p>In no event shall Three Dots Labs R. Laszczak M. Smolka s.c. or its suppliers be liable for any consequential loss suffered or incurred by you or any third party arising from the use or inability to use this website or the materials on this website, even if Three Dots Labs R. Laszczak M. Smolka s.c. or an authorized representative has been notified, orally or in writing, of the possibility of such damage. </p> <p>In the context of this agreement, “consequential loss” includes any consequential loss, indirect loss, real or anticipated loss of profit, loss of benefit, loss of revenue, loss of business, loss of goodwill, loss of opportunity, loss of savings, loss of reputation, loss of use and/or loss or corruption of data, whether under statute, contract, equity, tort (including negligence), indemnity, or otherwise. </p> <p>Because some jurisdictions do not allow limitations on implied warranties, or limitations of liability for consequential or incidental damages, these limitations may not apply to you. </p> <h3>Accuracy of Materials</h3> <p>The materials appearing on our website are not comprehensive and are for general information purposes only. Three Dots Labs R. Laszczak M. Smolka s.c. does not warrant or make any representations concerning the accuracy, likely results, or reliability of the use of the materials on this website, or otherwise relating to such materials or on any resources linked to this website. </p> <h3>Links</h3> <p>Three Dots Labs R. Laszczak M. Smolka s.c. has not reviewed all of the sites linked to its website and is not responsible for the contents of any such linked site. The inclusion of any link does not imply endorsement, approval, or control by Three Dots Labs R. Laszczak M. Smolka s.c. of the site. Use of any such linked site is at your own risk and we strongly advise you make your own investigations with respect to the suitability of those sites. </p> <h3>Right to Terminate</h3> <p>We may suspend or terminate your right to use our website and terminate these Terms of Service immediately upon written notice to you for any breach of these Terms of Service. </p> <h3>Severance</h3> <p>Any term of these Terms of Service which is wholly or partially void or unenforceable is severed to the extent that it is void or unenforceable. The validity of the remainder of these Terms of Service is not affected. </p> <h3>Governing Law</h3> <p>These Terms of Service are governed by and construed in accordance with the laws of Poland. You irrevocably submit to the exclusive jurisdiction of the courts in that State or location. </p> <h1>Cookie Policy</h1> <p>We use cookies to help improve your experience of our website at <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F">https://threedots.tech/</a>. This cookie policy is part of Three Dots Labs R. Laszczak M. Smolka s.c.'s privacy policy. It covers the use of cookies between your device and our site. </p> <p>We also provide basic information on third-party services we may use, who may also use cookies as part of their service. This policy does not cover their cookies. </p> <p>If you don’t wish to accept cookies from us, you should instruct your browser to refuse cookies from <a href="proxy.php?url=https%3A%2F%2Fthreedots.tech%2F">https://threedots.tech/</a>. In such a case, we may be unable to provide you with some of your desired content and services. </p> <h3>What is a cookie?</h3> <p>A cookie is a small piece of data that a website stores on your device when you visit. It typically contains information about the website itself, a unique identifier that allows the site to recognize your web browser when you return, additional data that serves the cookie’s purpose, and the lifespan of the cookie itself. </p> <p>Cookies are used to enable certain features (e.g. logging in), track site usage (e.g. analytics), store your user settings (e.g. time zone, notification preferences), and to personalize your content (e.g. advertising, language). </p> <p>Cookies set by the website you are visiting are usually referred to as first-party cookies. They typically only track your activity on that particular site. </p> <p>Cookies set by other sites and companies (i.e. third parties) are called third-party cookies They can be used to track you on other websites that use the same third-party service. </p> <h3>Types of cookies and how we use them</h3> <h4>Essential cookies</h4> <p>Essential cookies are crucial to your experience of a website, enabling core features like user logins, account management, shopping carts, and payment processing. </p> <p>We use essential cookies to enable certain functions on our website. </p> <h4>Performance cookies</h4> <p>Performance cookies track how you use a website during your visit. Typically, this information is anonymous and aggregated, with information tracked across all site users. They help companies understand visitor usage patterns, identify and diagnose problems or errors their users may encounter, and make better strategic decisions in improving their audience’s overall website experience. These cookies may be set by the website you’re visiting (first-party) or by third-party services. They do not collect personal information about you. </p> <p>We do not use this type of cookie on our site. </p> <h4>Functionality cookies</h4> <p>Functionality cookies are used to collect information about your device and any settings you may configure on the website you’re visiting (like language and time zone settings). With this information, websites can provide you with customized, enhanced, or optimized content and services. These cookies may be set by the website you’re visiting (first-party) or by third-party services. </p> <p>We use functionality cookies for selected features on our site. </p> <h4>Targeting/advertising cookies</h4> <p>Targeting/advertising cookies help determine what promotional content is most relevant and appropriate to you and your interests. Websites may use them to deliver targeted advertising or limit the number of times you see an advertisement. This helps companies improve the effectiveness of their campaigns and the quality of content presented to you. These cookies may be set by the website you’re visiting (first-party) or by third-party services. Targeting/advertising cookies set by third-parties may be used to track you on other websites that use the same third-party service. </p> <p>We use targeting/advertising cookies on our site. </p> <h1>Acceptable Use Policy</h1> <p>This acceptable use policy covers the products, services, and technologies (collectively referred to as the “Products”) provided by Three Dots Labs R. Laszczak M. Smolka s.c. under any ongoing agreement. It’s designed to protect us, our customers, and the general Internet community from unethical, irresponsible, and illegal activity. </p> <p>Three Dots Labs R. Laszczak M. Smolka s.c. customers found engaging in activities prohibited by this acceptable use policy can be liable for service suspension and account termination. In extreme cases, we may be legally obliged to report such customers to the relevant authorities. </p> <p>This policy was last reviewed on 27 May 2022. </p> <h3>Fair use</h3> <p>We provide our facilities with the assumption your use will be “business as usual”, as per our offer schedule. If your use is considered to be excessive, then additional fees may be charged, or capacity may be restricted. </p> <p>We are opposed to all forms of abuse, discrimination, rights infringement, and/or any action that harms or disadvantages any group, individual, or resource. We expect our customers and, where applicable, their users (“end-users”) to likewise engage our Products with similar intent. </p> <h3>Customer accountability</h3> <p>We regard our customers as being responsible for their own actions as well as for the actions of anyone using our Products with the customer’s permission. This responsibility also applies to anyone using our Products on an unauthorized basis as a result of the customer’s failure to put in place reasonable security measures. </p> <p>By accepting Products from us, our customers agree to ensure adherence to this policy on behalf of anyone using the Products as their end users. Complaints regarding the actions of customers or their end-users will be forwarded to the nominated contact for the account in question. </p> <p>If a customer — or their end-user or anyone using our Products as a result of the customer — violates our acceptable use policy, we reserve the right to terminate any Products associated with the offending account or the account itself or take any remedial or preventative action we deem appropriate, without notice. To the extent permitted by law, no credit will be available for interruptions of service resulting from any violation of our acceptable use policy. </p> <h3>Prohibited activity</h3> <h4>Copyright infringement and access to unauthorized material</h4> <p>Our Products must not be used to transmit, distribute or store any material in violation of any applicable law. This includes but isn’t limited to: </p> <ol type="i"> <li>any material protected by copyright, trademark, trade secret, or other intellectual property right used without proper authorization, and</li> <li>any material that is obscene, defamatory, constitutes an illegal threat or violates export control laws.</li> </ol> <p>The customer is solely responsible for all material they input, upload, disseminate, transmit, create or publish through or on our Products, and for obtaining legal permission to use any works included in such material. </p> <h4>SPAM and unauthorized message activity</h4> <p>Our Products must not be used for the purpose of sending unsolicited bulk or commercial messages in violation of the laws and regulations applicable to your jurisdiction (“spam”). This includes but isn’t limited to sending spam, soliciting customers from spam sent from other service providers, and collecting replies to spam sent from other service providers. </p> <p>Our Products must not be used for the purpose of running unconfirmed mailing lists or telephone number lists (“messaging lists”). This includes but isn’t limited to subscribing email addresses or telephone numbers to any messaging list without the permission of the email address or telephone number owner, and storing any email addresses or telephone numbers subscribed in this way. All messaging lists run on or hosted by our Products must be “confirmed opt-in”. Verification of the address or telephone number owner’s express permission must be available for the lifespan of the messaging list. </p> <p>We prohibit the use of email lists, telephone number lists or databases purchased from third parties intended for spam or unconfirmed messaging list purposes on our Products. </p> <p>This spam and unauthorized message activity policy applies to messages sent using our Products, or to messages sent from any network by the customer or any person on the customer’s behalf, that directly or indirectly refer the recipient to a site hosted via our Products. </p> <h4>Unethical, exploitative, and malicious activity</h4> <p>Our Products must not be used for the purpose of advertising, transmitting, or otherwise making available any software, program, product, or service designed to violate this acceptable use policy, or the acceptable use policy of other service providers. This includes but isn’t limited to facilitating the means to send spam and the initiation of network sniffing, pinging, packet spoofing, flooding, mail-bombing, and denial-of-service attacks. </p> <p>Our Products must not be used to access any account or electronic resource where the group or individual attempting to gain access does not own or is not authorized to access the resource (e.g. “hacking”, “cracking”, “phreaking”, etc.). </p> <p>Our Products must not be used for the purpose of intentionally or recklessly introducing viruses or malicious code into our Products and systems. </p> <p>Our Products must not be used for purposely engaging in activities designed to harass another group or individual. Our definition of harassment includes but is not limited to denial-of-service attacks, hate-speech, advocacy of racial or ethnic intolerance, and any activity intended to threaten, abuse, infringe upon the rights of, or discriminate against any group or individual. </p> <p>Other activities considered unethical, exploitative, and malicious include: </p> <ol> <li>Obtaining (or attempting to obtain) services from us with the intent to avoid payment;</li> <li>Using our facilities to obtain (or attempt to obtain) services from another provider with the intent to avoid payment;</li> <li>The unauthorized access, alteration, or destruction (or any attempt thereof) of any information about our customers or end-users, by any means or device;</li> <li>Using our facilities to interfere with the use of our facilities and network by other customers or authorized individuals;</li> <li>Publishing or transmitting any content of links that incite violence, depict a violent act, depict child pornography, or threaten anyone’s health and safety;</li> <li>Any act or omission in violation of consumer protection laws and regulations;</li> <li>Any violation of a person’s privacy.</li> </ol> <p>Our Products may not be used by any person or entity, which is involved with or suspected of involvement in activities or causes relating to illegal gambling; terrorism; narcotics trafficking; arms trafficking or the proliferation, development, design, manufacture, production, stockpiling, or use of nuclear, chemical or biological weapons, weapons of mass destruction, or missiles; in each case including any affiliation with others whatsoever who support the above such activities or causes. </p> <h4>Unauthorized use of Three Dots Labs R. Laszczak M. Smolka s.c. property</h4> <p>We prohibit the impersonation of Three Dots Labs R. Laszczak M. Smolka s.c., the representation of a significant business relationship with Three Dots Labs R. Laszczak M. Smolka s.c., or ownership of any Three Dots Labs R. Laszczak M. Smolka s.c. property (including our Products and brand) for the purpose of fraudulently gaining service, custom, patronage, or user trust. </p> <h3>About this policy</h3> <p>This policy outlines a non-exclusive list of activities and intent we deem unacceptable and incompatible with our brand. </p> <p>We reserve the right to modify this policy at any time by publishing the revised version on our website. The revised version will be effective from the earlier of: </p> <ul> <li>the date the customer uses our Products after we publish the revised version on our website; or</li> <li>30 days after we publish the revised version on our website.</li> </ul> </div>Go With The DomainMon, 01 Jan 0001 00:00:00 +0000<p>Examples are great for learning, so we decided to create a real, open-source, and deployable web application that would help us show the patterns. All chapters follow the same project, refactored over time.</p>Go MicroservicesMon, 01 Jan 0001 00:00:00 +0000<p>Did you try to use microservices and ended up with even a bigger mess than before? It&rsquo;s not your fault. Microservices aren&rsquo;t simple.</p>Go In One EveningMon, 01 Jan 0001 00:00:00 +0000<p>Learn one of the hottest programming languages in a fun and engaging way.</p>Go Event-DrivenMon, 01 Jan 0001 00:00:00 +0000<p>Learn building Extremely Scalable &amp; Resilient Go backend.</p>Go Backend MasterclassMon, 01 Jan 0001 00:00:00 +0000<p>Learn Go for practical backend development with real-world applications.</p>