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:

  1. Following Clean Architecture principles to the letter
  2. Using Test-Driven Development (TDD)
  3. 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

  1. Separation of Concerns: The domain logic knows nothing about Ktor, Mustache templates, or markdown parsing. It’s pure business rules.

  2. Testability: Each layer can be tested in isolation. The use cases don’t need a web server to be tested.

  3. 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:

  1. Add failing tests for everything; TDD for the win!
  2. Implement the markdown parser
  3. Add proper template rendering
  4. Add RSS feed generation
  5. (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!