This video is brain dump of some of the things that were top of mind for me regarding software architecture and testing strategies.
📄 Auto-Generated Transcript ▾
Transcript is auto-generated and may contain errors.
Hey folks, I'm just driving to the office here. Um, I'm just going to ramble about, I don't know, like software engineering, some architecture stuff. Um, just things I'm thinking about. I don't really have a I guess like a good cohesive plan in mind, but I'm driving to work partway through the day. I wanted to go in at lunch and then just got caught up with stuff, but I need to get into the office. So, um, I don't know. When I sat down in my car, I was like, I don't have a topic, but I I kind of just want to like at least for myself think through some of these. And I think sometimes it's kind of fun to to talk through it out loud. It's like uh when people say like when you write down notes and stuff, it gives you another opportunity for your brain to process it.
And I think it's like kind of the same thing when I'm when I'm chatting through this kind of stuff. So, two things are super high level that I'm hoping that I can kind of talk through as I as I drive to work here. Um, one is around my usage of plug-in architecture and uh some some patterns that are emerging and the things that I'm going to talk about there are not like they're not like groundbreaking like I'm inventing these things. cuz I'm I just want to talk about it as from the perspective of like I build things, follow a pattern and like at some point like more patterns emerge and that means I can go back and uh with like a new fresh lens on things. So I'll talk about that and then I think I don't know probably not the last video but recently I was recording talking about uh like Ralph loops and some orchestration and stuff like that.
So I want to kind of give an update on that um and just see where things go. So, if you're new to this channel, generally I'm answering submitted questions or I go to experienced devs and pick a post that's an interesting topic and primarily it's like around software engineering and especially like uh career development that kind of stuff and periodically I'll just talk about AI things or other software development. So, this is bit of a tangent but with that said if you have questions in general about software engineering career development please leave them below in the comments. Otherwise, you can go to codecomute.com. You can submit stuff anonymously that way and then I can make a video response for you and just share my perspective and try to help. Cool. Okay. So, um I've said this in other videos and other social media content and stuff, but for me, if left uh on my own, I really like building software uh with plug-in architectures.
And one of my reasons for doing this is that for me it's a very natural way to set up some boundaries. And so what I mean by that is when we think about coupling things together uh in terms of like dependencies or I don't know like really domains and responsibilities that are maybe bleeding together. Um, I like having plugins to uh to kind of give some forced separation. Like they truly sets up these concrete boundaries that um it almost feels like you have to to fight against the architecture to do the thing. So just to give you an example, uh like when I'm building plug-in architectures, I try to make it such that my plugins don't depend on each other, right? So, if I have plugins that you'd consider siblings, like uh say if a feature um is a pluggable feature and there's other plugins for other features, I don't generally have a uh like a feature that depends on another feature.
That would be a plugin directly referencing another plugin. And so that's one of uh one of my boundaries I set up. And when I say that you kind of have to fight against the architecture, what I mean is like in this particular case, you you'd literally have to go add a reference to this other U plug-in project. And it's not that that's impossible to do. It's actually not difficult to do. But it's out of place compared to the rest of the system. So when you do that, like you are the only person doing that. And I can actually enforce that with tests as well. I could enforce that. Uh, in this case, I'm not sure if I can use Roslin analyzers exactly that way just because of uh where they run, but you know, in theory, you can run stuff uh very early to prevent you from doing it.
It's not like you have to go wait until it's deployed in production and you go, "Aha, like I caught you cheating with, you know, cross referencing plugins or something." So, I like that. gives me um something early in my development process to kind of guide me down the patterns that I like to use. And again, this is just to be clear, this is not something that I will sit here and say like this is the best and only way to go build software. This is just an approach that I really like to use because it really aligns with like my way of thinking about systems. And um I guess from using it a lot, I find that it lets me build um extensible systems. And again, it doesn't mean that is the way to build the most extensible systems or that I figured everything out. It's just like this seems to work really well for me.
So I I gravitate towards it. Okay. So generally the way that my applications look is I have a very light entry point. So I usually keep the entry point of an application very light so that I could uh in theory like swap out to other things. And that might seem kind of silly like if I said hey I'm building a a web API. You might say like why do you care if the entry point is thin? Like it's only ever going to be a web API. Like who gives a Totally fair. Um, the idea though is that if I push a lot of my logic into plugins, literally with the web API example, I've done something where I have swapped out the entry point to be a console application and then I basically have a locally running um sort of debug console that that doesn't need to serve HTTP traffic.
I can actually call into my um sort of locally running services directly completely avoiding anything to do with HTTP. So like do I need to build everything like that? Absolutely not. But it's like one of the nice side effects of structuring things this way is I kind of I get that ability. Um some other examples, right? Uh what's a good one? I did something at work uh like few months back like some vibe coded thing basically I was using some time to go work with co-pilot CLI and I was just trying to like vibe code a a solution to like a I don't know like a simple solution to a a regularly occurring challenge I have and so uh I I built this thing and I said from the start working with co-pilot I want you to uh I didn't actually use plug-in architecture, but I I was pretty explicit that I wanted like an extremely thin entry point.
Kind of told it some of these constraints that I like thinking about when it comes to plug-in architectures. So like very separate domains. I want to make sure that my entry points thin um I don't have business logic for domains bleeding across each other. Um, and even like if you think about a layered architecture, I don't have uh those domain concepts sort of bleeding through the layers just really isolating uh different parts. And so again, like why does that matter? Well, I knew that if it ended up working, like I don't think I'm necessarily building the um I'm not building like the the final application that I know I'm going to want. That sounds kind of silly. I'm kind of like experimenting with it and I'm like, hey, if it works though, I don't want to toss the whole thing out. It would be cool to take most of it and like I don't need um a console application.
So in this in this simple example, I started with a um if if you're a .NET developer, there's something called Spectre console, which gives you like a I don't know, like a pretty nice like graphical terminal to work in, which is pretty cool. So I was like, I'm going to make a tool that can, you know, it's nice to use. It's on the command line or like in your terminal and uh it's not just like run it from PowerShell or Bash or something and it's a one-off command. And it's like you get to interact in this little terminal and know and so once things started working I said that's cool but like that's sort of the end of life already. I'm like I don't uh I don't need it to be an actual terminal app. And this light just went green and red and could not even get into the intersection.
And so what was really cool about that though was that I was like, hey, if I want to run this programmatically, this sort of graphical terminal is kind of awkward, but I can literally swap out the entry point and keep like 99% of the code and just put a normal um you know console entry point on top of it. So it can be run programmatically very easily. And then the next extension of this was like, wait a second. Um, for a little bit of extra context, I started to build in like an LLM harness into it. And I was like, wait, this is so dumb. Like, if I just make this thing an MCP server, then I can literally use C-Pilot as the harness. And so I completely inverted it. I switched the entry point to be an MCP server. And again, I could keep like almost all of the code unchanged and just swap the entry point to be an MCP server.
So I I find that, you know, it's not that we have to build the uh things this way. It's not that um it's not that building things this way is always going to give me awesome ROI, but I do find that because I because I do build things these way uh this way and I am familiar with it and I'm used to it that it's not a lot of extra effort for me to do like it's it almost feels like muscle memory. It's just the natural way to do it. And so these are some cool side effects that I get from building systems this way. Of course, if I were on a team and someone said like, "Nope, we're not going to build it that way." Like, "Here's reasons we should go use whatever else." I would be absolutely open to that. So, coming back to plug-in architecture, the way that this ends up looking for me is thin entry point for the most part, right?
Uh, keep it very thin. It's really responsible for um like like effectively just loading plugins. almost everything is a plugin beyond that and then when I have to have crossplugin communication because the more complex the application gets realistically it's not like every feature just works perfectly in isolation it's kind of rare that an application is totally like that especially when it's plugins top to bottom so there's these situations where like I have what what is essentially an SDK K. And so other features can interact with other plugins by going through the SDK. And having this layer for me is really nice because what it means is that if I want to um we talk about things like uh rewrites versus refactors or if you're familiar with like a strangler fig approach.
If I wanted to start rewriting my my service or my application or whatever, what's really cool is that I can literally take a plugin and I can start rewriting a plugin and I have a dedicated, you know, chunk of code that I can I can totally hot swap out. Like in theory, that's the the idea because it's so isolated. Obviously, if I implement it poorly, then like anything that uses that functionality is going to be busted. But the point is that uh from a code perspective, because I put up these boundaries, it it truly lets me have like very clear spots to go replace. They can't depend on each other, and so they go through this SDK in order to accommodate that. Now, what's nice about the SDK too is that, for example, if I'm dealing with a monolithic uh web service, um I can have all of my features that you might look at and say, "Hey, that could be a micro service." If I have them as their own plugins, they are isolated.
They're all set up in a way that's modular and isolated and they go through this SDK. the SDK like because you have this uh this interface, you can do that totally in memory with function calls like normal, right? So there there's literally like no messaging overhead. It's all in process. It's it's just a a logical boundary through a a C interface. And if at some point I said, you know what, like for whatever reason, this particular functionality needs to exist as as its own service. In theory, what I should be able to do is take the plugin itself, right? I'm not saying it wouldn't change at all, but I could take the plugin itself, pull it out somewhere else, put a new entry point around the plugin. Right? So now I have like a dedicated process. hopefully largely unchanged, just this new entry point on top.
And for the SDK, instead of it having a a function call like normal, the SDK would provide the callers with the interface and the implementation would be whatever I want like whether that's like a gRPC call, whether that's an HTT like a restful call, whatever. That's kind of an implementation detail of the SDK. So it lets me split things out nice and easily too. So that's kind of the purpose of this SDK layer and how I'm able to have parts of uh my my plugins in terms of features not directly depend on each other but they can uh they can call an SDK that like has some interface for for some behavior. It's just that the implementation is decoupled, I guess, is what I'm trying to highlight. So, more recently, what's been happening is this kind of going to go over to some tests, I guess.
So, for a little bit of extra background, um, when I talk about testing, I'm I I write code in a way that I feel like is mostly very unit testable. And what I mean specifically by that is like if I have a class, there are interfaces that are passed in as dependencies and I'm able to mock those out. If I need to separate my concerns from the dependencies, especially if that dependency is truly like an external system, I can mock it out and just isolate what I'm looking at uh and interested in testing. So I write code this way. It doesn't mean that all of my tests just have to use mocks. just gives me the ability to leverage that if I need to. And so I've always, not shouldn't say I've always, but um for a long time I've written code this way. And at one point in time I really did lean much more into like let me mock it and keep it isolated.
And so I still write code this way, but I have shifted a lot to like let me only mock the things that are like external systems I don't have control over. So like if I have to do a web request to some third party service, I'm not going to put that in my unit test. I don't want that running all the time. Like that to me doesn't make a lot of sense. Let me isolate it. And because things have evolved over time in in a really good way, like we can spin up databases and have, you know, a local database running and the schemas dropped in like all this cool stuff like super fast. And I guess like when I think about, you know, early on writing tests, like that was such an unrealistic thing that kind of like you're the battle was like if you need to use a database, like it's not going to go in like a in a coded test that's running on every build.
There's no way. But now we can. So, a lot of my arguments for like I really need to be able to to use mocks really is only for like third party systems, that's when I'll do that. If I have a database, I don't mock databases at all. Now, um often like if I have a SQLite setup, then I will usually use like an in-memory SQLite database. But otherwise, like I have a I have a real database running and I I test against it. doesn't have to be a production database, but I mean there is one running. So I've structured my tests in a way for a lot of my recent applications so that they are very much like functional tests. And this is totally fine when we take these features in isolation like these plugins because it's really just the domain that we're working in being tested.
But what like I don't want to call it a mistake but like one of the things that I had done over time was that going back to like features that know about each other they don't have a direct dependency on each other but they use each other through an SDK what ends up happening is that as I make more and more plugins and more and more of them get dropped into the SDK it means that the surface area like if you want to use the SDK and have it fully functional Now all of these plugins are basically a direct um dependency of the SDK. So that means to test one particular feature right in in its own set of tests if that if that uses the SDK now all of a sudden I'm bringing along all these other uh these packages and uh assemblies and
like it hasn't been an issue until it became an issue and what I mean by that is like even with because everything is a plugin I have like hundreds of projects And that's not so bad. It's just that in hundreds of projects, sorry, like including the the test projects that are associated with the individual project. It ends up becoming a really big problem when like one of those things has an external library. Uh, and for whatever reason it has unmanaged DLS or some other resources and now that's like a I don't know it's a 10 megabyte drop and I have I don't know like 60 test projects. So now your 10 megabyte drop times 60 test projects ends up being like over half a gig just for a dependency. And so I'm I kind of reach this point where I'm like, "Oh shit." Like this convenience of of having this SDK layer as a um as a facade to all of these other dependencies.
That's actually starting to bite me in the ass now because the whole point was to keep, you know, uh, isolation, to keep separation. But what's actually happening in practice is that I end up depending on all of these things being brought in anyway. And of course, like there there's ways around this, right? Like I could, and this is kind of where I'm I'm getting to, is like couple different levels here. The first was that I said, "Okay, you know what? like for some of these um for some of these features that depend on a really heavy dependency, right? Um something that's and I can kind of draw the line wherever I want, but I can make the cut and say, you know what, if you have to depend on this thing, I basically need an adapter project. Yes.
So one more project and that will be the thing that brings in the dependency and instead of having this you know if you want to use my feature instead of it pulling in some really heavy other dependency along with it I can actually say like you can use my feature and it comes with a default implementation that uh that could be like a noop implementation it could be like a an implementation that throws exceptions whatever I get to decide and then if you want to use the real thing like you use my adapter project alongside it. And so if we think about like inversion of control I can say you want to use my feature cool add that as a dependency and if you want it to work sort of properly use one of these adapters and that will let you inject real behavior for uh like using one of these heavy dependencies.
So to give you an example, um I have something where I needed a headless browser. All right? So one of my feature plugins needs a headless browser. And so the side effect of that is if I add one of these packages, it's like a a playright wrapper or something like that. All of a sudden I have megabytes of of uh assemblies being pulled in. And that's not okay if it's going to be shared across like 60 test projects where maybe even none of them actually use it. it's just pulled in because this SDK now instead I can say cool you can all depend or have a a reference to my my feature project but only those that actually need playright enabled can turn on the adapter that pulls in the heavy dependency. So this is like one mechanism I'm introducing to kind of give more granular control.
So that way like my entry point can add the adapter. If I have dedicated tests that really need to exercise that they can add the adapter. That's sort of one mechanism I'm I'm working with. And then the other is kind of like I think I need to go back to the drawing board on some of my tests because many of them the reality is many of them don't actually need like the full SDK with like cross feature interactions. Most of them don't need that. Oh, that is an ambulance coming up. So what they actually need is they need uh some common infrastructure like they need access to the database. Okay, I bet you if I were to clean that up from the testing perspective, I bet you that would address 80% of my sort of extra baggage coming in. I could just dedicate, you know, those references to just pulling in some database stuff.
And I think there's there's probably a couple other scenarios where I genuinely want to test cross features and I think those are very valid tests. I think that's totally fine. But I should make like a pattern dedicated to that. That was weird. That ambulance had its lights on. It was flying and now it's not. So, okay. So that latter part is still something I'm exploring because now I have a a good example of a lot of test projects that I need to go back through and try to clean up. Some of them will have just to give you an example, some will have like uh API tests. So truly for this feature, it's like running it top to bottom. you call like I literally will make HTTP requests locally for a server that's running and that server is literally the full server. It's like exercising the entire real topology.
So it has that and um I'll have tests like that that exercise things end to end. So real server, real database, cool. And then I have maybe the majority of the other tests that just don't need that. they might need access to the database, but they're very much isolated to just the feature. And I I'm thinking that I can probably find a good logical separation and then apply that pattern uh to the codebase and I would chip away at it. So, it's kind of in terms of like my my plug-in architecture views evolving, uh this adapter pattern, I think is something I'm going to start to use more. It's not new, like I didn't invent it, but uh really leaning on some inversion of control to say like you provide me um the implementation wrapped in the interface I want and I'm happy to go use it.
So I'll do that a little bit more and then some of my I don't know like test setup, test organization I want to address. So that's the plug-in architecture stuff. Uh the other thing I wanted to chat through is around Ralph loops. One of the things I was talking about in a recent video was trying to get uh I don't know like a a local repo setup that lets me kick off Ralph loops and play around and stuff like that. Um and where I was headed with that was I wanted to have some type of orchestration layer on top of that so that I could say, "Hey, like let me go, you know, I have an idea.
Let me go assign some work." And that way as as the Ralph loop is running like can I assign more work can I have other agents work on things also in Ralph loops and so I started this and it didn't finish but I had started and then I had this is like how I get all of my my updates uh my Google news on my phone I was like scrolling through and it's like oh open AI announced this thing called Symphony. Like what's what's that? And so apparently Symphony is basically just this thing I'm talking about. uh not specifically for for Ralph uh but basically how do you they came up with like a a a spec that's uh that's open and they have a reference implementation in Elixir but they have this huge spec for like how do you create an agent orchestration system
and so I was like oh my god like I basically was trying to start from scratch building something with co-pilot to do this which I know is going to be like I know other people are doing this kind of thing. I said that in the the other video. Uh I'm not obviously inventing this for the first time and I'm probably not going to build it the best like if we're being honest, right? It's like I'm just interested in trying it out. I want to explore it. And so uh I shifted gears a little bit. I just kind of went back to my co-pilot session and I said, "Hey, like here's here's this thing called Symphony. here's this spec they have and like how do we start aligning what we started building to this spec and so I don't the spec is huge it's like I
can't imagine giving this to an LLM and saying like just go build this because it is so huge like it's not you're not going to pull that into context and just hope it gets the thing right as like a one shot there's no way um So, you know, I'm kind of just trying to to work through it and say like how do we start moving some of our patterns in that direction and it was kind of interesting because like they're I think like rightfully so like opinionated about some of the things they try to be open but also opinionated in some ways. So for example, they said they have a reference implementation in elixir, but like the language doesn't matter, right? So that's fine. Um, but what I noticed go like having co-pilot kind of read through the spec versus what we have, it's calling out uh gaps and differences and that's actually highlighting where they are opinionated.
And so I'm trying to take the opportunity where you know co-pilot saying like hey spec says we got to do it this way. I'm kind of looking at that going like I I can understand why that's a way to do it but like does it need to be that way? And I want to think of a good example here. So like um if we think about something supervising agents that's that are doing work right like if that like like in real life if you're doing work and you have all of this work kind of cut out with dependencies you might be doing work and reach a point you go oh crap like we made a wrong assumption here like this doesn't actually work this way. So you could follow a pattern that's like let a supervisor um so something like that's overseeing the orchestration. let it make a decision about what to do, right?
And that might mean, like, just to give you an example, that might mean the orchestrator says, you know what, all of the downstream dependent work, cancel it. Just stop it right now because it's a waste. Like we are going to pivot. And then the orchestrator is allowed to stop everyone and then create like on its own create the new um sort of downstream dependencies and go uh keep moving along. That's that's like one implementation of how you could do that and that might be the one you want. That might be the best one. I don't know. My point is that that's one way to do it. And my other point is I don't know what the best way is. And I also don't think that the best way is always the best way.
So my thinking is like as I'm working with co-pilot going through this spec every time it's like hey they say to do this I'm trying to challenge it and I'm saying like hey can you make a level of abstraction here right so we talked about a supervisor that's able to do this autonomously cool what if I don't want that I want a human in the loop so if something goes wrong and our dependencies aren't right I want that to let me know. I want to make a decision about that. All right. Maybe again based on the implementation maybe it's like hey stop the presses and instead of let me go make you know uh the new plan automatically and start it. Maybe I want it to stop right there and then let me know. I'll make a decision and then we can go start the sort of the new plan from there.
That is another implementation. Maybe you allow um you know the worker that was doing the work to to have some responsibility in like in what should happen. I don't know like those are all different ways to do it. I I think like with everything there's pros and cons. So I have a I struggle a lot with like you know just do it this one way. This is the best way. So just that's how it is. So I'm like I I don't know like it might be the best way statistically, but if there's ever situations where we want something different, I would love the ability to have something different. So my focus going through this is really um again, it's not am I going to build the best system better than anyone else ever has. Like no, I'm not going to. There's literally companies that like do this.
Uh and I'm just a dude. So, um, my thinking is like I want to be able to explore and like look what those look at what those, uh, abstractions look like. For me, that's like that's kind of the exciting fun part. And if I get it working, like that's extra cool. But, uh, I actually really enjoy thinking about like what these abstractions look like. I don't know why. It's just like something that's enjoyable for me. So, I'm playing around with that. Um, I think the last time I checked in on it, uh, it was saying, oh, uh, I think it was kind of like, hey, man, you, if you're making a left-hand turn, get into the middle lane, not halfway. Come on. It's crazy. It's literally a center turning lane.
Uh the last time I checked in on it, it was saying that it was at a point where it could finally like it had test to prove that it could cut over from the old implementation to something that's closer aligned to Come on, man. Come on, the most outrageous this person. I'm turning left. Let's explain some some driving. I'm turning left. Someone coming the other way decided just to stop. They don't have a stop sign. And then there was a person trying to leave the parking lot. Who's trying to turn right and they're like, I don't think I can go because this person who just stopped clearly has the right away, but they're not going. So that was super awkward. They finally went, but so yeah, I think that's a a cool thing. I'm excited to to play around with. Um I have no idea if it will ever make anything that's remotely close to useful, but kind of neat.
And I want to have it so that it can run locally. So like you don't need to hook it up to a a remote git repo or Azure DevOps or anything. But then I also want a mode where like I can absolutely point it at a remote repo and say like I'm making git issues like just start taking them as they come in. I'm I'm excited to try something like that out but I have no idea if it'll I don't know like ever work like that. So we'll see. But anyway, that's my update. Thanks for being here. Like I said earlier, if you got questions about software engineering and career development, leave them below in the comments or go to code.com. It's totally anonymous and I'm happy to try making a video response.
Frequently Asked Questions
These Q&A summaries are AI-generated from the video transcript and may not reflect my exact wording. Watch the video for the full context.
- How do you enforce boundaries between plugins to avoid cross-dependencies?
- I like having plugins to give some forced separation. I make sure my plugins don't depend on each other; a feature plugin shouldn't reference another plugin. If someone tries to create such a dependency, I can catch that early with tests and prevent it during development.
- Why do you keep a very thin entry point in your applications?
- I keep the entry point extremely thin so I can swap it out and reuse the same code behind it. For example, I started with a console/terminal entry point, then swapped to an MCP server to host an LLM harness, keeping almost all of the code unchanged.
- What pattern do you use to manage heavy dependencies in plugin features?
- I use an adapter pattern with inversion of control. If a feature depends on a heavy dependency, I provide an adapter project that brings in the real implementation; otherwise I ship a default/no-op adapter. This lets you opt into the heavy dependency only when needed and keeps the rest of the system lightweight.