Let’s make a blog engine, ey? Link to heading
Don’t get me wrong - Hugo (the stuff that I’m currently using to drive this site) is great. It’s blazing fast, feature-rich, and battle-tested. But as a developer who’s been diving deep into Clean Architecture lately (as you might have noticed from my recent posts), I’ve been itching to apply these principles to a real project. And what better way to learn than by potentially over-engineering my own blog engine?
The Plan Link to heading
Rather than just throwing together another quick web app, I’m attempting to build this the “right” way:
- Following Clean Architecture principles to the letter
- Using Test-Driven Development (TDD)
- Keeping the codebase screaming its intent
The Architecture Link to heading
Following Uncle Bob’s Clean Architecture principles, I’ve structured the project into clear layers:
src/main/kotlin/io/github/cekrem/
โโโ domain/ # Core business entities and rules
โ โโโ model/ # Content, ContentBlock, ContentType, etc.
โโโ application/ # Application-specific business rules
โ โโโ config/ # Blog configuration (title, description, url, menu items etc)
โ โโโ gateway/ # Port interfaces (ContentSource)
โ โโโ parser/ # Content parsing interfaces
โ โโโ usecase/ # Application use cases
โโโ adapter/ # Interface adapters
โ โโโ presenter/ # Presentation interfaces
โโโ infrastructure/ # External frameworks and tools
โโโ contentsource/ # File, Mock, and RSS implementations
โโโ factory/ # Dependency injection
โโโ parser/ # Markdown parsing implementation
โโโ web/ # Ktor setup, routes, and templates
Domain Layer Link to heading
The domain models are pure Kotlin data classes, representing the core concepts:
data class Content(
val path: String,
val title: String,
val blocks: List<ContentBlock>,
val type: ContentType,
val metadata: Metadata,
val publishedAt: LocalDateTime? = null,
val updatedAt: LocalDateTime? = null,
val slug: String = path.split("/").last(),
)
Application Layer Link to heading
The application layer contains use cases that orchestrate the domain logic. I’m using the UseCase
interface pattern I discussed in A Use Case for UseCases in Kotlin:
interface UseCase<in I, out O> {
suspend operator fun invoke(input: I): O
}
class GetContentUseCase(
private val contentSource: ContentSource
) : UseCase<String, Content?> {
override suspend fun invoke(input: String): Content? =
contentSource.getByPath(input)
}
Interface Adapters Layer Link to heading
The adapters layer converts data between the formats most convenient for the use cases and external agencies. For this blog engine, the main adapter is the content presenter:
interface ContentPresenter {
fun presentContent(content: Content): Any
fun presentContentList(contents: List<ContentSummary>): Any
}
This layer ensures that our domain and application layers remain clean and independent of any presentation concerns. The Any
return type provides maximum flexibility - implementations can return HTML strings, view models, or any other format needed by the infrastructure layer. This approach perfectly aligns with the Clean Architecture’s dependency rule while keeping our options open for different presentation formats.
Infrastructure Layer Link to heading
The infrastructure layer handles the technical details of web serving (using Ktor), content storage, and template rendering:
fun startServer(
getContent: GetContentUseCase,
listContents: ListContentsByTypeUseCase,
getListableContentTypes: GetListableContentTypes,
contentPresenter: ContentPresenter,
config: ServerConfig,
) {
embeddedServer(Netty, port = config.port) {
// ... Ktor setup
}.start(wait = true)
}
Why This Matters Link to heading
Separation of Concerns: The domain logic knows nothing about Ktor, Mustache templates, or markdown parsing. It’s pure business rules.
Testability: Each layer can be tested in isolation. The use cases don’t need a web server to be tested.
Flexibility: Want to switch from Mustache to another template engine? Just create a new presenter implementation. Need to change how content is stored? The
ContentSource
interface makes that easy.
Current Status Link to heading
So far I’ve only got the basic scaffolding in place. The next steps are:
- Add failing tests for everything; TDD for the win!
- Implement the markdown parser
- Add proper template rendering
- Add RSS feed generation
- (Implement beyond MVP features as needed)
Is This Over-Engineering? Link to heading
Probably! But that’s not necessarily a bad thing when the goal is learning. As The Pragmatic Programmer reminds us, sometimes you need to go too far to find out where “too far” actually is. I’d rather go to far now and learn from it.
The real test will be whether this ends up being more maintainable and adaptable than Hugo for my specific needs. At worst, I’ll have learned a lot about Clean Architecture in practice. At best, I’ll have a blog engine that perfectly fits my needs and is a joy to extend.
Next Steps Link to heading
I’ll be documenting this journey as I go. The next post will likely focus on implementing the markdown parser while maintaining our clean architecture principles. But I’ll write some failing tests first, and do go full TDD Stay tuned!
“Architecture is about making it clear what the application does by looking at the structure of the code.” - Uncle Bob
Check out Uncle Bob’s Clean Architecture book if you haven’t already. It’s a great read and a good reminder of what we’re trying to achieve.
The code is available on GitHub if you want to follow along or contribute. Just remember - this is very much a work in progress!
Disclaimer Link to heading
I can’t promise I’ll add to this project weekly, there are other things I’d like to write about as well. Stay tuned in any case!