- gRPC service with MusicBrainz provider - PostgreSQL schema with migrations - Service layer with database-first caching - Repository pattern for data access - YAML configuration support - Research documentation for 17 music metadata projects
15 KiB
MusicBrainz Server Architecture
Design Pattern
Hybrid MVC + Service Layer architecture built on the Catalyst web framework. The application follows a layered approach with clear separation of concerns between presentation, business logic, and data access.
Directory Structure
lib/MusicBrainz/Server/
├── Controller/ # 53 controllers, 13,000 lines
│ ├── Artist.pm
│ ├── Release.pm
│ ├── Recording.pm
│ ├── WS/ # Web Service controllers
│ │ └── 2/ # API version 2
│ └── ...
├── Data/ # 106 modules, 26,000 lines
│ ├── Artist.pm
│ ├── Release.pm
│ ├── Recording.pm
│ ├── Relationship.pm
│ └── ...
├── Entity/ # 132 entity classes
│ ├── Artist.pm
│ ├── Release.pm
│ ├── Recording.pm
│ ├── Types.pm
│ └── ...
├── Form/ # 43 form handlers
│ ├── Artist.pm
│ ├── Release.pm
│ └── ...
├── View/ # 4 view modules
│ ├── Default.pm # Template Toolkit
│ ├── JSON.pm
│ ├── XML.pm
│ └── JSONLD.pm
├── WebService/ # API implementation
│ ├── Serializer/
│ │ ├── JSON/
│ │ ├── XML/
│ │ └── JSONLD/
│ └── Validator.pm
├── Edit/ # Edit system
│ ├── Artist/
│ ├── Release/
│ ├── Recording/
│ └── ...
├── Context.pm # Service layer coordinator
├── DBDefs.pm # Configuration
└── Sql.pm # SQL abstraction layer
admin/ # Database administration
├── sql/
│ ├── CreateTables.sql # Schema definition (4,068 lines)
│ └── updates/ # 332 migration files
root/ # Frontend assets
├── static/
│ ├── scripts/ # JavaScript source
│ │ ├── common/
│ │ ├── edit/
│ │ └── release/
│ ├── styles/ # CSS/LESS
│ └── images/
└── layout.tt # Main template
t/ # Tests
├── lib/ # Test utilities
├── pgtap/ # Database tests
└── selenium/ # Integration tests
Architectural Layers
Controller Layer (53 modules, 13,000 lines)
Responsibility: Handle HTTP requests, coordinate business logic, render responses.
Key Controllers:
Artist.pm- Artist entity operationsRelease.pm- Release entity operationsRecording.pm- Recording entity operationsReleaseGroup.pm- Release group operationsWork.pm- Work entity operationsLabel.pm- Label entity operationsEdit.pm- Edit submission and votingSearch.pm- Search interfaceWS::2::*- Web service API endpoints
Controller Pattern:
package MusicBrainz::Server::Controller::Artist;
use Moose;
BEGIN { extends 'MusicBrainz::Server::Controller' }
sub show : Path Args(1) {
my ($self, $c, $gid) = @_;
my $artist = $c->model('Artist')->get_by_gid($gid);
$c->stash( artist => $artist );
}
Responsibilities:
- Request validation
- Authentication/authorization checks
- Coordinate Data layer calls
- Prepare data for views
- Handle form submissions
Data Layer (106 modules, 26,000 lines)
Responsibility: Repository pattern for database access. Each entity has a corresponding Data module.
Key Data Modules:
Data::Artist- Artist CRUD operationsData::Release- Release CRUD operationsData::Recording- Recording CRUD operationsData::Relationship- Relationship managementData::Edit- Edit persistenceData::Search- Search operations
Data Module Pattern:
package MusicBrainz::Server::Data::Artist;
use Moose;
extends 'MusicBrainz::Server::Data::Entity';
sub _table { 'artist' }
sub _entity_class { 'MusicBrainz::Server::Entity::Artist' }
sub get_by_gid {
my ($self, $gid) = @_;
return $self->_get_by_key('gid', $gid);
}
Moose Roles:
Role::Editable- Entities that can be editedRole::Taggable- Entities that can be taggedRole::Rateable- Entities that can be ratedRole::Relatable- Entities that can have relationshipsRole::Aliasable- Entities that can have aliasesRole::Annotation- Entities that can be annotated
Data Access Pattern:
- No ORM (not DBIx::Class)
- Custom Moose-based abstraction
- Raw SQL via
DBD::Pg DBIx::Connectorfor connection poolingSql.pmprovides query builder utilities
Entity Layer (132 classes)
Responsibility: Domain objects representing database entities.
Key Entities:
Entity::Artist- Artist domain objectEntity::Release- Release domain objectEntity::Recording- Recording domain objectEntity::ReleaseGroup- Release group domain objectEntity::Work- Work domain objectEntity::Label- Label domain objectEntity::Relationship- Relationship between entities
Entity Pattern:
package MusicBrainz::Server::Entity::Artist;
use Moose;
extends 'MusicBrainz::Server::Entity';
has 'name' => ( is => 'rw', isa => 'Str' );
has 'sort_name' => ( is => 'rw', isa => 'Str' );
has 'type_id' => ( is => 'rw', isa => 'Maybe[Int]' );
has 'country_id' => ( is => 'rw', isa => 'Maybe[Int]' );
has 'begin_date' => ( is => 'rw', isa => 'PartialDate' );
has 'end_date' => ( is => 'rw', isa => 'PartialDate' );
Entity Characteristics:
- Immutable after construction (mostly)
- Type-safe via Moose type system
- Lazy loading of relationships
- No database logic (pure domain objects)
Form Layer (43 modules)
Responsibility: Form validation and processing using HTML::FormHandler.
Key Forms:
Form::Artist- Artist creation/editingForm::Release- Release creation/editingForm::Recording- Recording creation/editingForm::Edit::*- Edit-specific forms
Form Pattern:
package MusicBrainz::Server::Form::Artist;
use HTML::FormHandler::Moose;
extends 'MusicBrainz::Server::Form';
has_field 'name' => ( type => 'Text', required => 1 );
has_field 'sort_name' => ( type => 'Text', required => 1 );
has_field 'type_id' => ( type => 'Select' );
View Layer (4 modules)
Responsibility: Render responses in different formats.
Views:
View::Default- Template Toolkit for HTMLView::JSON- JSON serializationView::XML- XML serializationView::JSONLD- JSON-LD serialization
Edit System Architecture
Pattern: Command Pattern
Concept: All data modifications are represented as "edits" - versioned, votable changes that go through a review process.
Edit Lifecycle:
- User submits edit via form
- Edit is validated and persisted to
edittable - Edit enters voting period (typically 7 days)
- Community votes on edit (yes/no/abstain)
- Auto-editors can approve immediately
- Edit is applied or rejected based on votes
- Full audit trail maintained
Edit Types (examples):
Edit::Artist::Create- Create new artistEdit::Artist::Edit- Modify artist dataEdit::Artist::Delete- Delete artistEdit::Release::Create- Create new releaseEdit::Release::AddReleaseLabel- Add label to releaseEdit::Relationship::Create- Create relationshipEdit::Relationship::Edit- Modify relationshipEdit::Relationship::Delete- Delete relationship
Edit Structure:
package MusicBrainz::Server::Edit::Artist::Edit;
use Moose;
extends 'MusicBrainz::Server::Edit';
sub edit_type { 1 } # Unique edit type ID
sub edit_name { 'Edit artist' }
sub initialize {
my ($self, %opts) = @_;
# Store old and new data
$self->data({
entity_id => $opts{artist_id},
old => { ... },
new => { ... },
});
}
sub accept {
my $self = shift;
# Apply the edit
$self->c->model('Artist')->update($self->data->{entity_id}, $self->data->{new});
}
Edit Data Storage:
edittable - Edit metadata (type, status, votes)edit_datatable - Edit-specific data (JSON)votetable - User votes on edits
Edit Statuses:
- Open - Awaiting votes
- Applied - Accepted and applied
- Failed Vote - Rejected by community
- Failed Dependency - Dependent edit failed
- Error - Application error
- Deleted - Cancelled by submitter
Serialization Architecture
JSON Serializer
Location: lib/MusicBrainz/Server/WebService/Serializer/JSON/2/
Modules:
Artist.pm- Artist JSON serializationRelease.pm- Release JSON serializationRecording.pm- Recording JSON serializationUtils.pm- Common serialization utilities
Pattern:
sub serialize {
my ($self, $entity, $inc, $opts) = @_;
my $data = {
id => $entity->gid,
name => $entity->name,
'sort-name' => $entity->sort_name,
};
if ($inc->artist_credits) {
$data->{'artist-credit'} = $self->serialize_artist_credit($entity->artist_credit);
}
return $data;
}
XML Serializer
Location: lib/MusicBrainz/Server/WebService/Serializer/XML/2/
Namespace: http://musicbrainz.org/ns/mmd-2.0#
Pattern:
sub serialize {
my ($self, $entity, $inc, $opts) = @_;
my $xml = XML::LibXML::Element->new('artist');
$xml->setAttribute('id', $entity->gid);
$xml->appendTextChild('name', $entity->name);
$xml->appendTextChild('sort-name', $entity->sort_name);
return $xml;
}
JSON-LD Serializer
Location: lib/MusicBrainz/Server/WebService/Serializer/JSONLD/
Context: Schema.org vocabulary
Pattern:
sub serialize {
my ($self, $entity) = @_;
return {
'@context' => 'http://schema.org',
'@type' => 'MusicGroup',
'@id' => 'https://musicbrainz.org/artist/' . $entity->gid,
'name' => $entity->name,
};
}
Frontend Architecture
Template Toolkit (Server-Side Rendering)
Location: root/
Main Template: root/layout.tt
Template Structure:
root/
├── layout.tt # Main layout
├── artist/
│ ├── index.tt # Artist listing
│ ├── show.tt # Artist detail
│ └── edit.tt # Artist edit form
├── release/
│ ├── index.tt
│ ├── show.tt
│ └── edit.tt
└── components/
├── header.tt
├── footer.tt
└── sidebar.tt
Template Pattern:
[% WRAPPER 'layout.tt' title=artist.name %]
<h1>[% artist.name %]</h1>
<p>Sort name: [% artist.sort_name %]</p>
[% IF artist.releases.size %]
<h2>Releases</h2>
<ul>
[% FOR release IN artist.releases %]
<li><a href="/release/[% release.gid %]">[% release.name %]</a></li>
[% END %]
</ul>
[% END %]
[% END %]
React (Progressive Enhancement)
Location: root/static/scripts/
Strategy: Progressive enhancement - server renders HTML, React hydrates for interactivity.
Component Structure:
root/static/scripts/
├── common/
│ ├── components/
│ │ ├── EntityLink.js
│ │ ├── Autocomplete.js
│ │ └── ReleaseList.js
│ └── utility/
├── edit/
│ ├── components/
│ │ ├── EditNote.js
│ │ └── VotingSection.js
│ └── reducers/
└── release/
├── components/
│ ├── ReleaseHeader.js
│ └── TrackList.js
└── reducers/
React Pattern:
import React from 'react';
import ReactDOM from 'react-dom';
const ReleaseList = ({ releases }) => (
<ul>
{releases.map(release => (
<li key={release.gid}>
<a href={`/release/${release.gid}`}>{release.name}</a>
</li>
))}
</ul>
);
// Hydrate server-rendered content
const container = document.getElementById('release-list');
if (container) {
const releases = JSON.parse(container.dataset.releases);
ReactDOM.hydrate(<ReleaseList releases={releases} />, container);
}
Legacy Knockout.js
Status: Being phased out, but still present in some views.
Location: root/static/scripts/ (mixed with React)
Pattern:
ko.applyBindings({
releases: ko.observableArray([...]),
addRelease: function() { ... }
});
Service Layer (Context)
File: lib/MusicBrainz/Server/Context.pm
Responsibility: Coordinate operations across multiple Data modules, manage transactions, provide unified interface.
Pattern:
my $artist = $c->model('Artist')->get_by_gid($gid);
$c->model('ArtistCredit')->load($artist);
$c->model('Release')->load_for_artist($artist);
$c->model('Relationship')->load($artist);
Context Provides:
- Database connection management
- Transaction handling
- Model access (
$c->model('Artist')) - Configuration access (
$c->config) - Session management
- Request/response handling
Key Design Patterns
Repository Pattern
Implementation: Data layer modules
Purpose: Abstract database access, provide clean interface for entity operations.
Example:
# Instead of raw SQL everywhere:
my $artist = $c->model('Artist')->get_by_gid($gid);
# Data::Artist handles the SQL:
sub get_by_gid {
my ($self, $gid) = @_;
return $self->sql->select_single_row_hash(
'SELECT * FROM artist WHERE gid = ?', $gid
);
}
Command Pattern
Implementation: Edit system
Purpose: Encapsulate all data modifications as objects, enabling undo, audit trails, and voting.
Example:
my $edit = $c->model('Edit')->create(
edit_type => $EDIT_ARTIST_EDIT,
editor_id => $c->user->id,
artist_id => $artist->id,
old => { name => 'Old Name' },
new => { name => 'New Name' },
);
Service Pattern
Implementation: Context object
Purpose: Coordinate operations across multiple repositories, manage transactions.
Example:
$c->model('MB')->with_transaction(sub {
my $artist = $c->model('Artist')->insert({ name => 'New Artist' });
$c->model('Edit')->create(
edit_type => $EDIT_ARTIST_CREATE,
entity_id => $artist->id,
);
});
Data Access Layer
No ORM: MusicBrainz does not use DBIx::Class or any traditional ORM.
Custom Abstraction:
- Moose-based Data modules
- Raw SQL via
DBD::Pg DBIx::Connectorfor connection poolingSql.pmprovides query builder utilities
Rationale:
- Performance - Direct SQL is faster
- Flexibility - Complex queries easier to write
- Control - Full control over query execution
- Legacy - Codebase predates modern ORMs
SQL Abstraction Example:
# lib/MusicBrainz/Server/Data/Sql.pm
sub select_single_row_hash {
my ($self, $query, @args) = @_;
my $row = $self->dbh->selectrow_hashref($query, undef, @args);
return $row;
}
sub select_list_of_hashes {
my ($self, $query, @args) = @_;
my $rows = $self->dbh->selectall_arrayref($query, { Slice => {} }, @args);
return $rows;
}