Skip to content
AB

MongoDB Behind a Repository Boundary

Keep ObjectIds, query documents, and persistence models out of business logic while preserving the capabilities MongoDB is good at.

Ashish BhakhandPublished

When application code imports MongoDB collections directly, database decisions spread quickly: ObjectId appears in domain types, update operators appear in use cases, and every test needs a collection-shaped mock.

A repository creates a focused boundary around persistence:

Code
export interface MemberRepository {
  findById(id: string): Promise<Member | null>;
  add(member: Member): Promise<void>;
  changeRole(input: { memberId: string; role: MemberRole }): Promise<boolean>;
}

The interface speaks in application language. Its MongoDB adapter owns collection names, BSON identifiers, projections, and update documents.

Code
async changeRole(input: ChangeRoleInput): Promise<boolean> {
  const result = await this.collection.updateOne(
    { _id: new ObjectId(input.memberId) },
    { $set: { role: input.role, updatedAt: new Date() } },
  );

  return result.matchedCount === 1;
}

Map persistence data explicitly#

Database documents often contain _id, schema-version fields, and denormalized values the domain does not need. Map them at the adapter:

Code
function toMember(document: MemberDocument): Member {
  return {
    id: document._id.toHexString(),
    organizationId: document.organizationId.toHexString(),
    role: document.role,
    joinedAt: document.joinedAt,
  };
}

Explicit mapping also gives older documents a controlled migration path instead of letting optional legacy fields infect every caller.

Do not hide MongoDB's strengths#

A repository should not become a generic CRUD interface. If a feature needs an atomic claim, conditional update, or aggregation, name that operation directly: claimPendingJob, incrementDailyUsage, or findOverdueSubscriptions.

These methods express intent while allowing the adapter to use indexes and atomic operators effectively.

Test both sides of the boundary#

Use-case tests mock the small repository contract. Adapter tests use a real MongoDB-compatible environment to prove query filters, mappings, indexes, and concurrency behavior. A mocked collection cannot prove a unique index works.

The repository boundary is valuable because it localizes persistence knowledge. It does not pretend all databases are interchangeable. Business rules become easy to read and test, while MongoDB-specific code remains free to use MongoDB well.

← Back to all writing