Upload
bora-tunca
View
773
Download
3
Embed Size (px)
DESCRIPTION
A brief look at API architecture of SoundCloud and examples of how we use Scala to implement our APIs
Citation preview
Product Engineering
Bora
SoundCloud Public API
● for external developers● no built-in assumptions
Growing
● api team became a bottleneck● undocumented endpoints
Client Specific APIs
Reduce Chattiness
soundcloud.com/explore/explore/categories
API
/explore/{category}
/tracks?ids={ids}
/visuals/{ids}
soundcloud.com/explore/explore
API
Remove Dependencies
SC Microservices
Public API
SC Microservices
API-MobileAPI-V2 API-Embedded API-Partners API-*
● Dealing with the Monolith● Breaking the Monolith● Microservices in Scala and Finagle
Building Products at SoundCloud
Java Interoperability
SC Microservices
API-MobileAPI-V2 API-Embedded API-Partners API-*
BFF
JVM-KIT
Futures
SC Microservices
API-MobileAPI-V2 API-Embedded API-Partners API-*
def userTracks(userUrn: Urn): Future[List[Track]] = {
for {
trackUrns <- userTrackRepo.trackUrns(userUrn)
tracks <- trackRepo.tracks(trackUrns)
} yield tracks
}
val resources: List[Future[List[Resource]]] = List(
trackRepo.tracks(trackUrns),
userRepo.users(userUrns),
playlistRepo.playlists(playlistUrns),
groupRepo.groups(groupUrns))
Future.collect(resources).map {
case List(tracks, users, playlists, groups) => ...
}
Traits
abstract class JsonMapping(val trackJson: JsValue)
trait MiniTrack extends JsonMapping {
val urn = Urn((trackJson \ "self" \ "urn").as[String])
val title = (trackJson \ "title").as[String]
}
trait Stats extends JsonMapping {
val play_count = (trackJson \ "play_count").as[Int]
val like_count = (trackJson \ "like_count").as[Int]
}
trait Artwork extends JsonMapping {
val artwork_url = (trackJson \ "artwork_url").as[String]
}
new JsonMapping(trackJson)
with MiniTrack
with Stats
with Artwork
...
new JsonMapping(trackJson)
with MiniTrack
with Stats
...
new JsonMapping(trackJson)
with MiniTrack
with Scheduling
Higher-order Functions
trait TracksController extends BaseController {
get("/tracks/:urn") {
request =>
val trackUrn = Urn(request.routeParams("urn"))
trackRepo.track(trackUrn).map(render)
}
}
...
def get(path: String)(callback: RequestHandler) =
register(path, HttpMethod.GET, callback)
type RequestHandler = Request => Future[Response]
Collections
// Playlist has a tracks : List[Tracks]
val playlists: List[Playlist]
val emptyPlaylists =
playlists.filter(_.tracks.isEmpty)
val allTracks =
playlists.map(_.tracks).flatten
val tracksByCreator: Map[User, List[Track]] =
allTracks.groupBy(_.creator)
val trackCount =
playlists.map(_.tracks.size).sum
Case Classes & Pattern Matching
case class JsonResponse(
status: StatusCode,
body: JsValue,
headers: HttpHeaders = HttpHeaders.EMPTY_HEADERS,
pagination: Pagination = Pagination.empty
)
def tracks(urns: List[Urn]) = {
fetch(Path() / "tracks", urns).map {
case JsonResponse(OkStatus, body, _, _) => body
case JsonResponse(NotFoundStatus, _, _, _) => ...
case JsonResponse(UnauthorizedStatus, _, _, _)
=> ...
case _ => throw ...
}
}
class Urn implements Serializable, Comparable<Urn>//JAVA
Interop
object Urn {
...
def unapply(obj: Urn): Option[(String, String, String)]
=
Some(obj.getNamespace, obj.getCollection,
obj.getIdentifier)
}
urn match {
case Urn(_, "tracks", _) => ...
case Urn(_, "playlists", _) => …
case Urn(_, "comments", _) => ...
}
Implicits
def tracks(urns: List[Urn]) =
fetch(Path() / "tracks", urns)
...
def fetch(path: Path, params: Params)
// defined in the package object
implicit def urnsToParams(urns: Iterable[Urn]): Params =
Params("urns" -> urns)
…
// we can still use make this
fetch(Path() / "tracks", Params("ids" -> ids))
XML!!!
trait AssignmentsController extends V2Controller with ExceptionHandler {class TracksXmlVisitor(val wrapped: Node)
extends TracksVisitor {
type TrackType = XmlTrack
def apply(visit: VisitTrack): Option[Node] =
apply(wrapped, visit)
private def apply(node: Node, visit: VisitTrack): Option[Node] = {
node match {
case node: Node if (isTrack(node)) =>
visitTrack(node, visit)
case node: Elem =>
val att = node.attributes
val children = node.child.map {
case text: Text =>
Some(text)
case other =>
apply(other, visit)
}.flatten
Some(new Elem(node.prefix, node.label, node.attributes, node.scope, true, children: _*))
case other =>
Some(other)
}
}
private def visitTrack(node: Node, visit: VisitTrack) = {
val id = (node \ "id").text.toInt
val urn = Urn(s"soundcloud:tracks:$id")
visit(urn, XmlTrack(node))
}
private def isTrack(node: Node) =
(node \ "kind").text == "track"
}