In the search for a simple, self-hosted commenting solution, one developer embarked on a technical journey through three popular platforms, uncovering a landscape of over-engineered architectures, inconsistent design patterns, and questionable implementation decisions. What began as a quest for functionality turned into a revealing examination of the current state of software development practices.

The developer's exploration began with Remark42, a project that presented a stark contrast between its frontend and backend implementations. "If the front end is poorly implemented then should I trust the back end?" the author questioned, noting the frontend's excessive Redux boilerplate and over-modularization.

The frontend was started circa 2019 while IE 11 was still an issue, so React-like framework was fine. But even for React you don't usually deploy via find . -regex '.*\.\(html\|js\|mjs\)$' -print -exec sed -i "s|{% REMARK_URL %}|${REMARK_URL}|g" {} \;

Remark42's backend, however, earned praise for its solid implementation, particularly its use of BoltDB for hierarchical data storage—a choice that aligns storage technology with data model rather than forcing a mismatch. The author noted the backend's rock-solid nature despite initial skepticism about its 140 dependencies.

Moving to Artalk, the developer encountered a project that exemplifies the "feature-first" approach to software development. With support for "all the databases, all the caches, comment moderation, captchas and other anti-spam, visitor stats, and god knows what else," Artalk represents a comprehensive solution that comes at a cost.

The dependency count ballooned to 430 Go packages, including substantial libraries like aws-sdk-go. Even after removing Redis, Memcached, and Swagger, the binary size remained substantial at 43MB—down from 62MB. The author noted that the original binary "contains self-hosted API docs, whether you want it or not (you cannot disable it) — that's how deeply they went into 'having all the features'."

Code samples revealed architectural inconsistencies:

;(() => {
  // Init event manager
  EventsService(ctx)

  // Init config service
  ConfigService(ctx)
})()

The author questioned the purpose of this Immediately Invoked Function Expression (IIFE), suggesting it serves no functional purpose and represents either "neuro or human slop." Further examination revealed a pattern of duplicated architecture:

export interface EditorOptions {
  getEvents: () => EventManager
  getConf: () => ConfigManager
}

export class Editor implements IEditor {
  constructor(opts: EditorOptions) {
    this.opts = opts
  }
  getPlugins() {
    return this.plugins
  }

  setPlugins(plugins: PluginManager) {
    this.plugins = plugins
  }

  setContent(val: string) {
    this.ui.$textarea.value = val

    // plug hook: content updated
    this.getPlugins()?.getEvents().trigger('content-updated', val)
  }

  submit() {
    const next = () => {
      this.getPlugins()?.getEvents().trigger('editor-submit')
      this.opts.getEvents().trigger('editor-submit')
    }
  }
}

The critique highlighted a mix of dependency injection and ad-hoc plugin management, with "duplication in the architecture: first dep-injected service locator queried via this.opts.getEvents(), and second ad-hoc this.getPlugins() to consume this.setPlugins() value in violation of the previous dep injection."

The server-side implementation raised even greater concerns:

// Delete all foreign key constraints
// Leave relationship maintenance to the program and reduce the difficulty of database management.
// because there are many different DBs and the implementation of foreign keys may be different,
// and the DB may not support foreign keys, so don't rely on the foreign key function of the DB system.
dao.DropConstraintsIfExist()

The author expressed disbelief at the decision to drop database foreign keys entirely, noting that this approach to data consistency management was fundamentally flawed:

func (dao *Dao) MergePages() {
    // merge pages with same key and site_name, sum pv
    pages := []*entity.Page{}

    // load all pages
    if err := dao.DB().Order("id ASC").Find(&pages).Error; err != nil {
        log.Fatal("Failed to load pages. ", err.Error)
    }
    beforeLen := len(pages)

    // merge pages
    mergedPages := map[string]*entity.Page{}
    for _, page := range pages {
        key := page.SiteName + page.Key
        if _, ok := mergedPages[key]; !ok {
            mergedPages[key] = page
        } else {
            mergedPages[key].PV += page.PV
            mergedPages[key].VoteUp += page.VoteUp
            mergedPages[key].VoteDown += page.VoteDown
        }
    }

    // delete all pages
    dao.DB().Where("1 = 1").Delete(&entity.Page{})

    // insert merged pages
    pages = []*entity.Page{}
    for _, page := range mergedPages {
        pages = append(pages, page)
    }
    if err := dao.DB().CreateInBatches(pages, 1000); err.Error != nil {
        log.Fatal("Failed to insert merged pages. ", err.Error)
    }
}

The approach of loading all data into memory, deleting everything, and then recreating records represents a dangerous pattern for data management.

Finally, the developer examined Comentario, which presented yet another set of architectural challenges. While the author expressed disdain for Angular's constraints, the server-side implementation revealed a different kind of over-engineering:

Services.CommentService(nil).Create(c);

This call initiates a complex chain of abstractions:

type ServiceManager interface {
    CommentService(tx *persistence.DatabaseTx) CommentService
    ...
}

// dbTxAware is a database implementation of persistence.Tx
type dbTxAware struct {
    tx *persistence.DatabaseTx // Optional transaction
    db *persistence.Database   // Connected database instance
}

// dbx returns a database executor to be used with database statements and queries: the transaction, if set, otherwise
// the database itself
func (d *dbTxAware) dbx() persistence.DBX {
    if d.tx != nil {
        return d.tx
    }
    if d.db == nil {
        panic("dbTxAware.dbx: db not assigned")
    }
    return d.db
}

func (m *serviceManager) CommentService(tx *persistence.DatabaseTx) CommentService {
    return &commentService{dbTxAware{tx: tx, db: m.db}}
}

The author traced how transactions were dragged through multiple layers of abstraction, only to discover that the entire complex system was unnecessary:

func (db *Database) Begin() (*DatabaseTx, error) {
    // SQLite doesn't support concurrent writes (see e.g. https://github.com/mattn/go-sqlite3/issues/50,
    // https://github.com/mattn/go-sqlite3/issues/1179), so we'll avoid using them altogether to prevent intermittent
    // failures
    if db.dialect == dbSQLite3 {
        return nil, ErrTxUnsupported
        return nil, err
    } else if gtx, err := db.goquDB().Begin(); err != nil {
    } else {
        return &DatabaseTx{tx: gtx}, nil
    }
}

The project's transaction handling ultimately defaulted to a simplified approach for databases that didn't support proper transactions, rendering the complex abstraction layer pointless.

The author concluded with a reflection on the broader implications: "The projects I've mentioned still required lots of work and those are not a junior-dev results. I'm sure all the people involved are gladly working as respectable devs and consultants. It's not that they are evil, it's the consumer quality bar dropped so low that this kind of sloppiness has become totally acceptable, or even a requirement when you are expected to deliver features first and maybe latter ask 'why?'."

This examination of self-hosted commenting systems reveals a troubling pattern in modern software development: the prioritization of features and speed over architectural soundness and code quality. As the author suggests, when "eventual consistency is much more important than atomicity or fault tolerance" for a commenting system, the choice of a fully-fledged ACID database with dropped consistency guarantees represents a fundamental misunderstanding of both requirements and technology.

The source for this analysis is available at: https://bykozy.me/blog/state-of-decay-in-self-hosted-commenting/