Arjan delivers a much-needed reality check against the cargo cult of over-engineering, reminding us that abstraction is a cost rather than a virtue. True architectural maturity lies in the restraint to keep code simple until the pain of coupling genuinely outweighs the overhead of a pattern.
Deep Dive
Prerequisite Knowledge
- No data available.
Where to go next
- No data available.
Deep Dive
You Don’t Need That Design Pattern
Added:Here's Python script that summarizes text that uses an LLM and it also has a couple of design patterns. For example, there's a prompt strategy, which is an abstract base class. It has a build method and then we have concise prompt strategy and a bullet prompt strategy.
And then we also have a factory that is going to create these objects for us depending on the style of summarization that we want. So, that's a whole bunch of classes to basically build a string.
We don't always need design patterns.
I'll show you how to simplify this, but then I'll also show where a design pattern in this case actually makes sense and it's different from what we're seeing here. Now, writing great software is not about memorizing design patterns.
It's about understanding when abstractions actually help and when they make your code harder to work with.
That's exactly what I go deep into in software design mastery. This is not just another course with patterns and principles. It's a system that guides you through the fundamentals of software design and teaches you how to make high-level decisions while AI helps you write the actual code. If you want to be part of that, join the waiting list at iron.codes/mastery.
The link is in the video description.
Now, the problem with this code is not that the strategy pattern is bad. It's that the code does not really need all this structure yet. There's just a little bit of variation in this code.
There's one uh prompt string uh that's selected by a style name, but uh in this particular solution, we already have like a bunch of abstract base classes, uh strategies, and factories, and all sorts of other things. It's fine if you know beforehand that you're going to need like a million different prompt styles or whatever, but in this case, every abstraction has a cost. And in this case, the cost is just a bunch of added complexity. The way that all these classes are used is that we have a couple of functions here, one to call an LLM, uh one to split text into chunks.
Uh there is a summarize document function that uses all of these things or builds the factory and then creates a prompt strategy. And that's the main function where we have some document text and it summarizes the document.
When I run this, then this is what we get. In Python, functions are objects.
That means you can pass them around, you can store them in dictionaries, you can test them directly. So, for lots of cases when you write code and you need to add complication, functions are pretty powerful. For example, here what you could do instead of this entire hierarchy of classes, we could also create two functions that each represents a different prompting strategy. So, we would have a concise prompt and we have a bullet prompt.
And then I'm also going to delete this prompt strategy class. And instead of the factory, we can use a dictionary.
And this dictionary is a mapping from string to one of these functions. So, let's call that a prompt builder function.
And let's also define an alias for that.
So, this is a callable that takes a string and returns a string.
And then inside this dictionary, we have the concise prompt builder and the bullet prompt builder like so.
We also don't need this anymore, but we can now replace it with a function build prompt.
And this is going to get style and text.
And it will also return text. And then what we can do is simply return Let's actually call this prompts. That's better.
We're going to pass the style.
And we're going to call that with the text. Now you could wrap this around a try except block if you want and then turn the key error into a value error that would be a bit better error reporting. But for now, this is fine.
And then what to use that, the only thing we need to do is go to our summarize document function and we don't need this anymore.
But then here instead of the whole strategy process, we simply call build prompt and we pass the style and the chunk. And same here, this is going to be build prompt and we're going to pass the style and the text.
Like so. Let's run this again and you see we get exactly the same results. But now our code has become way simpler because we just have a couple of functions, no abstract base classes, etc. It's still designed well, it's just a lot smaller. What's also nice about this approach is that actually now our summarize document function becomes shorter. We can see exactly what is happening here. We have a splitting into chunks, then we create the summaries, and then finally we create a full document report. It means that because of our simpler design, it's easier to see what this code is actually doing. So that's all great and you might say, "Well, that's kind of obvious that you can remove this. You always do that, right? You you start with a bunch of classes and you replace them by functions. Yay, well done." But the interesting thing here is that there are other aspects of this code that actually do merit some design improvements and that perhaps do merit using a class with some abstraction. For example, if you take a look at how the LLM is actually called, so that's currently happens with call LLM. So, that's fine and it returns this response which is not a big deal, but the problem is of course that in real life call LLM is probably pretty complex, especially if you use different LLM providers. The SDK is going to be different, the response formats are going to be different, there might be different token usage field, the context limit might be different. And that's a problem for example because well, basically summarize documents just chunks the text now by using a maximum of number of tokens that's just a magic number. So, that means that at the moment this code is not very flexible.
So, in essence the summarize document function should know any details about how or about what LLM is being used and what the constraints are of that LLM. In order to support that, let's introduce some abstraction and I'm going to use a protocol class for that.
So, let's say we have a class LLM client which is a protocol and that is going to have a couple of helpful methods, properties optionally that deal with what a particular LLM client needs to provide. So, that's going to be a generate method that's going to get a prompt.
And this is going to return the LLM response. So, that means that our clients, our implementation of the clients, they're going to be responsible for translating for a particular specific LLM into these LLM responses that then our text summarization knows about.
But since we now have a class, we can add other useful things here that are related to LLMs.
Like let's say the tokens.
Or, we might want to know a max context window.
And of course, you can add more functionality here as well. So, the class is not just a simple wrapper around a method or something. It's really an abstraction of what an LLM actually provides and what the summarization workflow actually needs, which is being able to generate text, counting tokens, knowing things like the context window limit, and getting back normalized response data. And once you have this class, we can provide a simple implementation.
So, let's say I create a fake LLM client. There's no inheritance relationship because of duck typing. And then we're going to have these three functions. And what we can do is basically move the bodies of these functions that we already have right here.
And this is then where we can also put our magic number.
The only thing we need to do now is fix the summarize document function. And because we're now dealing with a client object, we can use dependency injection.
And as you can see, I'm specifying the protocol here. So, that means that anything that implements the protocol, I can use together with my summarize document function. And now, instead of hard coding this data here, I can use the maximum context window.
And for example, cutting that in half just to be sure.
Then here, instead of doing call LM, I'm doing LM.generate.
And same thing here. And finally, the only thing I need to do in my main function is create the client.
So, let's use our fake LM client and simply pass that as an argument to the summarize document function.
Like so.
Let's run this code one more time. As you can see, the result is exactly the same. But now we introduced a useful abstraction, which is that all of the LM stuff, well, we're going to put that behind an LM client interface that our summarization function then uses. Now, for specific types of LM providers, we can simply implement a class and we don't need to change anything in summarize document. What's interesting while I'm doing this refactor is that now actually, secretly, we have introduced a design pattern, namely, the adapter. The adapter pattern basically means that you create a single interface and then you create implementations of that interface that adapt a specific service or tool to that interface. And that's exactly what we're doing here. We have our LM client interface, but we can now create an adapter for each of the different LM providers. For example, let's say that next to this fake LM client, I can have another client called the OpenAI client, which has different settings. Maybe you want to pass an API key here as well. The generate method has a different implementation. Uh the max context window is different from our fake LM client. And it's really easy to change this now because in my main function, the only thing I have to do is I have to create my OpenAI client instead of the fake LM client. And then, when I run this again, you see now we get an OpenAI summary of the text. Well, not real. I mean, I'm kind of faking everything here, but uh you get the idea. The most important thing to remember from this is the order in which this happens. I didn't start with, "Hey, what pattern should I use?"
or "I want to use the adapter pattern."
I started with the analysis that in this particular case we have provider-specific SDK details and they're being used in functions that should not need to know about those things. And then by refactoring the code and introducing a solution to that, the pattern appeared. And it's not a classic object-oriented implementation of the pattern, either.
I'm using protocol classes and structural typing. So, I'm using the natural features of Python to achieve this goal. But, we do end up with a design pattern, namely adapter. But, it appeared naturally. It We didn't think about this beforehand. Finally having a good abstraction like this instead of just a bunch of random strategies and factories, it actually makes testing a lot easier. So, that's a good way to verify that what you're doing is actually going in the right direction.
For example, if you want to write a test for this, well, then you simply import that summarize document function, you create a fake client that you're just going to use for testing, and then you can create your test summarize document function, and then do some asserts to verify that it's actually working correctly.
And then we can run that, and it's very simple and straightforward to write test for this code. It means test writing is basically boring, which is good because that means that AI can probably write most of those tests for you, so you don't have to do all that work. By the way, if you enjoy videos about writing simpler, better Python code, subscribe.
Give this video a like. That way I know I should make more content like this.
So, rule of thumb is to not start with patterns immediately. Look at where there's too much coupling or where a function tries to do too many different things and start simple. You can already start by moving a couple of things into separate functions and then introduce abstractions if they really reduce coupling and they tie in with what you expect to change in the future.
And then design patterns will naturally appear. But, I'd like to hear from you.
Do you think about design patterns at all when you write code? Does AI introduce design patterns and do they make sense in your experience?
Let me know in the comments.
Now, YouTube thinks you might like this video next. Thanks for watching and see you next time.
Related Videos
LBF101 Creating an XML Changelog
liquibase7511
3K views•2026-06-15
Alta Labs Cloud Dashboard Real time Network & Xnet Insights!
ShinyTechThings
158 views•2026-06-17
Wait... Group Policy Not Applying? Check This First!
keeplearning_iT
144 views•2026-06-15
Leetcode Weekly Contest 506 | Life's boring these days
Pudeesht
2K views•2026-06-14
microJAM: MAKING A MICRO GAME FOR A GAME JAM IN CLOJURESCRIPT AND TOTALLY NOT C
janetacarr
156 views•2026-06-18
Partitioning vs Bucketing vs Clustering: How to Make Queries 100x Faster
thedataandaiguy
194 views•2026-06-16
Design Claude Code Like a Senior Engineer
hayk.simonyan
344 views•2026-06-19
Linus Torvalds: AI Won’t Replace Understanding Code
SavvyNik
140 views•2026-06-19











